├── .codecov.yaml ├── .codespellrc ├── .coveragerc ├── .cruft.json ├── .flake8 ├── .github └── workflows │ ├── ci.yml │ ├── label_sync.yml │ ├── scheduled_builds.yml │ ├── stale_bot.yml │ └── sub_package_update.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .rtd-environment.yml ├── .ruff.toml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── changelog └── README.rst ├── docs ├── Makefile ├── api.rst ├── coc.rst ├── conf.py ├── dev_guide │ ├── index.rst │ ├── query.rst │ ├── tables.rst │ └── working.rst ├── how_to │ ├── index.rst │ └── wavelength.rst ├── index.rst ├── make.bat ├── nitpick-exceptions └── whatsnew │ ├── changelog.rst │ └── index.rst ├── examples ├── README.txt ├── detector_wavelength.py ├── quick.py ├── search_attrs.py └── solar_distance_query.py ├── licenses ├── LICENSE.rst └── README.rst ├── pyproject.toml ├── pytest.ini ├── setup.py ├── sunpy_soar ├── __init__.py ├── _dev │ ├── __init__.py │ └── scm_version.py ├── attrs.py ├── client.py ├── conftest.py ├── data │ ├── README.rst │ ├── attrs.json │ ├── instrument_attrs.json │ └── soop_attrs.json ├── tests │ ├── __init__.py │ └── test_sunpy_soar.py └── version.py ├── tools └── update_data.py └── tox.ini /.codecov.yaml: -------------------------------------------------------------------------------- 1 | comment: off 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | threshold: 0.2% 7 | 8 | codecov: 9 | require_ci_to_pass: false 10 | notify: 11 | wait_for_ci: true 12 | -------------------------------------------------------------------------------- /.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | skip = *.asdf,*.fits,*.fts,*.header,*.json,*.xsh,*cache*,*egg*,*extern*,.git,.idea,.tox,_build,*truncated,*.svg,.asv_env,.history 3 | ignore-words-list = 4 | alog, 5 | nd, 6 | nin, 7 | observ, 8 | ot, 9 | te, 10 | upto, 11 | afile, 12 | precessed, 13 | precess, 14 | soop 15 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | sunpy_soar/conftest.py 4 | sunpy_soar/*setup_package* 5 | sunpy_soar/extern/* 6 | sunpy_soar/version* 7 | */sunpy_soar/conftest.py 8 | */sunpy_soar/*setup_package* 9 | */sunpy_soar/extern/* 10 | */sunpy_soar/version* 11 | 12 | [report] 13 | exclude_lines = 14 | # Have to re-enable the standard pragma 15 | pragma: no cover 16 | # Don't complain about packages we have installed 17 | except ImportError 18 | # Don't complain if tests don't hit assertions 19 | raise AssertionError 20 | raise NotImplementedError 21 | # Don't complain about script hooks 22 | def main(.*): 23 | # Ignore branches that don't pertain to this version of Python 24 | pragma: py{ignore_python_version} 25 | # Don't complain about IPython completion helper 26 | def _ipython_key_completions_ 27 | # typing.TYPE_CHECKING is False at runtime 28 | if TYPE_CHECKING: 29 | # Ignore typing overloads 30 | @overload 31 | -------------------------------------------------------------------------------- /.cruft.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "https://github.com/sunpy/package-template", 3 | "commit": "1bdd28c1e2d725d9ae9d9c0b6ad682d75687f45d", 4 | "checkout": null, 5 | "context": { 6 | "cookiecutter": { 7 | "package_name": "sunpy-soar", 8 | "module_name": "sunpy_soar", 9 | "short_description": "A sunpy FIDO plugin for accessing data in the Solar Orbiter Archive (SOAR).", 10 | "author_name": "The SunPy Community", 11 | "author_email": "sunpy@googlegroups.com", 12 | "project_url": "https://sunpy.org", 13 | "github_repo": "sunpy/sunpy-soar", 14 | "sourcecode_url": "https://github.com/sunpy/sunpy-soar", 15 | "download_url": "https://pypi.org/project/sunpy-soar", 16 | "documentation_url": "https://docs.sunpy.org/projects/sunpy-soar", 17 | "changelog_url": "https://docs.sunpy.org/projects/sunpy-soar/en/stable/whatsnew/changelog.html", 18 | "issue_tracker_url": "https://github.com/sunpy/sunpy-soar/issues", 19 | "license": "BSD 2-Clause", 20 | "minimum_python_version": "3.10", 21 | "use_compiled_extensions": "n", 22 | "enable_dynamic_dev_versions": "y", 23 | "include_example_code": "n", 24 | "include_cruft_update_github_workflow": "y", 25 | "use_extended_ruff_linting": "y", 26 | "_sphinx_theme": "sunpy", 27 | "_parent_project": "", 28 | "_install_requires": "", 29 | "_copy_without_render": [ 30 | "docs/_templates", 31 | "docs/_static", 32 | ".github/workflows/sub_package_update.yml" 33 | ], 34 | "_template": "https://github.com/sunpy/package-template", 35 | "_commit": "1bdd28c1e2d725d9ae9d9c0b6ad682d75687f45d" 36 | } 37 | }, 38 | "directory": null 39 | } 40 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | # missing-whitespace-around-operator 4 | E225 5 | # missing-whitespace-around-arithmetic-operator 6 | E226 7 | # line-too-long 8 | E501 9 | # unused-import 10 | F401 11 | # undefined-local-with-import-star 12 | F403 13 | # redefined-while-unused 14 | F811 15 | # Line break occurred before a binary operator 16 | W503, 17 | # Line break occurred after a binary operator 18 | W504 19 | max-line-length = 110 20 | exclude = 21 | .git 22 | __pycache__ 23 | docs/conf.py 24 | build 25 | sunpy_soar/__init__.py 26 | rst-directives = 27 | plot 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Main CI Workflow 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - 'main' 8 | - '*.*' 9 | - '!*backport*' 10 | tags: 11 | - 'v*' 12 | - '!*dev*' 13 | - '!*pre*' 14 | - '!*post*' 15 | pull_request: 16 | # Allow manual runs through the web UI 17 | workflow_dispatch: 18 | schedule: 19 | # ┌───────── minute (0 - 59) 20 | # │ ┌───────── hour (0 - 23) 21 | # │ │ ┌───────── day of the month (1 - 31) 22 | # │ │ │ ┌───────── month (1 - 12 or JAN-DEC) 23 | # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT) 24 | - cron: '0 7 * * 3' # Every Wed at 07:00 UTC 25 | 26 | concurrency: 27 | group: ${{ github.workflow }}-${{ github.ref }} 28 | cancel-in-progress: true 29 | 30 | jobs: 31 | core: 32 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 33 | with: 34 | submodules: false 35 | coverage: codecov 36 | toxdeps: tox-pypi-filter 37 | posargs: -n auto 38 | envs: | 39 | - linux: py313 40 | secrets: 41 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 42 | 43 | sdist_verify: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-python@v5 48 | with: 49 | python-version: '3.13' 50 | - run: python -m pip install -U --user build 51 | - run: python -m build . --sdist 52 | - run: python -m pip install -U --user twine 53 | - run: python -m twine check dist/* 54 | 55 | test: 56 | needs: [core, sdist_verify] 57 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 58 | with: 59 | submodules: false 60 | coverage: codecov 61 | toxdeps: tox-pypi-filter 62 | posargs: -n auto 63 | envs: | 64 | - windows: py311 65 | - macos: py312 66 | - linux: py310-oldestdeps 67 | - linux: py313-devdeps 68 | secrets: 69 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 70 | 71 | docs: 72 | needs: [core] 73 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 74 | with: 75 | default_python: '3.13' 76 | submodules: false 77 | pytest: false 78 | toxdeps: tox-pypi-filter 79 | cache-path: | 80 | docs/_build/ 81 | docs/generated/ 82 | .tox/sample_data/ 83 | cache-key: docs-${{ github.run_id }} 84 | libraries: | 85 | apt: 86 | - graphviz 87 | envs: | 88 | - linux: build_docs 89 | 90 | publish: 91 | # Build wheels on PRs only when labelled. Releases will only be published if tagged ^v.* 92 | # see https://github-actions-workflows.openastronomy.org/en/latest/publish.html#upload-to-pypi 93 | if: | 94 | github.event_name != 'pull_request' || 95 | ( 96 | github.event_name == 'pull_request' && 97 | contains(github.event.pull_request.labels.*.name, 'Run publish') 98 | ) 99 | needs: [test, docs] 100 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish_pure_python.yml@v1 101 | with: 102 | python-version: '3.13' 103 | test_extras: 'tests' 104 | test_command: 'pytest -p no:warnings --doctest-rst --pyargs sunpy_soar' 105 | submodules: false 106 | secrets: 107 | pypi_token: ${{ secrets.pypi_token }} 108 | -------------------------------------------------------------------------------- /.github/workflows/label_sync.yml: -------------------------------------------------------------------------------- 1 | name: Label Sync 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | # ┌───────── minute (0 - 59) 6 | # │ ┌───────── hour (0 - 23) 7 | # │ │ ┌───────── day of the month (1 - 31) 8 | # │ │ │ ┌───────── month (1 - 12 or JAN-DEC) 9 | # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT) 10 | - cron: '0 0 * * *' # run every day at midnight UTC 11 | 12 | # Give permissions to write issue labels 13 | permissions: 14 | issues: write 15 | 16 | jobs: 17 | label_sync: 18 | runs-on: ubuntu-latest 19 | name: Label Sync 20 | steps: 21 | - uses: srealmoreno/label-sync-action@850ba5cef2b25e56c6c420c4feed0319294682fd 22 | with: 23 | config-file: https://raw.githubusercontent.com/sunpy/.github/main/labels.yml 24 | -------------------------------------------------------------------------------- /.github/workflows/scheduled_builds.yml: -------------------------------------------------------------------------------- 1 | name: Scheduled builds 2 | 3 | on: 4 | # Allow manual runs through the web UI 5 | workflow_dispatch: 6 | schedule: 7 | # ┌───────── minute (0 - 59) 8 | # │ ┌───────── hour (0 - 23) 9 | # │ │ ┌───────── day of the month (1 - 31) 10 | # │ │ │ ┌───────── month (1 - 12 or JAN-DEC) 11 | # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT) 12 | - cron: '0 7 * * *' # Every day at 07:00 UTC 13 | 14 | jobs: 15 | dispatch_workflows: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - run: gh workflow run ci.yml --repo sunpy/sunpy-soar --ref main 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/stale_bot.yml: -------------------------------------------------------------------------------- 1 | name: Stale Bot 2 | 3 | on: 4 | schedule: 5 | # ┌───────── minute (0 - 59) 6 | # │ ┌───────── hour (0 - 23) 7 | # │ │ ┌───────── day of the month (1 - 31) 8 | # │ │ │ ┌───────── month (1 - 12 or JAN-DEC) 9 | # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT) 10 | - cron: '0 5 * * *' # Every day at 05:00 UTC 11 | 12 | jobs: 13 | stale: 14 | permissions: 15 | issues: write 16 | pull-requests: write 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/stale@v8 20 | with: 21 | repo-token: ${{ secrets.GITHUB_TOKEN }} 22 | operations-per-run: 50 23 | days-before-issue-stale: 99999999 # Never mark issues as stale 24 | days-before-pr-stale: 150 25 | days-before-close: 30 26 | stale-pr-label: 'Stale' 27 | stale-pr-message: >- 28 | Hello :wave:, 29 | Thanks for your contribution to sunpy! 30 | 31 | I have marked this pull request as stale because there hasn't had any activity in five months. 32 | If you are still working on this, or if it's waiting on a maintainer to look at it then please let us know and we will keep it open. 33 | Please add a comment with: @sunpy/sunpy-developers to get someone's attention. 34 | 35 | If nobody comments on this pull request for another month, it will be closed. 36 | close-pr-message: >- 37 | Hello again :wave:, 38 | We want to thank you again for your contribution to sunpy! 39 | 40 | This pull request has had no activity since my last reminder, so I am going to close it. 41 | If at any time you want to come back to this please feel free to reopen it! 42 | If you want to discuss this, please add a comment with: @sunpy/sunpy-developers and someone will get back to you soon. 43 | exempt-pr-labels: 'Keep Open' 44 | -------------------------------------------------------------------------------- /.github/workflows/sub_package_update.yml: -------------------------------------------------------------------------------- 1 | # This template is taken from the cruft example code, for further information please see: 2 | # https://cruft.github.io/cruft/#automating-updates-with-github-actions 3 | name: Automatic Update from package template 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | on: 9 | # Allow manual runs through the web UI 10 | workflow_dispatch: 11 | schedule: 12 | # ┌───────── minute (0 - 59) 13 | # │ ┌───────── hour (0 - 23) 14 | # │ │ ┌───────── day of the month (1 - 31) 15 | # │ │ │ ┌───────── month (1 - 12 or JAN-DEC) 16 | # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT) 17 | - cron: '0 7 * * 1' # Every Monday at 7am UTC 18 | 19 | jobs: 20 | update: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | fail-fast: true 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - uses: actions/setup-python@v5 28 | with: 29 | python-version: "3.11" 30 | 31 | - name: Install Cruft 32 | run: python -m pip install git+https://github.com/Cadair/cruft@patch-p1 33 | 34 | - name: Check if update is available 35 | continue-on-error: false 36 | id: check 37 | run: | 38 | CHANGES=0 39 | if [ -f .cruft.json ]; then 40 | if ! cruft check; then 41 | CHANGES=1 42 | fi 43 | else 44 | echo "No .cruft.json file" 45 | fi 46 | 47 | echo "has_changes=$CHANGES" >> "$GITHUB_OUTPUT" 48 | 49 | - name: Run update if available 50 | id: cruft_update 51 | if: steps.check.outputs.has_changes == '1' 52 | run: | 53 | git config --global user.email "${{ github.actor }}@users.noreply.github.com" 54 | git config --global user.name "${{ github.actor }}" 55 | 56 | cruft_output=$(cruft update --skip-apply-ask --refresh-private-variables) 57 | echo $cruft_output 58 | git restore --staged . 59 | 60 | if [[ "$cruft_output" == *"Failed to cleanly apply the update, there may be merge conflicts."* ]]; then 61 | echo merge_conflicts=1 >> $GITHUB_OUTPUT 62 | else 63 | echo merge_conflicts=0 >> $GITHUB_OUTPUT 64 | fi 65 | 66 | - name: Check if only .cruft.json is modified 67 | id: cruft_json 68 | if: steps.check.outputs.has_changes == '1' 69 | run: | 70 | git status --porcelain=1 71 | if [[ "$(git status --porcelain=1)" == " M .cruft.json" ]]; then 72 | echo "Only .cruft.json is modified. Exiting workflow early." 73 | echo "has_changes=0" >> "$GITHUB_OUTPUT" 74 | else 75 | echo "has_changes=1" >> "$GITHUB_OUTPUT" 76 | fi 77 | 78 | - name: Create pull request 79 | if: steps.cruft_json.outputs.has_changes == '1' 80 | uses: peter-evans/create-pull-request@v7 81 | with: 82 | token: ${{ secrets.GITHUB_TOKEN }} 83 | add-paths: "." 84 | commit-message: "Automatic package template update" 85 | branch: "cruft/update" 86 | delete-branch: true 87 | draft: ${{ steps.cruft_update.outputs.merge_conflicts == '1' }} 88 | title: "Updates from the package template" 89 | labels: | 90 | No Changelog Entry Needed 91 | body: | 92 | This is an autogenerated PR, which will applies the latest changes from the [SunPy Package Template](https://github.com/sunpy/package-template). 93 | If this pull request has been opened as a draft there are conflicts which need fixing. 94 | 95 | **To run the CI on this pull request you will need to close it and reopen it.** 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | tmp/ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | pip-wheel-metadata/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | sunpy_soar/_version.py 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | # automodapi 78 | docs/api 79 | docs/sg_execution_times.rst 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Rope project settings 132 | .ropeproject 133 | 134 | # mkdocs documentation 135 | /site 136 | 137 | # mypy 138 | .mypy_cache/ 139 | 140 | # Pyre type checker 141 | .pyre/ 142 | 143 | # IDE 144 | # PyCharm 145 | .idea 146 | 147 | # Spyder project settings 148 | .spyderproject 149 | .spyproject 150 | 151 | ### VScode: https://raw.githubusercontent.com/github/gitignore/master/Global/VisualStudioCode.gitignore 152 | .vscode/* 153 | .vs/* 154 | 155 | ### https://raw.github.com/github/gitignore/master/Global/OSX.gitignore 156 | .DS_Store 157 | .AppleDouble 158 | .LSOverride 159 | 160 | # Icon must ends with two \r. 161 | Icon 162 | 163 | # Thumbnails 164 | ._* 165 | 166 | # Files that might appear on external disk 167 | .Spotlight-V100 168 | .Trashes 169 | 170 | ### Linux: https://raw.githubusercontent.com/github/gitignore/master/Global/Linux.gitignore 171 | *~ 172 | 173 | # temporary files which can be created if a process still has a handle open of a deleted file 174 | .fuse_hidden* 175 | 176 | # KDE directory preferences 177 | .directory 178 | 179 | # Linux trash folder which might appear on any partition or disk 180 | .Trash-* 181 | 182 | # .nfs files are created when an open file is removed but is still being accessed 183 | .nfs* 184 | 185 | # pytype static type analyzer 186 | .pytype/ 187 | 188 | # General 189 | .DS_Store 190 | .AppleDouble 191 | .LSOverride 192 | 193 | # Icon must end with two \r 194 | Icon 195 | 196 | 197 | # Thumbnails 198 | ._* 199 | 200 | # Files that might appear in the root of a volume 201 | .DocumentRevisions-V100 202 | .fseventsd 203 | .Spotlight-V100 204 | .TemporaryItems 205 | .Trashes 206 | .VolumeIcon.icns 207 | .com.apple.timemachine.donotpresent 208 | 209 | # Directories potentially created on remote AFP share 210 | .AppleDB 211 | .AppleDesktop 212 | Network Trash Folder 213 | Temporary Items 214 | .apdisk 215 | 216 | ### Windows: https://raw.githubusercontent.com/github/gitignore/master/Global/Windows.gitignore 217 | 218 | # Windows thumbnail cache files 219 | Thumbs.db 220 | ehthumbs.db 221 | ehthumbs_vista.db 222 | 223 | # Dump file 224 | *.stackdump 225 | 226 | # Folder config file 227 | [Dd]esktop.ini 228 | 229 | # Recycle Bin used on file shares 230 | $RECYCLE.BIN/ 231 | 232 | # Windows Installer files 233 | *.cab 234 | *.msi 235 | *.msix 236 | *.msm 237 | *.msp 238 | 239 | # Windows shortcuts 240 | *.lnk 241 | 242 | ### Extra Python Items and Package Specific 243 | docs/whatsnew/latest_changelog.txt 244 | examples/**/*.csv 245 | figure_test_images* 246 | tags 247 | baseline 248 | docs/generated/ 249 | docs/sg_execution_times.rst 250 | 251 | # Release script 252 | .github_cache 253 | 254 | # Misc Stuff 255 | .history 256 | *.orig 257 | .tmp 258 | node_modules/ 259 | package-lock.json 260 | package.json 261 | .prettierrc 262 | *.swp 263 | 264 | # Log files generated by 'vagrant up' 265 | *.log 266 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | # This should be before any formatting hooks like isort 3 | - repo: https://github.com/astral-sh/ruff-pre-commit 4 | rev: "v0.11.12" 5 | hooks: 6 | - id: ruff 7 | args: ["--fix"] 8 | - repo: https://github.com/PyCQA/isort 9 | rev: 6.0.1 10 | hooks: 11 | - id: isort 12 | exclude: ".*(.fits|.fts|.fit|.header|.txt|tca.*|extern.*|{{ cookiecutter.module_name }}/extern)$" 13 | - repo: https://github.com/pre-commit/pre-commit-hooks 14 | rev: v5.0.0 15 | hooks: 16 | - id: check-ast 17 | - id: check-case-conflict 18 | - id: trailing-whitespace 19 | exclude: ".*(.fits|.fts|.fit|.header|.txt)$" 20 | - id: check-yaml 21 | - id: debug-statements 22 | - id: check-added-large-files 23 | args: ["--enforce-all", "--maxkb=1054"] 24 | - id: end-of-file-fixer 25 | exclude: ".*(.fits|.fts|.fit|.header|.txt|tca.*|.json)$|^CITATION.rst$" 26 | - id: mixed-line-ending 27 | exclude: ".*(.fits|.fts|.fit|.header|.txt|tca.*)$" 28 | - repo: https://github.com/codespell-project/codespell 29 | rev: v2.4.1 30 | hooks: 31 | - id: codespell 32 | args: [ "--write-changes" ] 33 | ci: 34 | autofix_prs: false 35 | autoupdate_schedule: "quarterly" 36 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-lts-latest 5 | tools: 6 | python: "mambaforge-latest" 7 | jobs: 8 | post_checkout: 9 | - git fetch --unshallow || true 10 | pre_install: 11 | - git update-index --assume-unchanged .rtd-environment.yml docs/conf.py 12 | 13 | conda: 14 | environment: .rtd-environment.yml 15 | 16 | sphinx: 17 | builder: html 18 | configuration: docs/conf.py 19 | fail_on_warning: false 20 | 21 | formats: 22 | - htmlzip 23 | 24 | python: 25 | install: 26 | - method: pip 27 | extra_requirements: 28 | - docs 29 | path: . 30 | -------------------------------------------------------------------------------- /.rtd-environment.yml: -------------------------------------------------------------------------------- 1 | name: rtd_soar 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.12 6 | - pip 7 | - graphviz!=2.42.*,!=2.43.* 8 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | target-version = "py310" 2 | line-length = 120 3 | exclude=[ 4 | ".git,", 5 | "__pycache__", 6 | "build", 7 | "tools/**", 8 | ] 9 | 10 | [lint] 11 | select = [ 12 | "E", 13 | "F", 14 | "W", 15 | "N", 16 | "UP", 17 | "PT", 18 | "BLE", 19 | "A", 20 | "C4", 21 | "INP", 22 | "PIE", 23 | "T20", 24 | "RET", 25 | "TID", 26 | "PTH", 27 | "PD", 28 | "PLC", 29 | "PLE", 30 | "FLY", 31 | "NPY", 32 | "PERF", 33 | "RUF", 34 | ] 35 | 36 | extend-ignore = [ 37 | # pycodestyle (E, W) 38 | "E501", # ignore line length will use a formatter instead 39 | # pyupgrade (UP) 40 | "UP038", # Use | in isinstance - not compatible with models and is slower 41 | # pytest (PT) 42 | "PT001", # Always use pytest.fixture() 43 | "PT023", # Always use () on pytest decorators 44 | # flake8-pie (PIE) 45 | "PIE808", # Disallow passing 0 as the first argument to range 46 | # flake8-use-pathlib (PTH) 47 | "PTH123", # open() should be replaced by Path.open() 48 | # Ruff (RUF) 49 | "RUF003", # Ignore ambiguous quote marks, doesn't allow ' in comments 50 | "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` 51 | "RUF013", # PEP 484 prohibits implicit `Optional` 52 | "RUF015", # Prefer `next(iter(...))` over single element slice 53 | ] 54 | 55 | [lint.per-file-ignores] 56 | "examples/*.py" = [ 57 | "INP001", # Implicit namespace package 58 | "T201", # Use print 59 | "B018", # Not print but display 60 | "ERA001", # Commented out code 61 | ] 62 | "setup.py" = [ 63 | "INP001", # File is part of an implicit namespace package. 64 | ] 65 | "conftest.py" = [ 66 | "INP001", # File is part of an implicit namespace package. 67 | ] 68 | "docs/conf.py" = [ 69 | "E402", # Module imports not at top of file 70 | "INP001", # conf.py is part of an implicit namespace package 71 | ] 72 | "docs/*.py" = [ 73 | "INP001", # File is part of an implicit namespace package. 74 | ] 75 | "examples/**.py" = [ 76 | "T201", # allow use of print in examples 77 | "INP001", # File is part of an implicit namespace package. 78 | ] 79 | "__init__.py" = [ 80 | "E402", # Module level import not at top of cell 81 | "F401", # Unused import 82 | "F403", # from {name} import * used; unable to detect undefined names 83 | "F405", # {name} may be undefined, or defined from star imports 84 | ] 85 | "test_*.py" = [ 86 | "E402", # Module level import not at top of cell 87 | ] 88 | 89 | [lint.pydocstyle] 90 | convention = "numpy" 91 | 92 | [format] 93 | docstring-code-format = true 94 | indent-style = "space" 95 | quote-style = "double" 96 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 1.11.1 (2025-03-12) 2 | =================== 3 | 4 | Documentation 5 | ------------- 6 | 7 | - Added developer guide with information about the internal logic of sunpy-soar. (`#147 `__) 8 | 9 | 10 | 1.11.0 (2024-12-27) 11 | =================== 12 | 13 | Breaking Changes 14 | ---------------- 15 | 16 | - Increased the minimum required version of ``sunpy`` to v6.0.0. (`#139 `__) 17 | 18 | 19 | New Features 20 | ------------ 21 | 22 | - Added support for ``detector`` and ``wavelength`` search attributes and the ability to filter with them. (`#118 `__) 23 | 24 | 25 | Documentation 26 | ------------- 27 | 28 | - Created a how-to guide with a guide on how to build a query using the wavelength attribute.(:ref:`sunpy-soar-how-to-query-wavelength`) 29 | Added a short gallery example on how to query using ``wavelength`` and ``detector`` attributes.(:ref:`sphx_glr_generated_gallery_detector_wavelength.py`) (`#121 `__) 30 | 31 | 32 | 1.10.0 (2023-11-16) 33 | =================== 34 | 35 | Also includes changes from v1.9 36 | 37 | Removals 38 | -------- 39 | 40 | - Removed the Identifier attribute, which is replaced by the Product attribute. (`#100 `__) 41 | 42 | 43 | New Features 44 | ------------ 45 | 46 | - Now query returns time sorted table of results. (`#96 `__) 47 | 48 | 49 | Bug Fixes 50 | --------- 51 | 52 | - Fixing upper case to lower case descriptors (products) for SOAR 1.11 (`#94 `__) 53 | 54 | 55 | Internal Changes 56 | ---------------- 57 | 58 | - Retemplate sunpy-soar to follow the sunpy package structure (`#95 `__) 59 | 60 | 1.8 61 | === 62 | 63 | - Added ability to query with SOOP name. 64 | 65 | 1.7 66 | === 67 | 68 | - Added STIX data products to the list of valid data product identifiers. 69 | 70 | 1.6 71 | === 72 | 73 | - Registered a list of instruments available from the SOAR, with the ``a.Instrument`` attribute. 74 | - Registered the SOAR in the ``a.Provider`` attribute, meaning that a user can specify to the Fido search to only query the SOAR by use of ``a.Provider.soar``. 75 | - The ``_can_handle_query`` function within the SOARClient now checks to make sure if the SOAR supplies the queried data which fixes a bug which searched the SOAR for any data (e.g. AIA data). 76 | 77 | 1.5 78 | === 79 | 80 | - Registered a list of valid data product identifiers with the ``a.soar.Product`` attribute. 81 | To see these use ``print(a.soar.Product)``. 82 | 83 | 1.4 84 | === 85 | 86 | - Added support for searching for and fetching low latency data. 87 | 88 | 1.3 89 | === 90 | 91 | - Added support for path string interpolation, which allows you to do (for example) 92 | ``Fido.fetch(query, path=tmp_path / '{instrument}')`` and the name of the instrument will be used in the save path. 93 | This works for all supported Fido attrs. 94 | 95 | 1.2 96 | === 97 | 98 | - The ``Identifier`` attribute is deprecated - use ``Product`` instead, which is a direct replacement (with a better name!). 99 | - Allow time-only searches to be made. 100 | - Registered the ``Product`` attribute in the ``sunpy.net.attrs.soar`` namespace. 101 | After running ``import sunpy.net.attrs as a``, the attribute can now be accessed using ``a.soar.Product``. 102 | - The ``"Filesize"`` column in returned results now has units of ``astropy.units.Mbyte`` (previously it had no units). 103 | - Removed a validation check on ``a.Level``. 104 | If an level that SOAR doesn't understand is passed, zero results will now be returned instead of an error 105 | being raised. 106 | 107 | 1.1 108 | === 109 | 110 | - Fixed download of data where multiple versions of the requested file are available. 111 | Only the most recent version will be downloaded. 112 | - Added some log messages to the sunpy logger at DEBUG level 113 | 114 | 1.0 115 | === 116 | 117 | - Fixed searches where there are no results. 118 | - Added filesize to the result table 119 | - Raise an error if the SOAR server can't be reached 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2024 David Stansby and The SunPy Community All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 22 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Exclude specific files 2 | # All files which are tracked by git and not explicitly excluded here are included by setuptools_scm 3 | # Prune folders 4 | prune build 5 | prune docs/_build 6 | prune docs/api 7 | global-exclude *.pyc *.o 8 | 9 | # This subpackage is only used in development checkouts 10 | # and should not be included in built tarballs 11 | prune sunpy_soar/_dev 12 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ************** 2 | ``sunpy-soar`` 3 | ************** 4 | 5 | |ci-status| |coverage| 6 | 7 | .. |ci-status| image:: https://github.com/sunpy/sunpy-soar/actions/workflows/ci.yml/badge.svg 8 | :alt: CI status 9 | 10 | .. |coverage| image:: https://codecov.io/gh/dstansby/sunpy-soar/branch/main/graph/badge.svg?token=5NKZHBX3AW 11 | :target: https://codecov.io/gh/dstansby/sunpy-soar 12 | :alt: Code coverage 13 | 14 | 15 | A sunpy Fido plugin for accessing data in the Solar Orbiter Archive (SOAR). 16 | 17 | See the `documentation `_ for installation instructions, examples, how to get help and how to contribute. 18 | 19 | Usage of Generative AI 20 | ---------------------- 21 | 22 | We expect authentic engagement in our community. 23 | Be wary of posting output from Large Language Models or similar generative AI as comments on GitHub or any other platform, as such comments tend to be formulaic and low quality content. 24 | If you use generative AI tools as an aid in developing code or documentation changes, ensure that you fully understand the proposed changes and can explain why they are the correct approach and an improvement to the current state. 25 | -------------------------------------------------------------------------------- /changelog/README.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | .. note:: 6 | 7 | This README was adapted from the pytest changelog readme under the terms of the MIT licence. 8 | 9 | This directory contains "news fragments" which are short files that contain a small **ReST**-formatted text that will be added to the next ``CHANGELOG``. 10 | 11 | The ``CHANGELOG`` will be read by users, so this description should be aimed at SunPy users instead of describing internal changes which are only relevant to the developers. 12 | 13 | Make sure to use full sentences with correct case and punctuation, for example:: 14 | 15 | Add support for Helioprojective coordinates in `sunpy.coordinates.frames`. 16 | 17 | Please try to use Sphinx intersphinx using backticks. 18 | 19 | Each file should be named like ``.[.].rst``, where ```` is a pull request number, ``COUNTER`` is an optional number if a PR needs multiple entries with the same type and ```` is one of: 20 | 21 | * ``breaking``: A change which requires users to change code and is not backwards compatible. (Not to be used for removal of deprecated features.) 22 | * ``feature``: New user facing features and any new behavior. 23 | * ``bugfix``: Fixes a reported bug. 24 | * ``doc``: Documentation addition or improvement, like rewording an entire session or adding missing docs. 25 | * ``deprecation``: Feature deprecation 26 | * ``removal``: Feature removal. 27 | * ``trivial``: A change which has no user facing effect or is tiny change. 28 | 29 | So for example: ``123.feature.rst``, ``456.bugfix.rst``. 30 | 31 | If you are unsure what pull request type to use, don't hesitate to ask in your PR. 32 | 33 | Note that the ``towncrier`` tool will automatically reflow your text, so it will work best if you stick to a single paragraph, but multiple sentences and links are OK and encouraged. 34 | You can install ``towncrier`` and then run ``towncrier --draft`` if you want to get a preview of how your change will look in the final release notes. 35 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | Reference/API 2 | ============= 3 | 4 | .. automodapi:: sunpy_soar 5 | :no-inheritance-diagram: 6 | 7 | .. automodapi:: sunpy_soar.attrs 8 | :no-inheritance-diagram: 9 | 10 | 11 | .. note:: 12 | 13 | All attrs for sunpy_soar should be accessed as follows: 14 | 15 | .. code-block:: python 16 | 17 | import sunpy_soar 18 | from sunpy.net import Fido, attrs as a 19 | 20 | a.soar.Product 21 | -------------------------------------------------------------------------------- /docs/coc.rst: -------------------------------------------------------------------------------- 1 | Code of Conduct 2 | =============== 3 | 4 | When you are interacting with the SunPy community you are asked to follow our `Code of Conduct `__. 5 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file does only contain a selection of the most common options. For a 4 | # full list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | import datetime 8 | from pathlib import Path 9 | 10 | from packaging.version import Version 11 | from sunpy_sphinx_theme import PNG_ICON 12 | 13 | # -- Project information ----------------------------------------------------- 14 | # The full version, including alpha/beta/rc tags 15 | from sunpy_soar import __version__ 16 | 17 | # -- Project information ----------------------------------------------------- 18 | 19 | 20 | _version = Version(__version__) 21 | version = release = str(_version) 22 | # Avoid "post" appearing in version string in rendered docs 23 | if _version.is_postrelease: 24 | version = release = _version.base_version 25 | # Avoid long githashes in rendered Sphinx docs 26 | elif _version.is_devrelease: 27 | version = release = f"{_version.base_version}.dev{_version.dev}" 28 | is_development = _version.is_devrelease 29 | is_release = not(_version.is_prerelease or _version.is_devrelease) 30 | 31 | project = "sunpy-soar" 32 | copyright = f"{datetime.datetime.now().year}, The SunPy Community" # noqa: A001 33 | author = "The SunPy Community" 34 | 35 | # -- General configuration --------------------------------------------------- 36 | 37 | # Wrap large function/method signatures 38 | maximum_signature_line_length = 80 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named "sphinx.ext.*") or your custom 42 | # ones. 43 | extensions = [ 44 | "sphinx_gallery.gen_gallery", 45 | "matplotlib.sphinxext.plot_directive", 46 | "sphinx.ext.autodoc", 47 | "sphinx.ext.coverage", 48 | "sphinx.ext.doctest", 49 | "sphinx.ext.inheritance_diagram", 50 | "sphinx.ext.intersphinx", 51 | "sphinx.ext.napoleon", 52 | "sphinx.ext.todo", 53 | "sphinx.ext.viewcode", 54 | "sphinx_copybutton", 55 | "sphinx.ext.mathjax", 56 | "sphinx_automodapi.automodapi", 57 | "sphinx_automodapi.smart_resolver", 58 | "sphinx_changelog", 59 | ] 60 | 61 | # Add any paths that contain templates here, relative to this directory. 62 | # templates_path = ["_templates"] 63 | 64 | # List of patterns, relative to source directory, that match files and 65 | # directories to ignore when looking for source files. 66 | # This pattern also affects html_static_path and html_extra_path. 67 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 68 | 69 | # The suffix(es) of source filenames. 70 | # You can specify multiple suffix as a list of string: 71 | source_suffix = ".rst" 72 | 73 | # The master toctree document. 74 | master_doc = "index" 75 | 76 | # Treat everything in single ` as a Python reference. 77 | default_role = "py:obj" 78 | 79 | # Treat everything in single ` as a Python reference. 80 | default_role = "py:obj" 81 | 82 | # -- Options for intersphinx extension --------------------------------------- 83 | 84 | intersphinx_mapping = { 85 | "python": ( 86 | "https://docs.python.org/3/", 87 | (None, "http://data.astropy.org/intersphinx/python3.inv"), 88 | ), 89 | "numpy": ( 90 | "https://docs.scipy.org/doc/numpy/", 91 | (None, "http://data.astropy.org/intersphinx/numpy.inv"), 92 | ), 93 | "scipy": ( 94 | "https://docs.scipy.org/doc/scipy/reference/", 95 | (None, "http://data.astropy.org/intersphinx/scipy.inv"), 96 | ), 97 | "matplotlib": ( 98 | "https://matplotlib.org/", 99 | (None, "http://data.astropy.org/intersphinx/matplotlib.inv"), 100 | ), 101 | "astropy": ("http://docs.astropy.org/en/stable/", None), 102 | "sunpy": ("https://docs.sunpy.org/en/stable/", None), 103 | "parfive": ("https://parfive.readthedocs.io/en/stable/", None), 104 | } 105 | 106 | # The theme to use for HTML and HTML Help pages. See the documentation for 107 | # a list of builtin themes. 108 | html_theme = "sunpy" 109 | 110 | # Render inheritance diagrams in SVG 111 | graphviz_output_format = "svg" 112 | 113 | graphviz_dot_args = [ 114 | "-Nfontsize=10", 115 | "-Nfontname=Helvetica Neue, Helvetica, Arial, sans-serif", 116 | "-Efontsize=10", 117 | "-Efontname=Helvetica Neue, Helvetica, Arial, sans-serif", 118 | "-Gfontsize=10", 119 | "-Gfontname=Helvetica Neue, Helvetica, Arial, sans-serif", 120 | ] 121 | 122 | # -- Options for HTML output ------------------------------------------------- 123 | 124 | # The theme to use for HTML and HTML Help pages. See the documentation for 125 | # a list of builtin themes. 126 | html_theme = "sunpy" 127 | 128 | # Render inheritance diagrams in SVG 129 | graphviz_output_format = "svg" 130 | 131 | graphviz_dot_args = [ 132 | "-Nfontsize=10", 133 | "-Nfontname=Helvetica Neue, Helvetica, Arial, sans-serif", 134 | "-Efontsize=10", 135 | "-Efontname=Helvetica Neue, Helvetica, Arial, sans-serif", 136 | "-Gfontsize=10", 137 | "-Gfontname=Helvetica Neue, Helvetica, Arial, sans-serif", 138 | ] 139 | 140 | # Add any paths that contain custom static files (such as style sheets) here, 141 | # relative to this directory. They are copied after the builtin static files, 142 | # so a file named "default.css" will overwrite the builtin "default.css". 143 | # html_static_path = ["_static"] 144 | 145 | # By default, when rendering docstrings for classes, sphinx.ext.autodoc will 146 | # make docs with the class-level docstring and the class-method docstrings, 147 | # but not the __init__ docstring, which often contains the parameters to 148 | # class constructors across the scientific Python ecosystem. The option below 149 | # will append the __init__ docstring to the class-level docstring when rendering 150 | # the docs. For more options, see: 151 | # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autoclass_content 152 | autoclass_content = "both" 153 | 154 | # -- Other options ---------------------------------------------------------- 155 | 156 | napoleon_use_rtype = False 157 | 158 | napoleon_google_docstring = False 159 | 160 | # Enable nitpicky mode, which forces links to be non-broken 161 | nitpicky = True 162 | # This is not used. See docs/nitpick-exceptions file for the actual listing. 163 | nitpick_ignore = [] 164 | with Path("nitpick-exceptions").open() as f: 165 | for line in f: 166 | if line.strip() == "" or line.startswith("#"): 167 | continue 168 | dtype, target = line.split(None, 1) 169 | target = target.strip() 170 | nitpick_ignore.append((dtype, target)) 171 | 172 | # -- Options for sphinx-copybutton --------------------------------------------- 173 | 174 | # Python Repl + continuation, Bash, ipython and qtconsole + continuation, jupyter-console + continuation 175 | copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " 176 | copybutton_prompt_is_regexp = True 177 | 178 | # -- Options for the Sphinx gallery ------------------------------------------- 179 | 180 | sphinx_gallery_conf = { 181 | "backreferences_dir": Path("generated") / "modules", 182 | "filename_pattern": "^((?!skip_).)*$", 183 | "examples_dirs": Path("..") / "examples", 184 | "gallery_dirs": Path("generated") / "gallery", 185 | "matplotlib_animations": True, 186 | # Comes from the theme. 187 | "default_thumb_file": PNG_ICON, 188 | "abort_on_example_error": False, 189 | "plot_gallery": "True", 190 | "remove_config_comments": True, 191 | "doc_module": ("sunpy-soar"), 192 | "only_warn_on_example_error": True, 193 | } 194 | -------------------------------------------------------------------------------- /docs/dev_guide/index.rst: -------------------------------------------------------------------------------- 1 | .. _sunpy-soar-dev-guide-index: 2 | 3 | *************** 4 | Developer Guide 5 | *************** 6 | 7 | This guide will explain the internals of ``sunpy-soar`` and how it interacts with `sunpy.net.Fido`. 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | working 13 | tables 14 | query 15 | -------------------------------------------------------------------------------- /docs/dev_guide/query.rst: -------------------------------------------------------------------------------- 1 | .. _sunpy-soar-dev-guide-query: 2 | 3 | *************************************** 4 | Request methods ``sunpy-soar`` supports 5 | *************************************** 6 | 7 | sunpy-soar currently supports two REQUEST methods: ``doQuery`` and ``doQueryFilteredByDistance``. 8 | 9 | ``doQuery``: This is the standard method used when no specific distance attribute is included in the search query. 10 | It performs a general query based on the provided parameters, retrieving the data that matches the criteria specified. 11 | 12 | ``doQueryFilteredByDistance``: This method is employed when a distance parameter is included in the search query. 13 | Unlike ``doQuery``, this method filters the entire database based on the specified distance value. 14 | The time attribute is not necessarily required when using ``doQueryFilteredByDistance``. 15 | The distance range of values is appended to the end of the query using ``&DISTANCE(dmin, dmax)``, where ``dmin`` and ``dmax`` are Astropy quantities representing distances. 16 | These values must fall within the range of 0.28 AU to 1.02 AU; otherwise, the query will not return any results. 17 | 18 | Using the example below, 19 | 20 | .. code-block:: python 21 | 22 | >>> import astropy.units as u 23 | >>> import sunpy.net.attrs as a 24 | >>> from sunpy.net import Fido 25 | 26 | >>> instrument = a.Instrument("RPW") 27 | >>> level = a.Level(2) 28 | >>> distance = a.soar.Distance(0.28 * u.AU, 0.30 * u.AU) 29 | >>> result = Fido.search(instrument & level & distance) # doctest: +REMOTE_DATA 30 | >>> result # doctest: +REMOTE_DATA 31 | 32 | Results from 1 Provider: 33 | 34 | 357 Results from the SOARClient: 35 | 36 | Instrument Data product Level Start time End time Filesize SOOP Name 37 | Mbyte 38 | ---------- ------------------- ----- ----------------------- ----------------------- -------- --------- 39 | RPW rpw-tds-surv-tswf-b L2 2022-10-09 00:00:00.000 2022-10-10 00:00:00.000 13.748 None 40 | RPW rpw-lfr-surv-bp1 L2 2022-10-09 00:00:00.000 2022-10-10 00:00:00.000 61.818 None 41 | RPW rpw-tds-surv-tswf-e L2 2022-10-09 00:00:00.000 2022-10-10 00:00:00.000 95.65 None 42 | RPW rpw-lfr-surv-bp2 L2 2022-10-09 00:00:00.000 2022-10-10 00:00:00.000 145.551 None 43 | RPW rpw-lfr-surv-swf-e L2 2022-10-09 00:00:00.000 2022-10-10 00:00:00.000 47.452 None 44 | ... ... ... ... ... ... ... 45 | RPW rpw-tds-surv-stat L2 2023-10-10 00:00:00.000 2023-10-11 00:00:00.000 0.531 None 46 | RPW rpw-tds-surv-hist2d L2 2023-10-10 00:00:00.000 2023-10-11 00:00:00.000 3.382 None 47 | RPW rpw-tnr-surv L2 2023-10-10 00:00:00.000 2023-10-11 00:00:00.000 341.182 None 48 | RPW rpw-lfr-surv-swf-e L2 2023-10-10 00:00:00.000 2023-10-11 00:00:00.000 200.206 None 49 | RPW rpw-tds-surv-tswf-e L2 2023-10-10 00:00:00.000 2023-10-11 00:00:00.000 173.184 None 50 | Length = 357 rows 51 | 52 | 53 | 54 | Here the query's "REQUEST" type to "doQueryFilteredByDistance", which is a special method that filters the entire database based on the specified distance value. 55 | 56 | The actual query this example produces is, 57 | 58 | .. code-block:: python 59 | 60 | "SELECT+*+FROM+v_sc_data_item+WHERE+instrument='RPW'+AND+level='L2'&DISTANCE(0.28,0.30)" 61 | 62 | How can other request methods be added? 63 | ======================================= 64 | 65 | To add support for additional request methods, you'll need to consider the impact they will have on the query structure. 66 | The REQUEST parameter in the query must be updated accordingly, and any necessary modifications to the query string should be made to accommodate the new method. 67 | If the new request method requires a specific attribute, you may need to add it as a class in the attrs.py file (if it doesn't already exist in (if it doesn't already exist in `sunpy.net.attrs `__). 68 | Additionally, a walker for this attribute will need to be created. 69 | -------------------------------------------------------------------------------- /docs/dev_guide/tables.rst: -------------------------------------------------------------------------------- 1 | .. _sunpy-soar-dev-guide-tables: 2 | 3 | ********************************** 4 | Tables supported in ``sunpy-soar`` 5 | ********************************** 6 | 7 | The ``sunpy-soar`` library currently supports data retrieval from both science and low-latency data tables, such as ``v_sc_data_item`` and ``v_ll_data_item``. 8 | Additionally, it provides support for join tables associated with remote instruments, such as ``v_eui_sc_fits``. 9 | These tables and their columns are described in the `Tables, Views, and Columns `__. 10 | 11 | In the context of ``sunpy-soar``, data tables contain columns related to scientific measurements, while instrument tables contain metadata specific to particular instruments. 12 | The ``sunpy-soar`` library specifically supports the wavelength and detector columns within these tables. 13 | These columns are linked to the data columns in the instrument tables through a join operation, using the ``data_item_oid`` as the key. 14 | 15 | How can a new column be added? 16 | ============================== 17 | 18 | If the new column you wish to add is part of an existing data or instrument FITS table, you can extend the corresponding ADQL query to include this column. 19 | For example, if the new column is in the instrument table, it should be included in the "SELECT" clause of the query for that table. 20 | 21 | Moreover, if one needs to enable filtering by this new column, you must consider adding it as an attribute in the ``attrs.py`` file. 22 | If the column already exists within `sunpy.net.attrs`, you should add a corresponding walker in the ``attrs.py`` file. 23 | The ``_can_handle_query`` method in the ``client.py`` file should also be updated to ensure proper support for this new column in queries. 24 | 25 | How can a new table be added? 26 | ============================= 27 | 28 | When adding support for a new table in ``sunpy-soar``, it is essential to fully understand the context and requirements. 29 | This includes identifying the key that will be used for join operations and determining what columns should be returned to the user. 30 | 31 | For direct joins, the key relationship between the tables should be carefully identified. 32 | Once the joins are established, the necessary columns can be included in the query, following the process outlined for adding columns. 33 | 34 | Depending on the use case, you might need to determine the type of join (e.g., inner join, outer join, left join) and what specific tables or columns should be displayed to the user. 35 | 36 | Finally, ensure that the ``_do_search`` method is updated to reflect these changes. 37 | This method should handle any additional columns or conditional logic for attribute-specific queries, ensuring that the appropriate columns is returned to the user. 38 | 39 | Allowing an attribute to work in the search query 40 | ================================================= 41 | 42 | For any attribute to work in the search query, we can divide the situation in two cases: 43 | 44 | Case 1 - It is already there in `sunpy.net.attrs`, in this situation just a walker needs to be created. 45 | 46 | Example on its implementation: 47 | 48 | .. code-block:: python 49 | 50 | # Adding a walker applier for that attribute. 51 | @walker.add_applier(a.AttrName) 52 | def _(wlk, attr, params): 53 | params.append(f"Detector='{attr.value}'") 54 | 55 | # Depending upon the attribute the value being added could be a range of values also. 56 | @walker.add_applier(a.AttrName) 57 | def _(wlk, attr, params) 58 | attrmin = attr.min.value 59 | attrmax = attr.max.value 60 | # appending the attribute depending upon how it should be used in the query 61 | params.append(f"Attrmin='{attrmin}'+AND+Attrmax='{attrmax}'") 62 | 63 | Case 2 - It is not already there in `sunpy.net.attrs`, in this situation we have to introduce the attribute and a walker needs to be created. 64 | 65 | Example on its implementation: 66 | 67 | .. code-block:: python 68 | 69 | class Attrname("Type of attribute: SimpleAttr, Range"): 70 | """ 71 | Description of attribute 72 | """ 73 | # Depending on the attribute, this would include methods to make it easy to use in the search query 74 | # for example in case of Product, we convert it to lower case for it to be used. 75 | def __init__(self, value): 76 | self.value = value.lower() 77 | 78 | # After this a walker applier need to be added, which have been shown above 79 | -------------------------------------------------------------------------------- /docs/dev_guide/working.rst: -------------------------------------------------------------------------------- 1 | .. _sunpy-soar-dev-guide-working: 2 | 3 | 4 | How data is retrieved from the SOAR 5 | =================================== 6 | 7 | To retrieve data from SOAR, you first use the `sunpy.net.Fido` object from `sunpy` and specify the desired attributes using `sunpy.net.attrs`. 8 | These attributes define the criteria for the data you want to retrieve, such as the time range, instrument, or wavelength. 9 | 10 | Here is an example of how to specify the time range for the data you want to retrieve: 11 | 12 | .. code-block:: python 13 | 14 | import sunpy.net.attrs as a 15 | from sunpy.net import Fido 16 | import sunpy_soar 17 | 18 | instrument = a.Instrument("EUI") 19 | time = a.Time("2021-02-01", "2021-02-02") 20 | level = a.Level(1) 21 | product = a.soar.Product("EUI-FSI174-IMAGE") 22 | 23 | result = Fido.search(instrument & time & level & product) 24 | 25 | 26 | ``sunpy-soar`` constructs the query based on the specified criteria, then generates a URL to interact with the SOAR API. 27 | This is done by using the Table Access Protocol (TAP), a widely adopted standard in the astronomical community for accessing large datasets. 28 | The queries are formulated in Astronomical Data Query Language (`ADQL `__), which allows for flexible querying of astronomical data. 29 | The results are returned in the form of an Astropy table, providing a structured and efficient format for further analysis and visualization within the Python environment. 30 | 31 | A generated query looks like: 32 | 33 | .. code-block:: SQL 34 | 35 | SELECT * FROM v_sc_data_item WHERE instrument='EPD' AND begin_time>='2021-02-01 00:00:00' AND begin_time<='2021-02-02 00:00:00' AND level='L1' AND descriptor='epd-epthet2-nom-close' 36 | 37 | Or with a JOIN 38 | 39 | .. code-block:: SQL 40 | 41 | SELECT h1.instrument, h1.descriptor, h1.level, h1.begin_time, h1.end_time, h1.data_item_id, h1.filesize, h1.filename, h1.soop_name, h2.detector, h2.wavelength, h2.dimension_index 42 | FROM v_sc_data_item AS h1 JOIN v_eui_sc_fits AS h2 USING (data_item_oid) WHERE h1.instrument='EUI' AND h1.begin_time>='2021-02-01 00:00:00' AND h1.begin_time<='2021-02-02 00:00:00' AND 43 | h2.dimension_index='1' AND h1.level='L1' AND h1.descriptor='eui-fsi174-image' 44 | 45 | The URL is generated with the query formed based on the parameters, then Fido is used to search and download the data. 46 | -------------------------------------------------------------------------------- /docs/how_to/index.rst: -------------------------------------------------------------------------------- 1 | .. _sunpy-soar-how-to-index: 2 | 3 | ************* 4 | How-To Guides 5 | ************* 6 | 7 | These how-to guides provide examples of how to perform specific tasks with sunpy-soar. 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | wavelength 13 | -------------------------------------------------------------------------------- /docs/how_to/wavelength.rst: -------------------------------------------------------------------------------- 1 | .. _sunpy-soar-how-to-query-wavelength: 2 | 3 | *************************************************************** 4 | Query for Solar Orbiter data using the ``Wavelength`` attribute 5 | *************************************************************** 6 | 7 | ``sunpy-soar`` provides convenient methods to construct queries using ``a.Wavelength`` for several different remote sensing instruments available through the SOAR. 8 | In this guide, we will demonstrate how we can query data using ``a.Wavelength`` for different instruments. 9 | For instruments EUI, METIS and SOLOHI we get query results in form of wavelength. 10 | However, at this time we cannot search for wavelengths for instruments SPICE, PHI, and STIX as this information is not yet available in the SOAR. 11 | 12 | For instruments EUI, METIS and SOLOHI passing a single Wavelength 13 | ================================================================= 14 | 15 | When a single wavelength is provided it is interpreted as the wavelength. 16 | 17 | .. code-block:: python 18 | 19 | >>> import astropy.units as u 20 | >>> import sunpy.net.attrs as a 21 | >>> from sunpy.net import Fido 22 | >>> import sunpy_soar 23 | 24 | >>> instrument = a.Instrument("METIS") 25 | >>> time = a.Time("2023-02-01 01:00", "2023-02-01 05:00") 26 | >>> level = a.Level(2) 27 | >>> wavelength = a.Wavelength(121.6 * u.AA) 28 | >>> result = Fido.search(instrument & time & level & wavelength) # doctest: +REMOTE_DATA 29 | >>> result # doctest: +REMOTE_DATA 30 | 31 | Results from 1 Provider: 32 | 33 | 8 Results from the SOARClient: 34 | 35 | Instrument Data product Level Start time End time Filesize SOOP Name Detector Wavelength 36 | Mbyte 37 | ---------- -------------- ----- ----------------------- ----------------------- -------- --------- -------- ---------- 38 | METIS metis-uv-image L2 2023-02-01 01:00:48.690 2023-02-01 01:11:46.866 0.85 none UVD 121.6 39 | METIS metis-uv-image L2 2023-02-01 01:30:48.680 2023-02-01 01:41:45.540 0.85 none UVD 121.6 40 | METIS metis-uv-image L2 2023-02-01 02:00:48.671 2023-02-01 02:11:44.213 12.64 none UVD 121.6 41 | METIS metis-uv-image L2 2023-02-01 02:30:48.661 2023-02-01 02:41:42.887 12.64 none UVD 121.6 42 | METIS metis-uv-image L2 2023-02-01 03:00:48.652 2023-02-01 03:11:41.560 12.64 none UVD 121.6 43 | METIS metis-uv-image L2 2023-02-01 03:30:48.643 2023-02-01 03:41:40.233 12.64 none UVD 121.6 44 | METIS metis-uv-image L2 2023-02-01 04:00:48.633 2023-02-01 04:11:38.907 12.64 none UVD 121.6 45 | METIS metis-uv-image L2 2023-02-01 04:30:38.163 2023-02-01 04:40:37.625 12.64 none UVD 121.6 46 | 47 | 48 | 49 | For instruments EUI, METIS and SOLOHI passing a range of Wavelength 50 | =================================================================== 51 | 52 | When a range of wavelength is provided, it is interpreted as the wavemin and wavemax. 53 | 54 | .. code-block:: python 55 | 56 | >>> wavelength = a.Wavelength(580 * u.AA, 640 * u.AA) 57 | >>> result = Fido.search(instrument & time & level & wavelength) # doctest: +REMOTE_DATA 58 | >>> result # doctest: +REMOTE_DATA 59 | 60 | Results from 1 Provider: 61 | 62 | 64 Results from the SOARClient: 63 | 64 | Instrument Data product Level Start time End time Filesize SOOP Name Detector Wavelength 65 | Mbyte 66 | ---------- ------------------ ----- ----------------------- ----------------------- -------- --------- -------- ---------- 67 | METIS metis-vl-tb L2 2023-02-01 01:00:00.147 2023-02-01 01:24:37.923 12.64 none VLD 610.0 68 | METIS metis-vl-stokes L2 2023-02-01 01:00:00.147 2023-02-01 01:24:37.923 21.067 none VLD 610.0 69 | METIS metis-vl-pb L2 2023-02-01 01:00:00.147 2023-02-01 01:24:37.923 12.64 none VLD 610.0 70 | METIS metis-vl-image L2 2023-02-01 01:00:00.147 2023-02-01 01:23:05.525 12.643 none VLD 610.0 71 | METIS metis-vl-pol-angle L2 2023-02-01 01:00:00.147 2023-02-01 01:24:37.923 12.64 none VLD 610.0 72 | METIS metis-vl-image L2 2023-02-01 01:00:30.944 2023-02-01 01:23:36.322 12.643 none VLD 610.0 73 | METIS metis-vl-image L2 2023-02-01 01:01:01.741 2023-02-01 01:24:07.127 12.643 none VLD 610.0 74 | METIS metis-vl-image L2 2023-02-01 01:01:32.538 2023-02-01 01:24:37.922 12.643 none VLD 610.0 75 | METIS metis-vl-stokes L2 2023-02-01 01:30:00.150 2023-02-01 01:54:37.922 21.067 none VLD 610.0 76 | METIS metis-vl-pol-angle L2 2023-02-01 01:30:00.150 2023-02-01 01:54:37.922 12.64 none VLD 610.0 77 | ... ... ... ... ... ... ... ... ... 78 | METIS metis-vl-image L2 2023-02-01 04:01:01.774 2023-02-01 04:24:07.158 50.388 none VLD 610.0 79 | METIS metis-vl-image L2 2023-02-01 04:01:32.571 2023-02-01 04:24:37.955 50.388 none VLD 610.0 80 | METIS metis-vl-pol-angle L2 2023-02-01 04:30:00.201 2023-02-01 04:54:37.981 50.388 none VLD 610.0 81 | METIS metis-vl-stokes L2 2023-02-01 04:30:00.201 2023-02-01 04:54:37.981 83.981 none VLD 610.0 82 | METIS metis-vl-tb L2 2023-02-01 04:30:00.201 2023-02-01 04:54:37.981 50.388 none VLD 610.0 83 | METIS metis-vl-pb L2 2023-02-01 04:30:00.201 2023-02-01 04:54:37.981 50.388 none VLD 610.0 84 | METIS metis-vl-image L2 2023-02-01 04:30:00.201 2023-02-01 04:53:05.585 50.388 none VLD 610.0 85 | METIS metis-vl-image L2 2023-02-01 04:30:30.999 2023-02-01 04:53:36.383 50.388 none VLD 610.0 86 | METIS metis-vl-image L2 2023-02-01 04:31:01.796 2023-02-01 04:54:07.184 50.388 none VLD 610.0 87 | METIS metis-vl-image L2 2023-02-01 04:31:32.593 2023-02-01 04:54:37.979 50.388 none VLD 610.0 88 | Length = 64 rows 89 | 90 | 91 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | **************************** 2 | ``sunpy-soar`` Documentation 3 | **************************** 4 | 5 | A sunpy Fido plugin for accessing data in the `Solar Orbiter Archive (SOAR) `__. 6 | 7 | Installation 8 | ============ 9 | 10 | ``sunpy-soar`` requires ``python >= 3.10`` and ``sunpy >= 6.0``. 11 | Currently it can be installed from PyPI using: 12 | 13 | .. code-block:: bash 14 | 15 | pip install sunpy-soar 16 | 17 | or conda using 18 | 19 | .. code-block:: bash 20 | 21 | conda install -c conda-forge sunpy-soar 22 | 23 | ``sunpy-soar`` and the VSO 24 | ========================== 25 | 26 | ``sunpy-soar`` queries the official repository of Solar Orbiter data, the SOAR. 27 | The Virtual Solar Observatory (VSO) as of writing (September 2022) mirrors a subset of the Solar Orbiter archive alongside many other solar physics data sources. 28 | The VSO allows data from multiple missions/observatories to be easily queried in one go, but users should be aware that the VSO is not the official repository for Solar Orbiter data and does not currently (as of September 2022) provide a comprehensive listing of all available Solar Orbiter data. 29 | 30 | Getting Help 31 | ============ 32 | 33 | For more information or to ask questions about ``sunpy-soar`` or any other SunPy Project library, check out: 34 | 35 | - `sunpy-soar documentation `__ 36 | - `SunPy chat`_ 37 | - `SunPy mailing list `__ 38 | - `SunPy community forum `__ 39 | 40 | Contributing 41 | ============ 42 | 43 | If you would like to get involved, start by joining the `SunPy Chat`_ and check out our `Newcomers' guide `__. 44 | This will walk you through getting set up for contributing. 45 | 46 | .. _SunPy Chat: https://app.element.io/#/room/#sunpy:openastronomy.org 47 | 48 | .. toctree:: 49 | :hidden: 50 | 51 | generated/gallery/index 52 | how_to/index 53 | dev_guide/index 54 | api 55 | whatsnew/index 56 | coc 57 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/nitpick-exceptions: -------------------------------------------------------------------------------- 1 | py:obj BaseQueryResponse 2 | -------------------------------------------------------------------------------- /docs/whatsnew/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | ************** 4 | Full Changelog 5 | ************** 6 | 7 | .. changelog:: 8 | :towncrier: ../../ 9 | :towncrier-skip-if-empty: 10 | :changelog_file: ../../CHANGELOG.rst 11 | -------------------------------------------------------------------------------- /docs/whatsnew/index.rst: -------------------------------------------------------------------------------- 1 | .. _whatsnew: 2 | 3 | *************** 4 | Release History 5 | *************** 6 | 7 | This page documents the releases for sunpy-soar 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | changelog 13 | -------------------------------------------------------------------------------- /examples/README.txt: -------------------------------------------------------------------------------- 1 | *************** 2 | Example Gallery 3 | *************** 4 | 5 | This gallery contains examples of how to use ``sunpy-soar``. 6 | 7 | -------------------------------------------------------------------------------- /examples/detector_wavelength.py: -------------------------------------------------------------------------------- 1 | """ 2 | ========================================================================= 3 | Searching for Solar Orbiter data using Wavelength and Detector attributes 4 | ========================================================================= 5 | 6 | This example demonstrates how to search and download Solar Orbiter data using ``sunpy.net.Fido``. 7 | To do this, we can build a query based on several attributes. 8 | Here, we will build a search for METIS data from the UVD (Ultra Violet Detector) detector for a specific wavelength. 9 | """ 10 | 11 | import astropy.units as u 12 | import sunpy.net.attrs as a 13 | from sunpy.net import Fido 14 | 15 | ############################################################################### 16 | # Importing sunpy_soar registers the client with sunpy Fido 17 | 18 | import sunpy_soar # NOQA: F401 isort:skip 19 | 20 | ############################################################################### 21 | # We shall start with constructing a search query with wavelength and detector. 22 | 23 | instrument = a.Instrument("METIS") 24 | time = a.Time("2023-02-01 01:00", "2023-02-01 05:00") 25 | level = a.Level(2) 26 | detector = a.Detector("UVD") 27 | wavelength = a.Wavelength(121.6 * u.AA) 28 | 29 | ############################################################################### 30 | # Now do the search. 31 | 32 | result = Fido.search(instrument & time & level & detector & wavelength) 33 | result 34 | 35 | ############################################################################### 36 | # To then download the data, you would then use Fido.fetch(result), which will download the data locally. 37 | -------------------------------------------------------------------------------- /examples/quick.py: -------------------------------------------------------------------------------- 1 | """ 2 | ============================================ 3 | Quick overview of using sunpy-soar with Fido 4 | ============================================ 5 | 6 | This example demonstrates how to search and download Solar Orbiter data using ``sunpy.net.Fido``. 7 | """ 8 | 9 | import sunpy.net.attrs as a 10 | from sunpy.net import Fido 11 | 12 | ##################################################### 13 | # Importing sunpy_soar registers the client with sunpy 14 | 15 | import sunpy_soar # NOQA: F401 isort:skip 16 | 17 | ##################################################### 18 | # We shall start with constructing a search query. 19 | 20 | instrument = a.Instrument("EUI") 21 | time = a.Time("2021-02-01", "2021-02-02") 22 | level = a.Level(1) 23 | product = a.soar.Product("EUI-FSI174-IMAGE") 24 | 25 | ##################################################### 26 | # Now do the search. 27 | 28 | result = Fido.search(instrument & time & level & product) 29 | result 30 | 31 | ##################################################### 32 | # Finally we can download the data. 33 | # 34 | # For this example, we will comment out the download part 35 | # as we want to avoid downloading data in the documentation build 36 | 37 | # files = Fido.fetch(result) 38 | # print(files) 39 | -------------------------------------------------------------------------------- /examples/search_attrs.py: -------------------------------------------------------------------------------- 1 | """ 2 | =========================== 3 | Available search attributes 4 | =========================== 5 | 6 | This example demonstrates the available search attributes 7 | for SOAR currently supported by ``sunpy-soar``. 8 | """ 9 | 10 | import sunpy.net.attrs as a 11 | 12 | ##################################################### 13 | # Importing sunpy_soar registers the client with sunpy 14 | 15 | import sunpy_soar # NOQA: F401 isort:skip 16 | 17 | ##################################################### 18 | # The easiest way to access search attributes is using 19 | # the attribute registry provided by `sunpy.net.attrs`. 20 | # 21 | # When constructing a search for SOAR ``a.Time`` must be provided. 22 | # Other search attributes can be used too - ``sunpy-soar`` recognizes the following: 23 | # 24 | # The third ``near`` argument to ``a.Time`` is not currently supported. 25 | # You will have to manually filter the results if you want to find the one closest to a given time. 26 | # 27 | # For instrument the following are supported: 28 | # 29 | # - "EPD": "Energetic Particle Detector" 30 | # - "EUI": "Extreme UV Imager" 31 | # - "MAG": "Magnetometer" 32 | # - "METIS": "Metis: Visible light and ultraviolet coronagraph" 33 | # - "PHI": "Polarimetric and Helioseismic Imager" 34 | # - "RPW": "Radio and Plasma Wave instrument" 35 | # - "SOLOHI": "Solar Orbiter Heliospheric Imager" 36 | # - "SPICE": "SPectral Investigation of the Coronal Environment" 37 | # - "STIX": "Spectrometer Telescope for Imaging X-rays" 38 | # - "SWA": "Solar Wind Analyser" 39 | # 40 | # For level the following are supported: 41 | # L0, L1, L2, L3, LL01, LL02, LL03 42 | # 43 | # For product: 44 | 45 | a.soar.Product 46 | -------------------------------------------------------------------------------- /examples/solar_distance_query.py: -------------------------------------------------------------------------------- 1 | """ 2 | =============================================================== 3 | Searching for Solar Orbiter data using Solar Distance attribute 4 | =============================================================== 5 | 6 | This example demonstrates how to search and download Solar Orbiter data using ``sunpy.net.Fido``. 7 | To do this, we can build a query based on several attributes. 8 | 9 | The ``Distance`` attribute allows us to specify a range of distances from the Sun in astronomical units (AU). 10 | 11 | """ 12 | 13 | import astropy.units as u 14 | import sunpy.net.attrs as a 15 | from sunpy.net import Fido 16 | 17 | ############################################################################### 18 | # Importing sunpy_soar registers the client with sunpy Fido 19 | import sunpy_soar # NOQA: F401 20 | 21 | ############################################################################### 22 | # We shall start with constructing a search query with instrument, level, detector, and distance. 23 | 24 | instrument = a.Instrument("EUI") 25 | time = a.Time("2022-10-29 05:00:00", "2022-10-29 06:00:00") 26 | level = a.Level(2) 27 | detector = a.Detector("HRI_EUV") 28 | distance = a.soar.Distance(0.45 * u.AU, 0.46 * u.AU) 29 | 30 | ############################################################################### 31 | # Now do the search without time attribute. 32 | 33 | result = Fido.search(instrument & level & detector & distance) 34 | result 35 | 36 | ############################################################################### 37 | # Now do the search with time attribute. 38 | result = Fido.search(instrument & level & detector & distance & time) 39 | result 40 | 41 | ############################################################################### 42 | # To then download the data, you would use Fido.fetch(result), which will download the data locally. 43 | -------------------------------------------------------------------------------- /licenses/LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024, The SunPy Community 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /licenses/README.rst: -------------------------------------------------------------------------------- 1 | Licenses 2 | ======== 3 | 4 | This directory holds license and credit information for the package, 5 | works the package is derived from, and/or datasets. 6 | 7 | Ensure that you pick a package licence which is in this folder and it matches 8 | the one mentioned in the top level README.rst file. If you are using the 9 | pre-rendered version of this template check for the word 'Other' in the README. 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=62.1", 4 | "setuptools_scm[toml]>=8.0.0", 5 | "wheel", 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "sunpy_soar" 11 | description = "A sunpy FIDO plugin for accessing data in the Solar Orbiter Archive (SOAR)." 12 | requires-python = ">=3.10" 13 | readme = { file = "README.rst", content-type = "text/x-rst" } 14 | license = { file = "licenses/LICENSE.rst" } 15 | authors = [ 16 | { name = "The SunPy Community", email = "sunpy@googlegroups.com" }, 17 | { name = "David Stansby"}, 18 | ] 19 | dependencies = [ 20 | "matplotlib>=3.6.0", 21 | "astropy>=5.3.0", 22 | "sunpy[net]>=6.0.0", 23 | "requests>=2.28.0", 24 | ] 25 | dynamic = ["version"] 26 | 27 | [project.optional-dependencies] 28 | tests = [ 29 | "pytest-cov", 30 | "pytest-doctestplus", 31 | "pytest-xdist", 32 | "pytest", 33 | "responses>=0.20.0", 34 | "sunpy[map,net]>=6.0.0", 35 | ] 36 | docs = [ 37 | "sphinx", 38 | "sphinx-automodapi", 39 | "matplotlib", 40 | "sphinx-changelog", 41 | "sphinx-copybutton", 42 | "sphinx-gallery", 43 | "sphinxext-opengraph", 44 | "sunpy-sphinx-theme", 45 | ] 46 | dev = ["sunpy-soar[docs,tests]"] 47 | 48 | [project.urls] 49 | Homepage = "https://sunpy.org" 50 | "Source Code" = "https://github.com/sunpy/sunpy-soar" 51 | Download = "https://pypi.org/project/sunpy-soar" 52 | Documentation = "https://docs.sunpy.org/projects/soar" 53 | Changelog = "https://docs.sunpy.org/projects/soar/en/stable/whatsnew/changelog.html" 54 | "Issue Tracker" = "https://github.com/sunpy/sunpy-soar/issues" 55 | 56 | [tool.setuptools] 57 | zip-safe = false 58 | include-package-data = true 59 | 60 | [tool.setuptools.packages.find] 61 | include = ["sunpy_soar*"] 62 | exclude = ["sunpy_soar._dev*"] 63 | 64 | [tool.setuptools_scm] 65 | version_file = "sunpy_soar/_version.py" 66 | 67 | [tool.gilesbot] 68 | [tool.gilesbot.pull_requests] 69 | enabled = true 70 | 71 | [tool.gilesbot.towncrier_changelog] 72 | enabled = true 73 | verify_pr_number = true 74 | changelog_skip_label = "No Changelog Entry Needed" 75 | help_url = "https://github.com/sunpy/sunpy-soar/blob/main/changelog/README.rst" 76 | 77 | changelog_missing_long = "There isn't a changelog file in this pull request. Please add a changelog file to the `changelog/` directory following the instructions in the changelog [README](https://github.com/sunpy/sunpy-soar/blob/main/changelog/README.rst)." 78 | 79 | type_incorrect_long = "The changelog file you added is not one of the allowed types. Please use one of the types described in the changelog [README](https://github.com/sunpy/sunpy-soar/blob/main/changelog/README.rst)" 80 | 81 | number_incorrect_long = "The number in the changelog file you added does not match the number of this pull request. Please rename the file." 82 | 83 | # TODO: This should be in towncrier.toml but Giles currently only works looks in 84 | # pyproject.toml we should move this back when it's fixed. 85 | [tool.towncrier] 86 | package = "sunpy_soar" 87 | filename = "CHANGELOG.rst" 88 | directory = "changelog/" 89 | issue_format = "`#{issue} `__" 90 | title_format = "{version} ({project_date})" 91 | 92 | [[tool.towncrier.type]] 93 | directory = "breaking" 94 | name = "Breaking Changes" 95 | showcontent = true 96 | 97 | [[tool.towncrier.type]] 98 | directory = "deprecation" 99 | name = "Deprecations" 100 | showcontent = true 101 | 102 | [[tool.towncrier.type]] 103 | directory = "removal" 104 | name = "Removals" 105 | showcontent = true 106 | 107 | [[tool.towncrier.type]] 108 | directory = "feature" 109 | name = "New Features" 110 | showcontent = true 111 | 112 | [[tool.towncrier.type]] 113 | directory = "bugfix" 114 | name = "Bug Fixes" 115 | showcontent = true 116 | 117 | [[tool.towncrier.type]] 118 | directory = "doc" 119 | name = "Documentation" 120 | showcontent = true 121 | 122 | [[tool.towncrier.type]] 123 | directory = "trivial" 124 | name = "Internal Changes" 125 | showcontent = true 126 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | minversion = 7.0 3 | testpaths = 4 | sunpy_soar 5 | docs 6 | norecursedirs = 7 | docs/_build 8 | docs/generated 9 | sunpy_soar/_dev 10 | sunpy_soar/extern 11 | doctest_plus = enabled 12 | doctest_optionflags = 13 | NORMALIZE_WHITESPACE 14 | FLOAT_CMP 15 | ELLIPSIS 16 | text_file_format = rst 17 | addopts = 18 | --doctest-rst 19 | -p no:unraisableexception 20 | -p no:threadexception 21 | filterwarnings = 22 | # Turn all warnings into errors so they do not pass silently. 23 | error 24 | # Do not fail on pytest config issues (i.e. missing plugins) but do show them 25 | always::pytest.PytestConfigWarning 26 | # A list of warnings to ignore follows. If you add to this list, you MUST 27 | # add a comment or ideally a link to an issue that explains why the warning 28 | # is being ignored 29 | # https://github.com/pandas-dev/pandas/issues/54466 30 | ignore:\nPyarrow will become a required dependency of pandas in the next major release of pandas:DeprecationWarning 31 | # See https://github.com/mvantellingen/python-zeep/issues/956 32 | ignore:defusedxml.lxml is no longer supported:DeprecationWarning 33 | ignore:numpy.ndarray size changed 34 | # This is coming from astropy 35 | ignore:The distutils package is deprecated 36 | # Zeep relies on deprecated cgi in Python 3.11 37 | ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning:zeep.utils 38 | # Not sure what these are 39 | ignore:unclosed None: 26 | self.value = value.lower() 27 | 28 | 29 | class SOOP(SimpleAttr): 30 | """ 31 | The SOOP name to search for. 32 | """ 33 | 34 | 35 | class Distance(Range): 36 | """ 37 | Specifies the distance range. 38 | 39 | Parameters 40 | ---------- 41 | dist_min : `~astropy.units.Quantity` 42 | The lower bound of the range. 43 | dist_max : `~astropy.units.Quantity` 44 | The upper bound of the range. 45 | 46 | Notes 47 | ----- 48 | The valid units for distance are AU, km, and mm. Any unit directly 49 | convertible to these units is valid input. This class filters the query 50 | by solar distance without relying on a specific distance column. 51 | """ 52 | 53 | @quantity_input(dist_min=u.m, dist_max=u.m) 54 | def __init__(self, dist_min: u.Quantity, dist_max: u.Quantity): 55 | # Ensure both dist_min and dist_max are scalar values 56 | if not all([dist_min.isscalar, dist_max.isscalar]): 57 | msg = "Both dist_min and dist_max must be scalar values." 58 | raise ValueError(msg) 59 | 60 | target_unit = u.AU 61 | # Convert both dist_min and dist_max to the target unit 62 | dist_min = dist_min.to(target_unit) 63 | dist_max = dist_max.to(target_unit) 64 | 65 | super().__init__(dist_min, dist_max) 66 | 67 | def collides(self, other): 68 | """ 69 | Check if the other attribute collides with this attribute. 70 | """ 71 | return isinstance(other, self.__class__) 72 | 73 | 74 | walker = AttrWalker() 75 | 76 | 77 | @walker.add_creator(AttrOr) 78 | def create_or(wlk, tree): 79 | """ 80 | Creator for OR. 81 | 82 | Loops through the next level down in the tree and appends the 83 | individual results to a list. 84 | """ 85 | return [wlk.create(sub) for sub in tree.attrs] 86 | 87 | 88 | @walker.add_creator(AttrAnd, DataAttr) 89 | def create_and(wlk, tree): 90 | """ 91 | Creator for And and other simple attributes. 92 | 93 | No walking needs to be done, so simply call the applier function. 94 | """ 95 | result = [] 96 | wlk.apply(tree, result) 97 | return [result] 98 | 99 | 100 | @walker.add_applier(AttrAnd) 101 | def apply_and(wlk, and_attr, params) -> None: 102 | """ 103 | Applier for And. 104 | 105 | Parameters 106 | ---------- 107 | wlk : AttrWalker 108 | and_attr : AttrAnd 109 | The AND attribute being applied. The individual attributes being 110 | AND'ed together are accessible with ``and_attr.attrs``. 111 | params : list[str] 112 | List of search parameters. 113 | """ 114 | for iattr in and_attr.attrs: 115 | wlk.apply(iattr, params) 116 | 117 | 118 | """ 119 | Below are appliers for individual attributes. 120 | 121 | The all convert the attribute object into a query string, that will eventually 122 | be passed as a query to the SOAR server. They all have the signature: 123 | 124 | Parameters 125 | ---------- 126 | wlk : AttrWalker 127 | The attribute walker. 128 | attr : 129 | The attribute being applied. 130 | params : list[str] 131 | List of search parameters. 132 | """ 133 | 134 | 135 | @walker.add_applier(a.Time) 136 | def _(wlk, attr, params) -> None: 137 | start = attr.start.strftime("%Y-%m-%d %H:%M:%S") 138 | end = attr.end.strftime("%Y-%m-%d %H:%M:%S") 139 | params.append(f"begin_time>='{start}' AND begin_time<='{end}'") 140 | 141 | 142 | @walker.add_applier(a.Level) 143 | def _(wlk, attr, params) -> None: 144 | level = attr.value 145 | if isinstance(level, int): 146 | level = f"L{level}" 147 | 148 | level = level.upper() 149 | allowed_levels = ("L0", "L1", "L2", "L3", "LL01", "LL02", "LL03") 150 | if level not in allowed_levels: 151 | warnings.warn( 152 | f"level not in list of allowed levels for SOAR: {allowed_levels}", 153 | SunpyUserWarning, 154 | stacklevel=2, 155 | ) 156 | 157 | params.append(f"level='{level}'") 158 | 159 | 160 | @walker.add_applier(a.Instrument) 161 | def _(wlk, attr, params) -> None: 162 | params.append(f"instrument='{attr.value}'") 163 | 164 | 165 | @walker.add_applier(Product) 166 | def _(wlk, attr, params) -> None: 167 | params.append(f"descriptor='{attr.value}'") 168 | 169 | 170 | @walker.add_applier(a.Provider) 171 | def _(wlk, attr, params) -> None: 172 | params.append(f"provider='{attr.value}'") 173 | 174 | 175 | @walker.add_applier(SOOP) 176 | def _(wlk, attr, params) -> None: 177 | params.append(f"soop_name='{attr.value}'") 178 | 179 | 180 | @walker.add_applier(a.Detector) 181 | def _(wlk, attr, params) -> None: 182 | params.append(f"Detector='{attr.value}'") 183 | 184 | 185 | @walker.add_applier(a.Wavelength) 186 | def _(wlk, attr, params) -> None: 187 | wavemin = attr.min.value 188 | wavemax = attr.max.value 189 | params.append(f"Wavemin='{wavemin}' AND Wavemax='{wavemax}'") 190 | 191 | 192 | @walker.add_applier(Distance) 193 | def _(wlk, attr, params): 194 | # The `Distance` attribute is used to filter the query by solar distance 195 | # without relying on a specific distance column. It is commonly used 196 | # to filter the query without time consideration. 197 | dmin = attr.min.value 198 | dmax = attr.max.value 199 | min_possible = 0.28 200 | max_possible = 1.0 201 | 202 | if not (min_possible <= dmin <= max_possible) or not (min_possible <= dmax <= max_possible): 203 | warnings.warn( 204 | "Distance values must be within the range 0.28 AU to 1.0 AU.", 205 | SunpyUserWarning, 206 | stacklevel=2, 207 | ) 208 | params.append(f"DISTANCE({dmin},{dmax})") 209 | -------------------------------------------------------------------------------- /sunpy_soar/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file defines the SOARClient class which is used to access the Solar 3 | Orbiter Archive (SOAR). 4 | """ 5 | 6 | import json 7 | import pathlib 8 | import re 9 | from copy import copy 10 | from json.decoder import JSONDecodeError 11 | 12 | import astropy.table 13 | import astropy.units as u 14 | import requests 15 | import sunpy.net.attrs as a 16 | from sunpy import log 17 | from sunpy.net.attr import and_ 18 | from sunpy.net.base_client import BaseClient, QueryResponseTable 19 | from sunpy.time import parse_time 20 | 21 | from sunpy_soar.attrs import SOOP, Distance, Product, walker 22 | 23 | __all__ = ["SOARClient"] 24 | 25 | 26 | class SOARClient(BaseClient): 27 | """ 28 | Provides access to Solar Orbiter Archive (SOAR) which provides data for 29 | Solar Orbiter. 30 | 31 | References 32 | ---------- 33 | * `SOAR `__ 34 | """ 35 | 36 | def search(self, *query, **kwargs): 37 | r""" 38 | Query this client for a list of results. 39 | 40 | Parameters 41 | ---------- 42 | *args: `tuple` 43 | `sunpy.net.attrs` objects representing the query. 44 | **kwargs: `dict` 45 | Any extra keywords to refine the search. 46 | Unused by this client. 47 | 48 | Returns 49 | ------- 50 | A ``QueryResponseTable`` instance containing the query result. 51 | """ 52 | query = and_(*query) 53 | queries = walker.create(query) 54 | 55 | results = [] 56 | for query_parameters in queries: 57 | if "provider='SOAR'" in query_parameters: 58 | query_parameters.remove("provider='SOAR'") 59 | results.append(self._do_search(query_parameters)) 60 | table = astropy.table.vstack(results) 61 | qrt = QueryResponseTable(table, client=self) 62 | qrt["Filesize"] = (qrt["Filesize"] * u.byte).to(u.Mbyte).round(3) 63 | qrt.hide_keys = ["Data item ID", "Filename"] 64 | return qrt 65 | 66 | @staticmethod 67 | def add_join_to_query(query: list[str], data_table: str, instrument_table: str): 68 | """ 69 | Construct the WHERE, FROM, and SELECT parts of the ADQL query. 70 | 71 | Parameters 72 | ---------- 73 | query : list[str] 74 | List of query items. 75 | data_table : str 76 | Name of the data table. 77 | instrument_table : str 78 | Name of the instrument table. 79 | 80 | Returns 81 | ------- 82 | tuple[str, str, str] 83 | WHERE, FROM, and SELECT parts of the query. 84 | """ 85 | final_query = "" 86 | # Extract wavemin and wavemax individually 87 | wavemin_pattern = re.compile(r"Wavemin='(\d+\.\d+)'") 88 | wavemax_pattern = re.compile(r"Wavemax='(\d+\.\d+)'") 89 | for current_parameter in query: 90 | parameter = copy(current_parameter) 91 | wavemin_match = wavemin_pattern.search(parameter) 92 | wavemax_match = wavemax_pattern.search(parameter) 93 | # If the wavemin and wavemax are same that means only one wavelength is given in query. 94 | if wavemin_match and wavemax_match and float(wavemin_match.group(1)) == float(wavemax_match.group(1)): 95 | # For PHI and SPICE, we can specify wavemin and wavemax in the query and get the results. 96 | # For PHI we have wavelength data in both angstrom and nanometer without it being mentioned in the SOAR. 97 | # For SPICE we get data in form of wavemin/wavemax columns, but only for the first spectral window. 98 | # To make sure this data is not misleading to the user we do not return any values for PHI AND SPICE. 99 | parameter = f"Wavelength='{wavemin_match.group(1)}'" 100 | elif wavemin_match and wavemax_match: 101 | parameter = f"Wavemin='{wavemin_match.group(1)}' AND h2.Wavemax='{wavemax_match.group(1)}'" 102 | prefix = "h1." if not parameter.startswith("Detector") and not parameter.startswith("Wave") else "h2." 103 | if parameter.startswith("begin_time"): 104 | time_list = parameter.split(" AND ") 105 | final_query += f"h1.{time_list[0]} AND h1.{time_list[1]} AND " 106 | # As there are no dimensions in STIX, the dimension index need not be included in the query for STIX. 107 | if "stx" not in instrument_table: 108 | # To avoid duplicate rows in the output table, the dimension index is set to 1. 109 | final_query += "h2.dimension_index='1' AND " 110 | else: 111 | final_query += f"{prefix}{parameter} AND " 112 | 113 | where_part = final_query[:-5] 114 | from_part = f"{data_table} AS h1" 115 | select_part = ( 116 | "h1.instrument, h1.descriptor, h1.level, h1.begin_time, h1.end_time, " 117 | "h1.data_item_id, h1.filesize, h1.filename, h1.soop_name" 118 | ) 119 | if instrument_table: 120 | from_part += f" JOIN {instrument_table} AS h2 USING (data_item_oid)" 121 | select_part += ", h2.detector, h2.wavelength, h2.dimension_index" 122 | return where_part, from_part, select_part 123 | 124 | @staticmethod 125 | def _construct_payload(query): 126 | """ 127 | Construct search payload. 128 | 129 | Parameters 130 | ---------- 131 | query : list[str] 132 | List of query items. 133 | 134 | Returns 135 | ------- 136 | dict 137 | Payload dictionary to be sent with the query. 138 | """ 139 | # Default data table 140 | data_table = "v_sc_data_item" 141 | instrument_table = None 142 | query_method = "doQuery" 143 | # Mapping is established between the SOAR instrument names and its corresponding SOAR instrument table alias. 144 | instrument_mapping = { 145 | "SOLOHI": "SHI", 146 | "EUI": "EUI", 147 | "STIX": "STX", 148 | "SPICE": "SPI", 149 | "PHI": "PHI", 150 | "METIS": "MET", 151 | } 152 | 153 | instrument_name = None 154 | distance_parameter = [] 155 | non_distance_parameters = [] 156 | query_method = "doQuery" 157 | instrument_name = None 158 | 159 | for q in query: 160 | if "DISTANCE" in str(q): 161 | distance_parameter.append(q) 162 | else: 163 | non_distance_parameters.append(q) 164 | if q.startswith("instrument") or (q.startswith("descriptor") and not instrument_name): 165 | instrument_name = q.split("=")[1][1:-1].split("-")[0].upper() 166 | elif q.startswith("level") and q.split("=")[1][1:3] == "LL": 167 | data_table = "v_ll_data_item" 168 | 169 | query = non_distance_parameters + distance_parameter 170 | if distance_parameter: 171 | query_method = "doQueryFilteredByDistance" 172 | if instrument_name: 173 | if instrument_name in instrument_mapping: 174 | instrument_name = instrument_mapping[instrument_name] 175 | instrument_table = f"v_{instrument_name.lower()}_sc_fits" 176 | if data_table == "v_ll_data_item" and instrument_table: 177 | instrument_table = instrument_table.replace("_sc_", "_ll_") 178 | 179 | # Need to establish join for remote sensing instruments as they have instrument tables in SOAR. 180 | if instrument_name in ["EUI", "MET", "SPI", "PHI", "SHI"]: 181 | where_part, from_part, select_part = SOARClient.add_join_to_query(query, data_table, instrument_table) 182 | else: 183 | from_part = data_table 184 | select_part = "*" 185 | where_part = " AND ".join(query) 186 | 187 | adql_query = {"SELECT": select_part, "FROM": from_part, "WHERE": where_part} 188 | adql_query_str = " ".join([f"{key} {value}" for key, value in adql_query.items()]) 189 | if query_method == "doQueryFilteredByDistance": 190 | adql_query_str = adql_query_str.replace(" AND h1.DISTANCE", "&DISTANCE").replace( 191 | " AND DISTANCE", "&DISTANCE" 192 | ) 193 | return {"REQUEST": query_method, "LANG": "ADQL", "FORMAT": "json", "QUERY": adql_query_str} 194 | 195 | @staticmethod 196 | def _do_search(query): 197 | """ 198 | Query the SOAR server with a single query. 199 | 200 | Parameters 201 | ---------- 202 | query : list[str] 203 | List of query items. 204 | 205 | Returns 206 | ------- 207 | astropy.table.QTable 208 | Query results. 209 | """ 210 | tap_endpoint = "http://soar.esac.esa.int/soar-sl-tap/tap" 211 | payload = SOARClient._construct_payload(query) 212 | # Need to force requests to not form-encode the parameters 213 | payload = "&".join([f"{key}={val}" for key, val in payload.items()]) 214 | # Get request info 215 | r = requests.get(f"{tap_endpoint}/sync", params=payload, timeout=60) 216 | log.debug(f"Sent query: {r.url}") 217 | r.raise_for_status() 218 | 219 | try: 220 | response_json = r.json() 221 | except JSONDecodeError as err: 222 | msg = "The SOAR server returned an invalid JSON response. It may be down or not functioning correctly." 223 | raise RuntimeError(msg) from err 224 | 225 | names = [m["name"] for m in response_json["metadata"]] 226 | info = {name: [] for name in names} 227 | 228 | for entry in response_json["data"]: 229 | for i, name in enumerate(names): 230 | info[name].append(entry[i]) 231 | 232 | if len(info["begin_time"]): 233 | info["begin_time"] = parse_time(info["begin_time"]).iso 234 | info["end_time"] = parse_time(info["end_time"]).iso 235 | 236 | result_table = astropy.table.QTable( 237 | { 238 | "Instrument": info["instrument"], 239 | "Data product": info["descriptor"], 240 | "Level": info["level"], 241 | "Start time": info["begin_time"], 242 | "End time": info["end_time"], 243 | "Data item ID": info["data_item_id"], 244 | "Filename": info["filename"], 245 | "Filesize": info["filesize"], 246 | "SOOP Name": info["soop_name"], 247 | }, 248 | ) 249 | if "detector" in info: 250 | result_table["Detector"] = info["detector"] 251 | if "wavelength" in info: 252 | result_table["Wavelength"] = info["wavelength"] 253 | result_table.sort("Start time") 254 | return result_table 255 | 256 | def fetch(self, query_results, *, path, downloader, **kwargs) -> None: 257 | """ 258 | Queue a set of results to be downloaded. 259 | `sunpy.net.base_client.BaseClient` does the actual downloading, so we 260 | just have to queue up the ``downloader``. 261 | 262 | Parameters 263 | ---------- 264 | query_results : sunpy.net.fido_factory.UnifiedResponse 265 | Results from a Fido search. 266 | path : str 267 | Path to download files to. Must be a format string with a ``file`` 268 | field for the filename. 269 | downloader : parfive.Downloader 270 | Downloader instance used to download data. 271 | kwargs : 272 | Keyword arguments aren't used by this client. 273 | """ 274 | base_url = "http://soar.esac.esa.int/soar-sl-tap/data?" "retrieval_type=LAST_PRODUCT" 275 | 276 | for row in query_results: 277 | url = base_url 278 | if row["Level"].startswith("LL"): 279 | url += "&product_type=LOW_LATENCY" 280 | else: 281 | url += "&product_type=SCIENCE" 282 | data_id = row["Data item ID"] 283 | url += f"&data_item_id={data_id}" 284 | filepath = str(path).format(file=row["Filename"], **row.response_block_map) 285 | log.debug(f"Queuing URL: {url}") 286 | downloader.enqueue_file(url, filename=filepath) 287 | 288 | @classmethod 289 | def _can_handle_query(cls, *query) -> bool: 290 | """ 291 | Check if this client can handle a given Fido query. Checks to see if a 292 | SOAR instrument or product is provided in the query. 293 | 294 | Returns 295 | ------- 296 | bool 297 | True if this client can handle the given query. 298 | """ 299 | required = {Distance} if any(isinstance(q, Distance) for q in query) else {a.Time} 300 | 301 | optional = {a.Instrument, a.Detector, a.Wavelength, a.Level, a.Provider, Product, SOOP, Distance, a.Time} 302 | if not cls.check_attr_types_in_query(query, required, optional): 303 | return False 304 | # check to make sure the instrument attr passed is one provided by the SOAR. 305 | # also check to make sure that the provider passed is the SOAR for which this client can handle. 306 | instr = [i[0].lower() for i in cls.register_values()[a.Instrument]] 307 | for x in query: 308 | if isinstance(x, a.Instrument) and str(x.value).lower() not in instr: 309 | return False 310 | if isinstance(x, a.Provider) and str(x.value).lower() != "soar": 311 | return False 312 | return True 313 | 314 | @classmethod 315 | def _attrs_module(cls): 316 | # Register SOAR specific attributes with Fido 317 | return "soar", "sunpy_soar.attrs" 318 | 319 | @classmethod 320 | def register_values(cls): 321 | """ 322 | Register the SOAR specific attributes with Fido. 323 | 324 | Returns 325 | ------- 326 | dict 327 | The dictionary containing the values formed into attributes. 328 | """ 329 | return cls.load_dataset_values() 330 | 331 | @staticmethod 332 | def load_dataset_values(): 333 | """ 334 | Loads the net attribute values from the JSON file. 335 | 336 | Returns 337 | ------- 338 | dict 339 | The dictionary containing the values formed into attributes. 340 | """ 341 | # Instrument attrs 342 | attrs_path = pathlib.Path(__file__).parent / "data" / "attrs.json" 343 | with attrs_path.open() as attrs_file: 344 | all_datasets = json.load(attrs_file) 345 | # Convert from dict to list of tuples 346 | all_datasets = list(all_datasets.items()) 347 | 348 | # Instrument attrs 349 | instr_path = pathlib.Path(__file__).parent / "data" / "instrument_attrs.json" 350 | with instr_path.open() as instr_attrs_file: 351 | all_instr = json.load(instr_attrs_file) 352 | all_instr = list(all_instr.items()) 353 | 354 | soop_path = pathlib.Path(__file__).parent / "data" / "soop_attrs.json" 355 | with soop_path.open() as soop_path_file: 356 | all_soops = json.load(soop_path_file) 357 | 358 | all_soops = list(all_soops.items()) 359 | 360 | return { 361 | Product: all_datasets, 362 | a.Instrument: all_instr, 363 | SOOP: all_soops, 364 | a.Provider: [("SOAR", "Solar Orbiter Archive.")], 365 | } 366 | -------------------------------------------------------------------------------- /sunpy_soar/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="session", autouse=True) 7 | def _hide_parfive_progress(request): 8 | """ 9 | Set the PARFIVE_HIDE_PROGRESS to hide the parfive progress bar in tests. 10 | """ 11 | os.environ["PARFIVE_HIDE_PROGRESS"] = "True" 12 | yield 13 | del os.environ["PARFIVE_HIDE_PROGRESS"] 14 | -------------------------------------------------------------------------------- /sunpy_soar/data/README.rst: -------------------------------------------------------------------------------- 1 | Data directory 2 | ============== 3 | 4 | This directory contains data files included with the package source 5 | code distribution. Note that this is intended only for relatively small files 6 | - large files should be externally hosted and downloaded as needed. 7 | -------------------------------------------------------------------------------- /sunpy_soar/data/attrs.json: -------------------------------------------------------------------------------- 1 | { 2 | "RPW-SBM1": "Solar Orbiter Radio/Plasma Wave, LL01 parameters", 3 | "RPW-SBM2": "Solar Orbiter Radio/Plasma Wave, LL01 parameters", 4 | "RPW-TNR": "Solar Orbiter Radio/Plasma Wave, LL01 parameters", 5 | "SOC-ORBIT": "Solar Orbiter SOC Ancillary Orbit Data", 6 | "epd-ept-1min": "Solar Orbiter, Level 3 Data, Energetic Particle Detector, Electron Proton Telescope, 1 minute resolution data", 7 | "epd-ept-asun-burst-ele-close": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, Anti-Sun direction, Burst, Electrons, Close mode", 8 | "epd-ept-asun-burst-ion": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, Anti-Sun direction, Burst, Ions", 9 | "epd-ept-asun-hcad": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, Anti-Sun direction, High Cadence", 10 | "epd-ept-asun-rates": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, Anti-Sun direction, Rates", 11 | "epd-ept-north-burst-ele-close": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, North direction, Burst, Electrons, Close mode", 12 | "epd-ept-north-burst-ion": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, North direction, Burst, Ions", 13 | "epd-ept-north-hcad": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, North direction, High Cadence", 14 | "epd-ept-north-rates": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, North direction, Rates", 15 | "epd-ept-south-burst-ele-close": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, South direction, Burst, Electrons, Close mode", 16 | "epd-ept-south-burst-ion": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, South direction, Burst, Ions", 17 | "epd-ept-south-hcad": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, South direction, High Cadence", 18 | "epd-ept-south-rates": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, South direction, Rates", 19 | "epd-ept-sun-burst-ele-close": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, Sun direction, Burst, Electrons, Close mode", 20 | "epd-ept-sun-burst-ion": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, Sun direction, Burst, Ions", 21 | "epd-ept-sun-hcad": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, Sun direction, High Cadence", 22 | "epd-ept-sun-rates": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Electron Proton Telescope, Sun direction, Rates", 23 | "epd-epthet1-burst1-close": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, ecliptic unit, Burst product 1 close mode", 24 | "epd-epthet1-burst2-close": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, ecliptic unit, Burst product 2 close mode", 25 | "epd-epthet1-burst3-close": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, ecliptic unit, Burst product 3 close mode", 26 | "epd-epthet1-nom-close": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, ecliptic unit, Nominal product close mode", 27 | "epd-epthet1-nom-far": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, ecliptic unit, Nominal product far mode", 28 | "epd-epthet1-quicklook": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, ecliptic unit, Quicklook product", 29 | "epd-epthet1-sc": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, ecliptic unit, Single counters", 30 | "epd-epthet2-burst1-close": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, polar unit, Burst product 1 close mode", 31 | "epd-epthet2-burst2-close": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, polar unit, Burst product 2 close mode", 32 | "epd-epthet2-burst3-close": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, polar unit, Burst product 3 close mode", 33 | "epd-epthet2-nom-close": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, polar unit, Nominal product close mode", 34 | "epd-epthet2-nom-far": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, polar unit, Nominal product far mode", 35 | "epd-epthet2-quicklook": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, polar unit, Quicklook product", 36 | "epd-epthet2-sc": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Electron Proton Telescope, High Energy Telescope, polar unit, Single counters", 37 | "epd-het-asun-burst": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, High Energy Telescope, Anti-Sun direction, Burst", 38 | "epd-het-asun-rates": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, High Energy Telescope, Anti-Sun direction, Rates", 39 | "epd-het-north-burst": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, High Energy Telescope, North direction, Burst", 40 | "epd-het-north-rates": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, High Energy Telescope, North direction, Rates", 41 | "epd-het-south-burst": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, High Energy Telescope, South direction, Burst", 42 | "epd-het-south-rates": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, High Energy Telescope, South direction, Rates", 43 | "epd-het-sun-burst": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, High Energy Telescope, Sun direction, Burst", 44 | "epd-het-sun-rates": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, High Energy Telescope, Sun direction, Rates", 45 | "epd-sis-a-hehist": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Suprathermal Ion Spectrograph, A Telescope, Helium histogram", 46 | "epd-sis-a-rates": "Solar Orbiter, Level 1 Low Latency Data, Energetic Particle Detector, Suprathermal Ion Spectrograph, A Telescope, Particle Rates", 47 | "epd-sis-a-rates-fast": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Suprathermal Ion Spectrograph, A Telescope, Particle rates, fast cadence", 48 | "epd-sis-a-rates-medium": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Suprathermal Ion Spectrograph, A Telescope, Particle rates, medium cadence", 49 | "epd-sis-a-rates-slow": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Suprathermal Ion Spectrograph, A Telescope, Particle rates, slow cadence", 50 | "epd-sis-b-hehist": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Suprathermal Ion Spectrograph, B Telescope, Helium histogram", 51 | "epd-sis-b-rates": "Solar Orbiter, Level 1 Low Latency Data, Energetic Particle Detector, Suprathermal Ion Spectrograph, B Telescope, Particle Rates", 52 | "epd-sis-b-rates-fast": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Suprathermal Ion Spectrograph, B Telescope, Particle rates, fast cadence", 53 | "epd-sis-b-rates-medium": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, Suprathermal Ion Spectrograph, B Telescope, Particle rates, medium cadence", 54 | "epd-sis-b-rates-slow": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, Suprathermal Ion Spectrograph, B Telescope, Particle rates, slow cadence", 55 | "epd-step-aux": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, SupraThermal Electrons and Protons, Auxiliary product", 56 | "epd-step-burst": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, SupraThermal Electrons and Protons, Burst", 57 | "epd-step-burst1-close": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, SupraThermal Electrons and Protons, Burst product 1 close mode", 58 | "epd-step-hcad": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, SupraThermal Electrons and Protons, High Cadence", 59 | "epd-step-main": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, SupraThermal Electrons and Protons, Main product", 60 | "epd-step-main-close": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, SupraThermal Electrons and Protons, Main product close mode", 61 | "epd-step-main-far": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, SupraThermal Electrons and Protons, Main product far mode", 62 | "epd-step-nom-close": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, SupraThermal Electrons and Protons, Nominal product close mode", 63 | "epd-step-nom-far": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, SupraThermal Electrons and Protons, Nominal product far mode", 64 | "epd-step-quicklook": "Solar Orbiter, Level 1 Data, Energetic Particle Detector, SupraThermal Electrons and Protons, Quicklook product", 65 | "epd-step-rates": "Solar Orbiter, Level 2 Data, Energetic Particle Detector, SupraThermal Electrons and Protons, Rates", 66 | "eui-fsi174-image": "", 67 | "eui-fsi174-image-dark": "", 68 | "eui-fsi174-image-occulter": "", 69 | "eui-fsi174-image-short": "", 70 | "eui-fsi304-image": "", 71 | "eui-fsi304-image-dark": "", 72 | "eui-fsi304-image-led": "", 73 | "eui-fsi304-image-occulter": "", 74 | "eui-fsi304-image-short": "", 75 | "eui-fsiblk-image": "", 76 | "eui-fsiblk-image-dark": "", 77 | "eui-fsiblk-image-led": "", 78 | "eui-hrieuv###-image": "", 79 | "eui-hrieuv174-image": "", 80 | "eui-hrieuv174-image-dark": "", 81 | "eui-hrieuv174-image-led": "", 82 | "eui-hrieuv174-image-short": "", 83 | "eui-hrieuvblk-image": "", 84 | "eui-hrieuvblk-image-dark": "", 85 | "eui-hrieuvblk-image-led": "", 86 | "eui-hrieuvnon-image": "", 87 | "eui-hrieuvopn-image": "", 88 | "eui-hrieuvopn-image-dark": "", 89 | "eui-hrieuvopn-image-led": "", 90 | "eui-hrieuvzer-image": "", 91 | "eui-hrieuvzer-image-dark": "", 92 | "eui-hrieuvzer-image-led": "", 93 | "eui-hrieuvzer-image-short": "", 94 | "eui-hrilya1216-image": "", 95 | "eui-hrilya1216-image-dark": "", 96 | "eui-hrilya1216-image-led": "", 97 | "mag": "Solar Orbiter Level 2 Low Latency Magnetometer Data", 98 | "mag-ibs": "Solar Orbiter Magnetometer L1 Data", 99 | "mag-obs": "Solar Orbiter Magnetometer L1 Data", 100 | "mag-rtn": "Solar Orbiter Magnetometer L2 Data", 101 | "mag-rtn derived from LL data": "Solar Orbiter Magnetometer L2 Data derived from LL data", 102 | "mag-rtn-burst": "Solar Orbiter Magnetometer Level 2 Burst Mode Data in RTN coordinates", 103 | "mag-rtn-normal": "Solar Orbiter Magnetometer Level 2 Normal Mode Data in RTN coordinates", 104 | "mag-srf": "Solar Orbiter Magnetometer L2 Data", 105 | "mag-srf derived from LL data": "Solar Orbiter Magnetometer L2 Data derived from LL data", 106 | "mag-srf-burst": "Solar Orbiter Magnetometer Level 2 Burst Mode Data in SRF coordinates", 107 | "mag-srf-normal": "Solar Orbiter Magnetometer Level 2 Normal Mode Data in SRF coordinates", 108 | "mag-vso": "Solar Orbiter Magnetometer L2 Data", 109 | "mag-vso-burst": "Solar Orbiter Magnetometer Level 2 Burst Mode Data in VSO coordinates", 110 | "mag-vso-normal": "Solar Orbiter Magnetometer Level 2 Normal Mode Data in VSO coordinates", 111 | "metis-uv-image": "", 112 | "metis-vl-image": "", 113 | "metis-vl-pb": "", 114 | "metis-vl-pol-angle": "", 115 | "metis-vl-stokes": "", 116 | "metis-vl-tb": "", 117 | "phi-fdt-bazi": "", 118 | "phi-fdt-binc": "", 119 | "phi-fdt-blos": "", 120 | "phi-fdt-bmag": "", 121 | "phi-fdt-icnt": "", 122 | "phi-fdt-stokes": "", 123 | "phi-fdt-vlos": "", 124 | "phi-hrt-bazi": "", 125 | "phi-hrt-binc": "", 126 | "phi-hrt-blos": "", 127 | "phi-hrt-bmag": "", 128 | "phi-hrt-icnt": "", 129 | "phi-hrt-stokes": "", 130 | "phi-hrt-vlos": "", 131 | "rates": "Solar Orbiter Level 1 Solar Wind Analyser Heavy Ion Sensor Rates", 132 | "rpw-bia-density": "Solar Orbiter Radio/Plasma Wave, LFR L3 plasma density derived from the spacecraft potential", 133 | "rpw-bia-density-10-seconds": "Solar Orbiter Radio/Plasma Wave, LFR L3 plasma density derived from the spacecraft potential, downsampled", 134 | "rpw-bia-density-10-seconds-cdag": "Solar Orbiter Radio/Plasma Wave, LFR L3 plasma density derived from the spacecraft potential, downsampled", 135 | "rpw-bia-density-cdag": "Solar Orbiter Radio/Plasma Wave, LFR L3 plasma density derived from the spacecraft potential", 136 | "rpw-bia-efield": "Solar Orbiter Radio/Plasma Wave, LFR L3 electric field vector", 137 | "rpw-bia-efield-10-seconds": "Solar Orbiter Radio/Plasma Wave, LFR L3 electric field vector, downsampled", 138 | "rpw-bia-efield-10-seconds-cdag": "Solar Orbiter Radio/Plasma Wave, LFR L3 electric field vector, downsampled", 139 | "rpw-bia-efield-cdag": "Solar Orbiter Radio/Plasma Wave, LFR L3 electric field vector", 140 | "rpw-bia-scpot": "Solar Orbiter Radio/Plasma Wave, LFR L3 spacecraft potential", 141 | "rpw-bia-scpot-10-seconds": "Solar Orbiter Radio/Plasma Wave, LFR L3 spacecraft potential, downsampled", 142 | "rpw-bia-scpot-10-seconds-cdag": "Solar Orbiter Radio/Plasma Wave, LFR L3 spacecraft potential, downsampled", 143 | "rpw-bia-scpot-cdag": "Solar Orbiter Radio/Plasma Wave, LFR L3 spacecraft potential", 144 | "rpw-bia-vht": "Solar Orbiter Radio/Plasma Wave, LFR L3 de Hoffmann-Teller solar wind velocity", 145 | "rpw-hfr-surv": "Solar Orbiter Radio/Plasma Wave, HFR L2 parameters", 146 | "rpw-lfr-surv-asm": "Solar Orbiter Radio/Plasma Wave, LFR L2 parameters", 147 | "rpw-lfr-surv-bp1": "Solar Orbiter Radio/Plasma Wave, LFR L2 parameters", 148 | "rpw-lfr-surv-bp2": "Solar Orbiter Radio/Plasma Wave, LFR L2 parameters", 149 | "rpw-lfr-surv-cwf": "Solar Orbiter Radio/Plasma Wave, LFR L1 parameters", 150 | "rpw-lfr-surv-cwf-b": "Solar Orbiter, Level 2, Radio and Plasma Waves, Low Frequency Receiver, Continous Waveform of magnetic data in survey mode", 151 | "rpw-lfr-surv-cwf-e": "Solar Orbiter Radio/Plasma Wave, LFR L2 electric parameters", 152 | "rpw-lfr-surv-cwf-e-cdag": "Solar Orbiter Radio/Plasma Wave, LFR L2 electric parameters", 153 | "rpw-lfr-surv-swf": "Solar Orbiter Radio/Plasma Wave, LFR L1 parameters", 154 | "rpw-lfr-surv-swf-b": "Solar Orbiter, Level 2, Radio and Plasma Waves, Low Frequency Receiver, Snapshot Waveform of magnetic data in survey mode", 155 | "rpw-lfr-surv-swf-e": "Solar Orbiter Radio/Plasma Wave, LFR L2 electric parameters", 156 | "rpw-lfr-surv-swf-e-cdag": "Solar Orbiter Radio/Plasma Wave, LFR L2 electric parameters", 157 | "rpw-sbm1": "Solar Orbiter Radio/Plasma Wave, LL02 parameters", 158 | "rpw-sbm2": "Solar Orbiter Radio/Plasma Wave, LL02 parameters", 159 | "rpw-tds-surv-hist1d": "Solar Orbiter Radio/Plasma Wave, TDS L2 parameters", 160 | "rpw-tds-surv-hist2d": "Solar Orbiter Radio/Plasma Wave, TDS L2 parameters", 161 | "rpw-tds-surv-mamp": "Solar Orbiter Radio/Plasma Wave, TDS L2 parameters", 162 | "rpw-tds-surv-rswf": "Solar Orbiter Radio/Plasma Wave, TDS L1 parameters", 163 | "rpw-tds-surv-rswf-b": "Solar Orbiter, Level 2, Radio and Plasma Waves, Time Domain Sampler, Regular Snapshot Waveform of magnetic data in survey mode", 164 | "rpw-tds-surv-rswf-e": "Solar Orbiter Radio/Plasma Wave, TDS L2 waveform snapshots", 165 | "rpw-tds-surv-stat": "Solar Orbiter Radio/Plasma Wave, TDS L2R parameters", 166 | "rpw-tds-surv-tswf": "Solar Orbiter Radio/Plasma Wave, TDS L1 parameters", 167 | "rpw-tds-surv-tswf-b": "Solar Orbiter, Level 2, Radio and Plasma Waves, Time Domain Sampler, Triggered Snapshot Waveform of magnetic data in survey mode", 168 | "rpw-tds-surv-tswf-e": "Solar Orbiter Radio/Plasma Wave, TDS L2 waveform snapshots", 169 | "rpw-tnr": "Solar Orbiter Radio/Plasma Wave, LL02 parameters", 170 | "rpw-tnr-fp": "Solar Orbiter Radio/Plasma Wave, data from plasma peak tracking L3", 171 | "rpw-tnr-surv": "Solar Orbiter Radio/Plasma Wave, TNR L2 parameters", 172 | "sensorrates": "solo_L1_swa-his-sensor_rates", 173 | "solohi-11s": "", 174 | "solohi-11t": "", 175 | "solohi-12t": "", 176 | "solohi-1ft": "", 177 | "solohi-1ut": "", 178 | "solohi-1vt": "", 179 | "solohi-21s": "", 180 | "solohi-21t": "", 181 | "solohi-22t": "", 182 | "solohi-23t": "", 183 | "solohi-24t": "", 184 | "solohi-2ft": "", 185 | "solohi-2us": "", 186 | "solohi-2ut": "", 187 | "solohi-31t": "", 188 | "solohi-32t": "", 189 | "solohi-3fg": "", 190 | "solohi-3ft": "", 191 | "solohi-3ut": "", 192 | "solohi-4-001800001780": "", 193 | "solohi-41t": "", 194 | "solohi-42t": "", 195 | "solohi-4fg": "", 196 | "solohi-4ft": "", 197 | "solohi-4ut": "", 198 | "solohi-map1-3": "", 199 | "solohi-map2-0": "", 200 | "solohi-map3-0": "", 201 | "solohi-map3-3": "", 202 | "solohi-map4-0": "", 203 | "spice-n-exp": "", 204 | "spice-n-ras": "", 205 | "spice-n-ras-int": "", 206 | "spice-n-sit": "", 207 | "spice-w-exp": "", 208 | "spice-w-ras": "", 209 | "spice-w-sit": "", 210 | "stix-cal-energy": "", 211 | "stix-hk-maxi": "", 212 | "stix-hk-mini": "", 213 | "stix-ql-background": "", 214 | "stix-ql-flareflag": "", 215 | "stix-ql-lightcurve": "", 216 | "stix-ql-spectra": "", 217 | "stix-ql-variance": "", 218 | "stix-sci-aspect-burst": "", 219 | "stix-sci-xray-cpd": "", 220 | "stix-sci-xray-cpd-sup1": "", 221 | "stix-sci-xray-rpd": "", 222 | "stix-sci-xray-rpd-sup1": "", 223 | "stix-sci-xray-scpd": "", 224 | "stix-sci-xray-scpd-sup1": "", 225 | "stix-sci-xray-spec": "", 226 | "stix-sci-xray-spec-sup1": "", 227 | "stix-sci-xray-spec-sup2": "", 228 | "stix-sci-xray-vis": "", 229 | "stix-sci-xray-vis-sup1": "", 230 | "swa-eas-PartMoms": "SWA-EAS Partial Moments data", 231 | "swa-eas-nmpad-psd": "SWA-EAS pitch angles from 3D psd data", 232 | "swa-eas-pad-def": "Solar Orbiter, Level L2, Solar Wind Analyser, Electron Analyser System, Pitch Angle Distributions, Differential Energy Flux", 233 | "swa-eas-pad-dnf": "Solar Orbiter, Level L2, Solar Wind Analyser, Electron Analyser System, Pitch Angle Distributions, Differential Number Flux", 234 | "swa-eas-pad-psd": "Solar Orbiter, Level L2, Solar Wind Analyser, Electron Analyser System, Pitch Angle Distributions, Phase Space Density", 235 | "swa-eas-padc": "Solar Orbiter, Level L1, Solar Wind Analyzer, Electron Analyser System, Pitch Angle Distribution Counts", 236 | "swa-eas-ss": "SWA-EAS Low Latency LL02 Data", 237 | "swa-eas1-NM3D": "SWA-EAS1 Nominal Mode 3D data", 238 | "swa-eas1-SSc": "SWA-EAS1 Single Strahl data", 239 | "swa-eas1-eflux": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 1, Energy Flux", 240 | "swa-eas1-nm3d": "Solar Orbiter, Level L1, Solar Wind Analyzer, Electron Analyser System 1, Nominal Mode 3D", 241 | "swa-eas1-nm3d-def": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 1, Nominal Mode 3D, Differential Energy Flux", 242 | "swa-eas1-nm3d-dnf": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 1, Nominal Mode 3D, Differential Number Flux", 243 | "swa-eas1-nm3d-psd": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 1, Nominal Mode 3D, Phase Space Density", 244 | "swa-eas1-ss-def": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 1, Single Energy Strahl, Differential Energy Flux", 245 | "swa-eas1-ss-dnf": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 1, Single Energy Strahl, Differential Number Flux", 246 | "swa-eas1-ss-psd": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 1, Single Energy Strahl, Phase Space Density", 247 | "swa-eas1-ssc": "Solar Orbiter, Level L1, Solar Wind Analyzer, Electron Analyser System 1, Single Energy Strahl Counts", 248 | "swa-eas2-NM3D": "SWA-EAS2 Nominal Mode 3D data", 249 | "swa-eas2-SSc": "SWA-EAS2 Single Strahl data", 250 | "swa-eas2-eflux": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 2, Energy Flux", 251 | "swa-eas2-nm3d": "Solar Orbiter, Level L1, Solar Wind Analyzer, Electron Analyser System 2, Nominal Mode 3D", 252 | "swa-eas2-nm3d-def": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 2, Nominal Mode 3D, Differential Energy Flux", 253 | "swa-eas2-nm3d-dnf": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 2, Differential Number Flux", 254 | "swa-eas2-nm3d-psd": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 2, Nominal Mode 3D, Phase Space Density", 255 | "swa-eas2-ss-def": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 2, Single Energy Strahl, Differential Energy Flux", 256 | "swa-eas2-ss-dnf": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 2, Single Energy Strahl, Differential Number Flux", 257 | "swa-eas2-ss-psd": "Solar Orbiter, Level L2, Solar Wind Analyzer, Electron Analyser System 2, Single Energy Strahl, Phase Space Density", 258 | "swa-eas2-ssc": "Solar Orbiter, Level L1, Solar Wind Analyzer, Electron Analyser System 2, Single Energy Strahl Counts", 259 | "swa-his-comp-10min": "Solar Orbiter, Level 3 Data, Solar Wind Analyser, Heavy Ion Sensor Composition 10 Minute Resolution", 260 | "swa-his-hk": "Solar Orbiter Level 2 Solar Wind Analyser Heavy Ion Sensor Housekeeping Parameters", 261 | "swa-his-pha": "Solar Orbiter Level 2 Solar Wind Analyser Heavy Ion Sensor Pulse Height Analyzed", 262 | "swa-his-rat": "SWA-HIS LL02 Ratios and Spectra", 263 | "swa-pas-3d": "Solar Orbiter Proton Analyser Sensor L1 data", 264 | "swa-pas-cal": "Solar Orbiter Proton Analyser Sensor L1 Inflight calibration", 265 | "swa-pas-eflux": "Solar Orbiter Proton Analyser Sensor L2 data", 266 | "swa-pas-grnd-mom": "Solar Orbiter Proton Analyser Sensor L2 data", 267 | "swa-pas-mom": "Solar Orbiter Proton Analyser Sensor L1 Onboard Moments", 268 | "swa-pas-vdf": "Solar Orbiter Proton Analyser Sensor L2 data" 269 | } -------------------------------------------------------------------------------- /sunpy_soar/data/instrument_attrs.json: -------------------------------------------------------------------------------- 1 | { 2 | "EPD": "Energetic Particle Detector", 3 | "EUI": "Extreme UV Imager", 4 | "MAG": "Magnetometer", 5 | "METIS": "Metis: Visible light and ultraviolet coronagraph", 6 | "PHI": "Polarimetric and Helioseismic Imager", 7 | "RPW": "Radio and Plasma Wave instrument", 8 | "SOLOHI": "Solar Orbiter Heliospheric Imager", 9 | "SPICE": "SPectral Investigation of the Coronal Environment", 10 | "STIX": "Spectrometer Telescope for Imaging X-rays", 11 | "SWA": "Solar Wind Analyser" 12 | } -------------------------------------------------------------------------------- /sunpy_soar/data/soop_attrs.json: -------------------------------------------------------------------------------- 1 | { 2 | "CC_OFFPOI_ALIGNMENT": "", 3 | "CC_OFFPOI_FLATFIELD_FULL": "", 4 | "CC_OFFPOI_FLATFIELD_HRI": "", 5 | "CC_OFFPOI_OOF": "", 6 | "CC_OFFPOI_STAR": "", 7 | "CC_OFFPOI_STRAYLIGHT": "", 8 | "CC_ROLLS_RS": "", 9 | "COORD_CALIBRATION": "", 10 | "I_DEFAULT": "", 11 | "L_BOTH_HRES_HCAD_Major-Flare": "", 12 | "L_BOTH_HRES_LCAD_CH-Boundary-Expansion": "", 13 | "L_BOTH_LRES_MCAD_Pole-to-Pole": "", 14 | "L_BOTH_MRES_MCAD_Farside-Connection": "", 15 | "L_BOTH_MRES_MCAD_Flare-SEPs": "", 16 | "L_FULL_HRES_HCAD_Coronal-Dynamics": "", 17 | "L_FULL_HRES_HCAD_Eruption-Watch": "", 18 | "L_FULL_HRES_LCAD_MagnFieldConfig": "", 19 | "L_FULL_HRES_MCAD_Coronal-He-Abundance": "", 20 | "L_FULL_LRES_MCAD_Coronal-Synoptic": "", 21 | "L_FULL_LRES_MCAD_Probe-Quadrature": "", 22 | "L_FULL_MRES_MCAD_CME-SEPs": "", 23 | "L_IS_STIX": "", 24 | "L_IS_SoloHI_STIX": "", 25 | "L_SMALL_HRES_HCAD_Fast-Wind": "", 26 | "L_SMALL_HRES_HCAD_Slow-Wind-Connection": "", 27 | "L_SMALL_MRES_MCAD_Ballistic-Connection": "", 28 | "L_SMALL_MRES_MCAD_Composition-Mosaic": "", 29 | "L_SMALL_MRES_MCAD_Connection-Mosaic": "", 30 | "L_SMALL_MRES_MCAD_Earth-Quadrature": "", 31 | "L_TEMPORARY": "", 32 | "R_BOTH_HRES_HCAD_Filaments": "", 33 | "R_BOTH_HRES_HCAD_Nanoflares": "", 34 | "R_BOTH_HRES_MCAD_Bright-Points": "", 35 | "R_FULL_HRES_HCAD_Density-Fluctuations": "", 36 | "R_FULL_LRES_HCAD_Full-Disk-Helioseismology": "", 37 | "R_FULL_LRES_LCAD_Out-of-RSW-synoptics": "", 38 | "R_FULL_LRES_LCAD_Transition-Corona": "", 39 | "R_SMALL_HRES_HCAD_AR-Dynamics": "", 40 | "R_SMALL_HRES_HCAD_Atmospheric-Dynamics-Structure": "", 41 | "R_SMALL_HRES_HCAD_Ephemeral": "", 42 | "R_SMALL_HRES_HCAD_Granulation-Tracking": "", 43 | "R_SMALL_HRES_HCAD_Local-Area-Helioseismology": "", 44 | "R_SMALL_HRES_HCAD_PDF-Mosaic": "", 45 | "R_SMALL_HRES_HCAD_RS-burst": "", 46 | "R_SMALL_HRES_HCAD_Wave-Stereoscopy": "", 47 | "R_SMALL_HRES_LCAD_Composition-vs-Height": "", 48 | "R_SMALL_HRES_LCAD_Fine-Scale-Structure": "", 49 | "R_SMALL_HRES_MCAD_AR-Heating": "", 50 | "R_SMALL_HRES_MCAD_Full-Disk-Mosaic": "", 51 | "R_SMALL_HRES_MCAD_Polar-Observations": "", 52 | "R_SMALL_MRES_HCAD_Sunspot-Oscillations": "", 53 | "R_SMALL_MRES_MCAD_AR-Long-Term": "" 54 | } -------------------------------------------------------------------------------- /sunpy_soar/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains package tests. 3 | """ 4 | -------------------------------------------------------------------------------- /sunpy_soar/tests/test_sunpy_soar.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import astropy.units as u 4 | import pytest 5 | import responses 6 | import sunpy.map 7 | from requests.exceptions import HTTPError 8 | from sunpy.net import Fido 9 | from sunpy.net import attrs as a 10 | from sunpy.util.exceptions import SunpyUserWarning 11 | 12 | from sunpy_soar.client import SOARClient 13 | 14 | 15 | def test_search() -> None: 16 | instrument = a.Instrument("EUI") 17 | time = a.Time("2022-02-11", "2022-02-12") 18 | level = a.Level(1) 19 | product = a.soar.Product("eui-fsi174-image") 20 | 21 | res = Fido.search(instrument, time, level, product) 22 | assert len(res) == 1 23 | assert len(res[0]) == 660 24 | assert u.allclose(res[0, 0]["Filesize"], 2.439 * u.Mbyte) 25 | 26 | # check passing upper case descriptor 27 | product = a.soar.Product("EUI-FSI174-IMAGE") 28 | res = Fido.search(time, level, product) 29 | assert res.file_num == 660 30 | 31 | files = Fido.fetch(res[0, 0]) 32 | assert len(files) == 1 33 | fname = files[0] 34 | 35 | assert u.allclose(Path(fname).stat().st_size * u.byte, res[0, 0]["Filesize"], atol=1e-3 * u.Mbyte) 36 | 37 | # Smoke test that we can read this into a map 38 | sunpy.map.Map(fname) 39 | 40 | 41 | def test_search_low_latency() -> None: 42 | time = a.Time("2020-11-13", "2020-11-14") 43 | level = a.Level("LL02") 44 | product = a.soar.Product("epd-het-asun-rates") 45 | 46 | res = Fido.search(time, level, product) 47 | assert len(res) == 1 48 | assert len(res[0]) == 1 49 | 50 | files = Fido.fetch(res[0, 0]) 51 | assert len(files) == 1 52 | 53 | 54 | def test_insitu_search() -> None: 55 | instrument = a.Instrument("MAG") 56 | time = a.Time("2020-04-16", "2020-04-17") 57 | product = a.soar.Product("mag-rtn-normal-1-minute") 58 | 59 | res = Fido.search(instrument, time, product) 60 | assert len(res) == 1 61 | assert len(res[0]) == 2 62 | 63 | files = Fido.fetch(res[0, 0]) 64 | assert len(files) == 1 65 | 66 | 67 | def test_no_results() -> None: 68 | instrument = a.Instrument("EUI") 69 | time = a.Time("2019-02-01", "2019-02-02") 70 | query = instrument & time 71 | 72 | res = SOARClient().search(query) 73 | assert len(res) == 0 74 | 75 | 76 | def test_no_instrument() -> None: 77 | # Check that a time only search returns results 78 | time = a.Time("2020-04-16", "2020-04-17") 79 | res = SOARClient().search(time) 80 | assert len(res) == 63 81 | 82 | 83 | def test_download_path(tmp_path) -> None: 84 | # Check that we can download things to a custom path using 85 | # the search parameters 86 | instrument = a.Instrument("EUI") 87 | time = a.Time("2021-02-01", "2021-02-02") 88 | level = a.Level(1) 89 | res = Fido.search(instrument & time & level) 90 | files = Fido.fetch(res[0, 0], path=tmp_path / "{instrument}") 91 | assert len(files) == 1 92 | for f in files: 93 | assert "EUI" in f 94 | 95 | 96 | def test_registered_attrs() -> None: 97 | attr_str = str(a.soar.Product) 98 | # Check that at least one attr value is registered 99 | assert "epd_ept_asun_burst_ele_close" in attr_str 100 | 101 | 102 | def test_registered_instr_attrs() -> None: 103 | # Check if the Solo instruments are registered in a.Instrument 104 | instr_attr = a.Instrument 105 | assert "SOAR" in instr_attr._attr_registry[instr_attr].client 106 | assert "stix" in instr_attr._attr_registry[instr_attr].name 107 | 108 | 109 | def test_registered_soop_names() -> None: 110 | # Check if the soop names are registered in a.soar.SOOP 111 | soop_attr = str(a.soar.SOOP) 112 | assert "\nr_small_mres_mcad_ar_long_term" in soop_attr 113 | 114 | 115 | def test_search_soop() -> None: 116 | instrument = a.Instrument("EUI") 117 | time = a.Time("2022-04-01 01:00", "2022-04-01 02:00") 118 | soop_attr = a.soar.SOOP.r_small_mres_mcad_ar_long_term 119 | res = Fido.search(time, instrument, soop_attr) 120 | assert "SOOP Name" in res[0].columns 121 | assert res.file_num == 16 122 | 123 | # test non valid soop name passed 124 | res = Fido.search(time, instrument, a.soar.SOOP("hello")) 125 | assert res.file_num == 0 126 | 127 | 128 | def test_when_soar_provider_passed() -> None: 129 | # Tests when a.Provider.soar is passed that only SOARClient results are returned 130 | instrument = a.Instrument("EUI") 131 | time = a.Time("2022-04-01 00:00", "2022-04-01 01:00") 132 | provider = a.Provider.soar 133 | res = Fido.search(time & instrument & provider) 134 | assert len(res) == 1 135 | assert res["soar"] 136 | 137 | 138 | def test_when_sdac_provider_passed() -> None: 139 | # tests that only VSO EUI results are returned when explicitly setting the provider to SDAC 140 | instrument = a.Instrument("EUI") 141 | time = a.Time("2022-04-01 00:00", "2022-04-01 01:00") 142 | provider = a.Provider.sdac 143 | res = Fido.search(time & instrument & provider) 144 | assert len(res) == 1 145 | assert res["vso"] 146 | 147 | 148 | def test_when_wrong_provider_passed() -> None: 149 | # Tests that no results are returned when a provider is passed which does not provide EUI data. 150 | # This is different from the above test because the SDAC and the SOAR both provide EUI data while 151 | # NOAA has no overlap with the data provided by the SOAR. 152 | instrument = a.Instrument("EUI") 153 | time = a.Time("2022-04-01 00:00", "2022-04-01 01:00") 154 | provider = a.Provider.noaa 155 | res = Fido.search(time & instrument & provider) 156 | assert len(res) == 0 157 | 158 | 159 | def test_search_wavelength_detector_column() -> None: 160 | instrument = a.Instrument("EUI") 161 | time = a.Time("2021-02-01", "2021-02-02") 162 | level = a.Level(1) 163 | product = a.soar.Product("EUI-FSI174-IMAGE") 164 | res = Fido.search(instrument & time & level & product) 165 | assert "Wavelength" in res[0].columns 166 | assert "Detector" in res[0].columns 167 | 168 | 169 | def test_search_detector_instrument_dimension_2() -> None: 170 | # Instruments "EUI", "METIS", "PHI" and "SOLOHI" have two dimensions in the SOAR data. 171 | # Selecting no dimension index in the query results in two identical output rows. 172 | # To avoid repeating data, we have methods to take dimension index=1, which avoids any repetition. 173 | instrument = a.Instrument("EUI") 174 | time = a.Time("2020-03-03", "2020-03-04") 175 | level = a.Level(1) 176 | detector = a.Detector("HRI_EUV") 177 | res = Fido.search(instrument & time & level & detector) 178 | assert "Detector" in res[0].columns 179 | assert res.file_num == 266 180 | 181 | 182 | def test_search_detector_instrument_dimension_4() -> None: 183 | # The "SPICE" instrument has four dimensions in the SOAR data. As a result, 184 | # selecting no dimension index in the query results in four identical output rows. 185 | # To avoid repeating data, we have methods to take dimension index=1, which avoids any repetition. 186 | instrument = a.Instrument("SPICE") 187 | time = a.Time("2023-03-03 15:00", "2023-03-03 16:00") 188 | level = a.Level(1) 189 | detector = a.Detector("SW") 190 | res = Fido.search(instrument & time & level & detector) 191 | assert "Detector" in res[0].columns 192 | assert res.file_num == 11 193 | 194 | 195 | def test_invalid_detector() -> None: 196 | instrument = a.Instrument("SPICE") 197 | time = a.Time("2023-03-03 15:00", "2023-03-03 16:00") 198 | level = a.Level(1) 199 | detector = a.Detector("hello") 200 | res = Fido.search(instrument & time & level & detector) 201 | assert "Detector" in res[0].columns 202 | assert res.file_num == 0 203 | 204 | 205 | def test_wavelength_column_wavelength_exists() -> None: 206 | # For instruments EUI, METIS and SOLOHI "wavelength" column is available. 207 | # Test to check if the "Wavelength" column exists in the search results. 208 | instrument = a.Instrument("EUI") 209 | time = a.Time("2023-04-03 15:00", "2023-04-03 16:00") 210 | level = a.Level(1) 211 | wavelength = a.Wavelength(304 * u.AA) 212 | res = Fido.search(instrument & time & level & wavelength) 213 | assert "Wavelength" in res[0].columns 214 | assert res.file_num == 12 215 | 216 | 217 | def test_wavelength_single() -> None: 218 | # Test to check if the wavelength value is filtered for a single value provided. 219 | instrument = a.Instrument("EUI") 220 | time = a.Time("2023-04-03 15:00", "2023-04-03 16:00") 221 | level = a.Level(1) 222 | wavelength = a.Wavelength(304 * u.AA) 223 | res = Fido.search(instrument & time & level & wavelength) 224 | for table in res: 225 | assert all(table["Wavelength"] == 304) 226 | 227 | 228 | def test_wavelength_range() -> None: 229 | # Test to check if the wavelength value is filtered for wavemin and wavemax provided. 230 | instrument = a.Instrument("EUI") 231 | time = a.Time("2023-04-03 15:00", "2023-04-03 16:00") 232 | level = a.Level(1) 233 | wavelength = a.Wavelength(171 * u.AA, 185 * u.AA) 234 | res = Fido.search(instrument & time & level & wavelength) 235 | for table in res: 236 | assert all(table["Wavelength"] == 174) 237 | 238 | 239 | def test_join_science_query() -> None: 240 | result = SOARClient._construct_payload( 241 | [ 242 | "instrument='EUI'", 243 | "begin_time>='2021-02-01 00:00:00' AND begin_time<='2021-02-02 00:00:00'", 244 | "level='L1'", 245 | "descriptor='eui-fsi174-image'", 246 | ] 247 | ) 248 | 249 | assert result["QUERY"] == ( 250 | "SELECT h1.instrument, h1.descriptor, h1.level, h1.begin_time, h1.end_time, " 251 | "h1.data_item_id, h1.filesize, h1.filename, h1.soop_name, h2.detector, h2.wavelength, " 252 | "h2.dimension_index FROM v_sc_data_item AS h1 JOIN v_eui_sc_fits AS h2 USING (data_item_oid)" 253 | " WHERE h1.instrument='EUI' AND h1.begin_time>='2021-02-01 00:00:00' AND h1.begin_time<='2021-02-02 00:00:00'" 254 | " AND h2.dimension_index='1' AND h1.level='L1' AND h1.descriptor='eui-fsi174-image'" 255 | ) 256 | 257 | 258 | def test_join_low_latency_query() -> None: 259 | result = SOARClient._construct_payload( 260 | [ 261 | "instrument='EUI'", 262 | "begin_time>='2021-02-01 00:00:00' AND begin_time<='2021-02-02 00:00:00'", 263 | "level='LL01'", 264 | "descriptor='eui-fsi174-image'", 265 | ] 266 | ) 267 | 268 | assert result["QUERY"] == ( 269 | "SELECT h1.instrument, h1.descriptor, h1.level, h1.begin_time, h1.end_time, " 270 | "h1.data_item_id, h1.filesize, h1.filename, h1.soop_name, h2.detector, h2.wavelength, " 271 | "h2.dimension_index FROM v_ll_data_item AS h1 JOIN v_eui_ll_fits AS h2 USING (data_item_oid)" 272 | " WHERE h1.instrument='EUI' AND h1.begin_time>='2021-02-01 00:00:00' AND h1.begin_time<='2021-02-02 00:00:00'" 273 | " AND h2.dimension_index='1' AND h1.level='LL01' AND h1.descriptor='eui-fsi174-image'" 274 | ) 275 | 276 | 277 | def test_distance_query(): 278 | result = SOARClient._construct_payload( 279 | [ 280 | "instrument='RPW'", 281 | "DISTANCE(0.28,0.30)", 282 | "level='L2'", 283 | ] 284 | ) 285 | 286 | assert result["QUERY"] == ("SELECT * FROM v_sc_data_item WHERE instrument='RPW' AND level='L2'&DISTANCE(0.28,0.30)") 287 | 288 | 289 | def test_distance_join_query(): 290 | result = SOARClient._construct_payload( 291 | [ 292 | "instrument='EUI'", 293 | "DISTANCE(0.28,0.30)", 294 | "level='L2'", 295 | "descriptor='eui-fsi174-image'", 296 | ] 297 | ) 298 | 299 | assert result["QUERY"] == ( 300 | "SELECT h1.instrument, h1.descriptor, h1.level, h1.begin_time, h1.end_time, " 301 | "h1.data_item_id, h1.filesize, h1.filename, h1.soop_name, h2.detector, h2.wavelength, " 302 | "h2.dimension_index FROM v_sc_data_item AS h1 JOIN v_eui_sc_fits AS h2 USING (data_item_oid)" 303 | " WHERE h1.instrument='EUI' AND h1.level='L2' AND h1.descriptor='eui-fsi174-image'&DISTANCE(0.28,0.30)" 304 | ) 305 | 306 | 307 | def test_distance_search_remote_sensing(): 308 | instrument = a.Instrument("RPW") 309 | product = a.soar.Product("rpw-tnr-surv") 310 | level = a.Level(2) 311 | distance = a.soar.Distance(0.28 * u.AU, 0.30 * u.AU) 312 | res = Fido.search(distance & instrument & product & level) 313 | assert res.file_num == 21 314 | 315 | 316 | def test_distance_search_insitu(): 317 | instrument = a.Instrument("METIS") 318 | level = a.Level(2) 319 | product = a.soar.Product("metis-vl-pol-angle") 320 | distance = a.soar.Distance(0.45 * u.AU, 0.46 * u.AU) 321 | res = Fido.search(distance & instrument & product & level) 322 | assert res.file_num == 284 323 | 324 | 325 | def test_distance_time_search(): 326 | instrument = a.Instrument("EUI") 327 | time = a.Time("2023-04-27", "2023-04-28") 328 | level = a.Level(2) 329 | product = a.soar.Product("eui-fsi174-image") 330 | distance = a.soar.Distance(0.45 * u.AU, 0.46 * u.AU) 331 | res = Fido.search(instrument & product & level & time) 332 | assert res.file_num == 96 333 | # To check if we get different value when distance parameter is added in search. 334 | res = Fido.search(distance & instrument & product & level & time) 335 | assert res.file_num == 48 336 | 337 | 338 | def test_distance_out_of_bounds_warning(recwarn): 339 | instrument = a.Instrument("EUI") 340 | time = a.Time("2023-04-27", "2023-04-28") 341 | level = a.Level(2) 342 | product = a.soar.Product("eui-fsi174-image") 343 | distance = a.soar.Distance(0.45 * u.AU, 1.2 * u.AU) 344 | # Run the search and ensure it raises an HTTPError 345 | with pytest.raises(HTTPError): 346 | Fido.search(distance & instrument & product & level & time) 347 | # Check if the warning was raised 348 | warnings_list = recwarn.list 349 | assert any( 350 | warning.message.args[0] == "Distance values must be within the range 0.28 AU to 1.0 AU." 351 | and issubclass(warning.category, SunpyUserWarning) 352 | for warning in warnings_list 353 | ) 354 | 355 | 356 | @responses.activate 357 | def test_soar_server_down() -> None: 358 | # As the SOAR server is expected to be down in this test, a JSONDecodeError is expected 359 | # to be raised due to the absence of a valid JSON response. 360 | tap_endpoint = ( 361 | "http://soar.esac.esa.int/soar-sl-tap/tap/sync?REQUEST=doQuery&LANG=ADQL&FORMAT=json&QUERY=SELECT" 362 | " * FROM v_ll_data_item WHERE begin_time%3E='2020-11-13 00:00:00' AND " 363 | "begin_time%3C='2020-11-14 00:00:00' AND level='LL02' AND descriptor='mag'" 364 | ) 365 | # We do not give any json data similar to the condition when the server is down. 366 | responses.add(responses.GET, tap_endpoint, body="Invalid JSON response", status=200) 367 | 368 | time = a.Time("2020-11-13", "2020-11-14") 369 | level = a.Level("LL02") 370 | product = a.soar.Product("mag") 371 | 372 | with pytest.raises( 373 | RuntimeError, 374 | match=("The SOAR server returned an invalid JSON response. It may be down or not functioning correctly."), 375 | ): 376 | Fido.search(time, level, product) 377 | -------------------------------------------------------------------------------- /sunpy_soar/version.py: -------------------------------------------------------------------------------- 1 | # NOTE: First try _dev.scm_version if it exists and setuptools_scm is installed 2 | # This file is not included in muse wheels/tarballs, so otherwise it will 3 | # fall back on the generated _version module. 4 | try: 5 | try: 6 | from ._dev.scm_version import version 7 | except ImportError: 8 | from ._version import version 9 | except Exception: # NOQA: BLE001 10 | import warnings 11 | 12 | warnings.warn( 13 | f'could not determine {__name__.split(".")[0]} package version; this indicates a broken installation', 14 | stacklevel=3, 15 | ) 16 | del warnings 17 | version = "0.0.0" 18 | 19 | from packaging.version import parse as _parse 20 | 21 | _version = _parse(version) 22 | major, minor, bugfix = [*_version.release, 0][:3] 23 | release = not _version.is_devrelease 24 | -------------------------------------------------------------------------------- /tools/update_data.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib 3 | 4 | from astroquery.utils.tap.core import TapPlus 5 | 6 | soar = TapPlus(url="http://soar.esac.esa.int/soar-sl-tap/tap") 7 | 8 | 9 | def get_cdf_descriptors(): 10 | # Get data descriptors for CDF files 11 | print("Updating CDF descriptors...") 12 | job = soar.launch_job("select * from soar.cdf_dataset") 13 | res = job.get_results() 14 | descriptors = {} 15 | for row in res: 16 | desc = row["logical_source"].split("_")[-1] 17 | descriptors[desc] = row["logical_source_description"] 18 | return descriptors 19 | 20 | 21 | def get_fits_descriptors(): 22 | # Get data descriptors for FITS files 23 | print("Updating FITS descriptors...") 24 | soar = TapPlus(url="http://soar.esac.esa.int/soar-sl-tap/tap") 25 | job = soar.launch_job("select * from soar.fits_dataset") 26 | res = job.get_results() 27 | descriptors = {} 28 | for row in res: 29 | desc = row["logical_source"].split("_")[-1] 30 | # Currently no way to get a description from the FITS table 31 | descriptors[desc] = "" 32 | return descriptors 33 | 34 | 35 | def get_all_descriptors(): 36 | desc = get_cdf_descriptors() 37 | desc.update(get_fits_descriptors()) 38 | return desc 39 | 40 | 41 | def get_all_instruments(): 42 | # Get the unique instrument names 43 | SOAR = TapPlus(url="http://soar.esac.esa.int/soar-sl-tap/tap") 44 | job = SOAR.launch_job("select * from soar.instrument") 45 | res = job.get_results() 46 | 47 | instruments = [ 48 | "EPD", 49 | "EUI", 50 | "MAG", 51 | "METIS", 52 | "PHI", 53 | "RPW", 54 | "SOLOHI", 55 | "SPICE", 56 | "STIX", 57 | "SWA", 58 | ] 59 | instr_desc = {} 60 | for r in res: 61 | if r["name"] not in instruments: 62 | pass 63 | else: 64 | instr_desc[r["name"]] = r["long_name"] 65 | return instr_desc 66 | 67 | 68 | def get_all_soops(): 69 | # Get the unique soop names 70 | print("Updating SOOP descriptors...") 71 | SOAR = TapPlus(url="http://soar.esac.esa.int/soar-sl-tap/tap") 72 | job = SOAR.launch_job("select * from soar.soop") 73 | res = job.get_results() 74 | 75 | soop_names = {} 76 | for row in res: 77 | soop_names[row["soop_name"]] = "" 78 | 79 | return soop_names 80 | 81 | 82 | if __name__ == "__main__": 83 | attr_file = ( 84 | pathlib.Path(__file__).parent.parent / "sunpy_soar" / "data" / "attrs.json" 85 | ) 86 | descriptors = get_all_descriptors() 87 | with attr_file.open("w") as attrs_file: 88 | json.dump(dict(sorted(descriptors.items())), attrs_file, indent=2) 89 | 90 | instr_file = ( 91 | pathlib.Path(__file__).parent.parent 92 | / "sunpy_soar" 93 | / "data" 94 | / "instrument_attrs.json" 95 | ) 96 | instr_descriptors = get_all_instruments() 97 | with instr_file.open("w") as instrs_file: 98 | json.dump(dict(sorted(instr_descriptors.items())), instrs_file, indent=2) 99 | 100 | soop_file = ( 101 | pathlib.Path(__file__).parent.parent / "sunpy_soar" / "data" / "soop_attrs.json" 102 | ) 103 | soop_descriptors = get_all_soops() 104 | with soop_file.open("w") as soops_file: 105 | json.dump(dict(sorted(soop_descriptors.items())), soops_file, indent=2) 106 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | min_version = 4.0 3 | requires = 4 | tox-pypi-filter>=0.14 5 | envlist = 6 | py{310,311,312,313} 7 | py313-devdeps 8 | py310-oldestdeps 9 | codestyle 10 | build_docs 11 | 12 | [testenv] 13 | pypi_filter = https://raw.githubusercontent.com/sunpy/sunpy/main/.test_package_pins.txt 14 | # Run the tests in a temporary directory to make sure that we don't import 15 | # the package from the source tree 16 | change_dir = .tmp/{envname} 17 | description = 18 | run tests 19 | oldestdeps: with the oldest supported version of key dependencies 20 | devdeps: with the latest developer version of key dependencies 21 | pass_env = 22 | # A variable to tell tests we are on a CI system 23 | CI 24 | # Custom compiler locations (such as ccache) 25 | CC 26 | # Location of locales (needed by sphinx on some systems) 27 | LOCALE_ARCHIVE 28 | # If the user has set a LC override we should follow it 29 | LC_ALL 30 | set_env = 31 | MPLBACKEND = agg 32 | COLUMNS = 180 33 | devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple 34 | deps = 35 | # For packages which publish nightly wheels this will pull the latest nightly 36 | devdeps: astropy>=0.0.dev0 37 | # Packages without nightly wheels will be built from source like this 38 | devdeps: git+https://github.com/sunpy/sunpy 39 | devdeps: git+https://github.com/psf/requests 40 | oldestdeps: minimum_dependencies 41 | # old astropy isn't compatible with numpy 2, but numpy isn't a direct dep of sunpy-soar 42 | oldestdeps: numpy<2 43 | pytest-cov 44 | pytest-xdist 45 | # The following indicates which extras_require will be installed 46 | extras = 47 | tests 48 | commands_pre = 49 | oldestdeps: minimum_dependencies sunpy-soar --filename requirements-min.txt 50 | oldestdeps: pip install -r requirements-min.txt 51 | pip freeze --all --no-input 52 | commands = 53 | # To amend the pytest command for different factors you can add a line 54 | # which starts with a factor like `online: --remote-data=any \` 55 | # If you have no factors which require different commands this is all you need: 56 | pytest \ 57 | -vvv \ 58 | -r fEs \ 59 | --pyargs sunpy_soar \ 60 | --cov-report=xml \ 61 | --cov=sunpy_soar \ 62 | --cov-config={toxinidir}/.coveragerc \ 63 | {toxinidir}/docs \ 64 | {posargs} 65 | 66 | [testenv:codestyle] 67 | pypi_filter = 68 | skip_install = true 69 | description = Run all style and file checks with pre-commit 70 | deps = 71 | pre-commit 72 | commands = 73 | pre-commit install-hooks 74 | pre-commit run --color always --all-files --show-diff-on-failure 75 | 76 | [testenv:build_docs] 77 | description = invoke sphinx-build to build the HTML docs 78 | change_dir = 79 | docs 80 | extras = 81 | docs 82 | commands = 83 | pip freeze --all --no-input 84 | sphinx-build --color -W --keep-going -b html -d _build/.doctrees . _build/html {posargs} 85 | python -c 'import pathlib; print("Documentation available under file://\{0\}".format(pathlib.Path(r"{toxinidir}") / "docs" / "_build" / "index.html"))' 86 | --------------------------------------------------------------------------------