pax_global_header00006660000000000000000000000064151726601070014517gustar00rootroot0000000000000052 comment=c00df9641495f54e8e53a020e0a09175f1c26c89 CDSETool-0.3.1/000077500000000000000000000000001517266010700130745ustar00rootroot00000000000000CDSETool-0.3.1/.github/000077500000000000000000000000001517266010700144345ustar00rootroot00000000000000CDSETool-0.3.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001517266010700166175ustar00rootroot00000000000000CDSETool-0.3.1/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000012051517266010700212250ustar00rootroot00000000000000--- name: "Bug report \U0001F41E" about: Create a bug report labels: bug --- ## Describe the bug A clear and concise description of what the bug is. ### Steps to reproduce Code snippets and steps to reproduce the behavior. e.g. ```python from cdsetool.query import query_features query_features("InvalidCollection") ``` ### Expected behavior A clear and concise description of what you expected to happen. ### Environment - OS: [e.g. Arch Linux] - Package version: [e.g. cdsetool==X.Y.Z] - Python version: [e.g. 3.11.2] - Other details that you think may affect. ### Additional context Add any other context about the problem here. CDSETool-0.3.1/.github/ISSUE_TEMPLATE/feature-request.md000066400000000000000000000004431517266010700222630ustar00rootroot00000000000000--- name: "Feature request \U0001F680" about: Suggest an idea labels: enhancement --- ## Summary Brief explanation of the feature. ### Basic example Include a basic example or links here. ### Motivation Why are we doing this? What use cases does it support? What is the expected outcome?CDSETool-0.3.1/.github/dependabot.yml000066400000000000000000000010161517266010700172620ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" CDSETool-0.3.1/.github/workflows/000077500000000000000000000000001517266010700164715ustar00rootroot00000000000000CDSETool-0.3.1/.github/workflows/lint.yml000066400000000000000000000033301517266010700201610ustar00rootroot00000000000000name: Lint on: [push, pull_request] permissions: {} jobs: ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0 with: args: 'format --check --diff' - uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0 with: args: 'check' pylint: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install .[test] - name: Analysing the code with pylint run: | pylint $(git ls-files 'src/*.py') pyright: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e .[test] - uses: jakebailey/pyright-action@8ec14b5cfe41f26e5f41686a31eb6012758217ef # v3.0.2 CDSETool-0.3.1/.github/workflows/publish.yml000066400000000000000000000022261517266010700206640ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: release: types: [published] permissions: {} jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} CDSETool-0.3.1/.github/workflows/pytest.yml000066400000000000000000000016641517266010700205530ustar00rootroot00000000000000name: Pytest on: push: branches: [ "main" ] pull_request: branches: [ "main" ] permissions: {} jobs: pytest: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e .[test] - name: Test with pytest run: | pytest --cov=cdsetool --cov-report xml # - name: Python Coverage # uses: orgoro/coverage@3f13a558c5af7376496aa4848bf0224aead366ac # v3.2 # with: # coverageFile: coverage.xml # token: ${{ secrets.GITHUB_TOKEN }} CDSETool-0.3.1/.github/workflows/scan.yml000066400000000000000000000020461517266010700201420ustar00rootroot00000000000000name: Scan on: schedule: - cron: '0 0 * * *' workflow_dispatch: permissions: {} # When a PR is updated, cancel the jobs from the previous version. Merges # do not define head_ref, so use run_id to never cancel those jobs. concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: cve-scanner: runs-on: ubuntu-latest permissions: security-events: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run vulnerability scanner uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # 0.36.0 with: scan-type: 'fs' ignore-unfixed: true format: 'sarif' output: 'trivy-results.sarif' severity: 'MEDIUM,HIGH,CRITICAL' - name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: sarif_file: 'trivy-results.sarif' CDSETool-0.3.1/.gitignore000066400000000000000000000060661517266010700150740ustar00rootroot00000000000000.DS_Store .coverage environment main.py # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ .vscode/ CDSETool-0.3.1/LICENSE000066400000000000000000000020731517266010700141030ustar00rootroot00000000000000MIT License Copyright (c) 2023-2025 CDSETool Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. CDSETool-0.3.1/README.md000066400000000000000000000302341517266010700143550ustar00rootroot00000000000000# CDSETool ## About CDSETool This script downloads copernicus data from the Copernicus Data Space Ecosystem ## Quick start ```python from cdsetool.query import query_features, shape_to_wkt from cdsetool.credentials import Credentials from cdsetool.download import download_features from cdsetool.monitor import StatusMonitor from datetime import date features = query_features( "SENTINEL-1", { "contentDateStartGe": "2020-12-20", "contentDateStartLe": date(2020, 12, 25), "processingLevel": "LEVEL1", "productType": "IW_GRDH_1S", "geometry": shape_to_wkt("path/to/shapefile.shp"), }, ) list( download_features( features, "path/to/output/folder/", { "concurrency": 4, "monitor": StatusMonitor(), "credentials": Credentials("username", "password"), }, ) ) ``` Or use the CLI: ```bash cdsetool query search SENTINEL-2 --search-term contentDateStartGe=2020-01-01 --search-term contentDateStartLe=2020-01-10 --search-term productType=S2MSI2A cdsetool download SENTINEL-2 PATH/TO/DIR --concurrency 4 --search-term contentDateStartGe=2020-01-01 --search-term contentDateStartLe=2020-01-10 --search-term productType=S2MSI2A ``` ## Table of Contents - [CDSETool](#cdsetool) * [About CDSETool](#about-cdsetool) * [Quick Start](#quick-start) * [Table of Contents](#table-of-contents) * [Installation](#installation) * [Usage](#usage) + [Querying features](#querying-features) - [Querying by shapes](#querying-by-shapes) - [Querying by lists of parameters](#querying-by-lists-of-parameters) - [Querying by dates](#querying-by-dates) - [Listing search terms](#listing-search-terms) + [Downloading features](#downloading-features) - [Authenticating](#authenticating) - [Concurrently downloading features](#concurrently-downloading-features) - [Sequentially downloading features](#sequentially-downloading-features) * [Roadmap](#roadmap) * [Contributing](#contributing) * [LICENSE](#license) ## Installation Install `cdsetool` using pip: ```bash pip install cdsetool==0.3.1 ``` ## Usage ### Querying features Querying is always done in batches, returning `len(results) <= top` records each time. A local buffer is filled and gradually emptied as results are yielded. When the buffer is empty, more results will be requested and the process repeated until no more results are available, or the iterator is discarded. **Important**: The API has a pagination limit of 10,000 results per query. If your query returns more results, you'll need to narrow your search criteria (e.g., use smaller date ranges). Since downloading features is the most common use-case, `query_features` assumes that the query will run till the end. Because of this, the batch size is set to `1000`, which is the size limit set by CDSE. ```python from cdsetool.query import query_features collection = "SENTINEL-2" search_terms = { "top": 100, # batch size, between 1 and 1000 (default: 1000) "contentDateStartGe": "2024-01-01", "productType": "S2MSI1C" } # wait for a single batch to finish, yield results immediately for feature in query_features(collection, search_terms): # do something with feature # wait for all batch requests to complete, returning list features = list(query_features(collection, search_terms)) # manually iterate iterator = query_features(collection, search_terms) featureA = next(iterator) featureB = next(iterator) # ... ``` #### Query Options **Product Format** Query results are returned directly from the Copernicus API. Each product has the following structure: ```python product = { "Id": "uuid-string", "Name": "S2A_MSIL2A_20240110T105421_...", "Collection": "SENTINEL-2", "ContentDate": {"Start": "2024-01-10T10:54:21Z", "End": "..."}, "Online": True, "ContentLength": 1043654649, "GeoFootprint": {"type": "Polygon", "coordinates": [...]}, "Attributes": [ {"Name": "productType", "Value": "S2MSI2A"}, {"Name": "cloudCover", "Value": 5.2}, ... ] } ``` **Expand Product Attributes** By default, query results do not include product attributes (productType, cloudCover, platform, instrument, etc.). To include product attributes, you need to request this from the server using the option `expand_attributes` and can then access the attributes using the function `get_product_attribute()`: ```python from cdsetool.query import query_features, get_product_attribute features = query_features( "SENTINEL-2", {"contentDateStartGe": "2024-01-01"}, options={"expand_attributes": True} ) feature = features[0] # Access basic properties directly print(feature["Name"]) # Product name print(feature["Id"]) # Product UUID # Access attributes using helper function cloud_cover = get_product_attribute(feature, "cloudCover") product_type = get_product_attribute(feature, "productType") ``` #### Querying by geometry To query by shapes, you must first convert your shape to Well Known Text (WKT). The included `shape_to_wkt` can solve this. ```python from cdsetool.query import query_features, shape_to_wkt geometry = shape_to_wkt("path/to/shape.shp") features = query_features("SENTINEL-3", {"geometry": geometry}) ``` #### Querying by lists of parameters Most search terms only accept a single argument. To query by a list of arguments, loop the arguments and pass them one by one to the query function. ```python from cdsetool.query import query_features tile_ids = ["32TPT", "32UPU", "32UPU", "31RFL", "37XDA"] for tile_id in tile_ids: features = query_features("SENTINEL-2", {"tileId": tile_id}) for feature in features: # do things with feature ``` #### Querying by dates Its quite common to query for features created before, after or between dates. Search terms support comparison operator suffixes: | Suffix | Meaning | Example | |--------|----------------------------|----------------------| | `Eq` | equals (=) | `contentDateStartEq` | | `Gt` | greater than (>) | `contentDateStartGt` | | `Ge` | greater than or equal (>=) | `contentDateStartGe` | | `Lt` | less than (<) | `contentDateStartLt` | | `Le` | less than or equal (<=) | `contentDateStartLe` | `Eq` can be applied on any field but the other suffixes can only be applied to numeric and date fields. Interval syntax is only allowed on the base name, not on suffixed variants, and only on numeric and date fields. | Interval notation | Suffixes to combine | Meaning | |-------------------|---------------------|--------------------------------| | [a, b] | `Ge` + `Le` | a <= value <= b (closed) | | (a, b) | `Gt` + `Lt` | a < value < b (open) | | [a, b) | `Ge` + `Lt` | a <= value < b (half-open) | | (a, b] | `Gt` + `Le` | a < value <= b (half-open) | ```python from cdsetool.query import query_features from datetime import date, datetime date_from = date(2020, 1, 1) # or datetime(2020, 1, 1, 23, 59, 59, 123456) or "2020-01-01" or "2020-01-01T23:59:59.123456Z" date_to = date(2020, 12, 31) features = query_features("SENTINEL-2", {"contentDateStartGe": date_from, "contentDateStartLe": date_to, "cloudCover": "[0, 30]"}) ``` #### Listing search terms To get a list of all search terms for a given collection, you may either use the `describe_collection` function or use the CLI: ```python from cdsetool.query import describe_collection search_terms = describe_collection("SENTINEL-2").keys() print(search_terms) ``` And the CLI: ```bash $ cdsetool query search-terms SENTINEL-2 ``` ### Downloading features #### Authenticating An account is required to download features from the Copernicus distribution service. To authenticate using an account, instantiate `Credentials` and pass your username and password ```python from cdsetool.credentials import Credentials username = "konata@izumi.com" password = "password123" credentials = Credentials(username, password) ``` Alternatively, `Credentials` can pull from `~/.netrc` when username and password are left blank. ```python # ~/.netrc machine https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token login konata@izumi.com password password123 # main.py from cdsetool.credentials import Credentials credentials = Credentials() ``` The credentials object may then be passed to a download function. If left out, the download functions will default to using `.netrc`. ```python credentials = Credentials() download_features(features, "/some/download/path", {"credentials": credentials}) ``` Credentials can be validated using the `validate_credentials` function which will return a boolean. ```python from cdsetool.credentials import validate_credentials validate_credentials(username='user', password='password') ``` If None are passed to username and password, `validate_credentials` will validate `.netrc` #### Concurrently downloading features CDSETool provides a method for concurrently downloading features. The concurrency level should match your accounts privileges. See [CDSE quotas](https://documentation.dataspace.copernicus.eu/Quotas.html) The downloaded feature ids are yielded, so its required to await the results. ```python from cdsetool.query import query_features from cdsetool.download import download_features features = query_features("SENTINEL-2", {"contentDateStartGe": "2024-01-01", "contentDateStartLe": "2024-01-10"}) download_path = "/path/to/download/folder" downloads = download_features(features, download_path, {"concurrency": 4}) for id in downloads: print(f"feature {id} downloaded") # or list(downloads) ``` #### Sequentially downloading features Its possible to download features sequentially in a single thread if desired. ```python from cdsetool.query import query_features from cdsetool.download import download_feature features = query_features("SENTINEL-2", {"contentDateStartGe": "2024-01-01", "contentDateStartLe": "2024-01-10"}) download_path = "/path/to/download/folder" for feature in features: download_feature(feature, download_path) ``` #### Download specific files within features It's possible to download specific files within products bundles using Unix filename pattern matching. It can be used in CDSETool: - Through the `filter_pattern` option of `download_features` and `download_feature`: ```python from cdsetool.query import query_features from cdsetool.download import download_features features = query_features("SENTINEL-2", {"contentDateStartGe": "2024-01-01", "contentDateStartLe": "2024-01-10"}) download_path = "/path/to/download/folder" filter_pattern = "*TCI.jp2" downloads = download_features(features, download_path, {"filter_pattern": filter_pattern}) for id in downloads: print(f"feature {id} downloaded") ``` - Or through the CLI: ``` cdsetool download SENTINEL-2 PATH/TO/DIR --filter-pattern *TCI.jp2 --concurrency 4 --search-term contentDateStartGe=2024-01-01 --search-term contentDateStartLe=2024-01-10 --search-term productType=S2MSI2A ``` ## Roadmap - [X] Query schema validation - [ ] High-level API - [ ] Query features - [ ] Download features - [ ] Download single feature - [ ] Download list of features - [ ] Download by ID - [ ] Download by URL - [ ] Command-Line Interface - [ ] Update to match the high-level API - [ ] Better `--help` messages - [ ] Quickstart guide in README.md - [ ] Test suite - [ ] Query - [ ] Credentials - [ ] Download - [ ] Monitor - [ ] Strategy for handling HTTP and connection errors ## Contributing Any contributions you make are **greatly appreciated**. If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 1. Fork the Project 2. Create your Feature Branch (`git checkout -b feature/cool-new-feature`) 3. Commit your Changes (`git commit -m 'Add some feature'`) 4. Push to the Branch (`git push origin feature/cool-new-feature`) 5. Open a Pull Request ## LICENSE Distributed under the MIT License. See [LICENSE](LICENSE) for more information. CDSETool-0.3.1/pyproject.toml000066400000000000000000000032151517266010700160110ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "cdsetool" version = "0.3.1" authors = [ { name="Jacob Vejby", email="javej@sdfi.dk" }, ] description = "Tools & CLI for interacting with CDSE product APIs" readme = "README.md" requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] dependencies = [ "typer >= 0.9,< 1", "rich >= 13.6,< 16", "requests >= 2.28.1,< 3", "pyjwt[crypto] >= 2.8,< 2.13", "geopandas >= 0.13.2", ] [dependency-groups] dev = [ "ruff==0.15.11", ] # (Jan 30 2025) Pip does not support dependency-groups yet, so # leave the optional dependencies for everything except Ruff. [project.optional-dependencies] test = [ "pylint==3.3.9", "pytest==8.4.2", "pytest-cov==7.1.0", "requests-mock==1.12.1", "pytest-mock==3.15.1", ] [project.urls] "Homepage" = "https://github.com/CDSETool/CDSETool" "Bug Tracker" = "https://github.com/CDSETool/CDSETool/issues" [project.scripts] cdsetool = "cdsetool:cli.main" [tool.pylint.design] max-locals = 20 [tool.pylint.format] max-line-length = "88" disable="fixme" [tool.pylint.imports] disable="wrong-import-order" [tool.pyright] exclude = ["**/__pycache__", ".venv"] # Can be strict too, see more fine-grained settings at: # https://github.com/microsoft/pyright/blob/main/docs/configuration.md#diagnostic-settings-defaults # reportMissingParameterType = true typeCheckingMode = "standard" executionEnvironments = [ { root = "./" } ] [tool.pytest.ini_options] addopts = [ "--import-mode=importlib", ] [tool.ruff.lint] select = [ "I", # isort ] CDSETool-0.3.1/src/000077500000000000000000000000001517266010700136635ustar00rootroot00000000000000CDSETool-0.3.1/src/cdsetool/000077500000000000000000000000001517266010700154775ustar00rootroot00000000000000CDSETool-0.3.1/src/cdsetool/__init__.py000066400000000000000000000000001517266010700175760ustar00rootroot00000000000000CDSETool-0.3.1/src/cdsetool/_attributes.py000066400000000000000000000403321517266010700204000ustar00rootroot00000000000000"""Attribute definitions for CDSE OData product queries.""" from typing import Dict, List, TypedDict class AttributeInfo(TypedDict, total=False): """Lookup entry for a product attribute's type and supported collections.""" Type: str Title: str Collections: List[str] ATTRIBUTES: Dict[str, AttributeInfo] = { "USGScollection": { "Type": "String", "Collections": [ "LANDSAT-8", "LANDSAT-9", ], }, "acquisitionType": { "Type": "String", "Collections": [ "SENTINEL-5P", ], }, "authority": { "Type": "String", "Collections": [ "SENTINEL-6", "SENTINEL-1-RTC", "ENVISAT", "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "baselineCollection": { "Type": "String", "Collections": [ "SENTINEL-3", "SENTINEL-5P", ], }, "brightCover": { "Type": "Double", "Collections": [ "SENTINEL-3", ], }, "card4lSpecification": { "Type": "String", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "card4lSpecificationVersion": { "Type": "String", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "closedSeaCover": { "Type": "Integer", "Collections": [ "SENTINEL-3", ], }, "cloudCover": { "Type": "Double", "Title": "Cloud cover percentage (0-100)", "Collections": [ "SENTINEL-2", "SENTINEL-3", "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "cloudCoverLand": { "Type": "Double", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "coastalCover": { "Type": "Double", "Collections": [ "SENTINEL-3", ], }, "collectionCategory": { "Type": "String", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "collectionName": { "Type": "String", "Collections": [ "LANDSAT-5", "LANDSAT-7", ], }, "collectionNumber": { "Type": "String", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "completionTimeFromAscendingNode": { "Type": "Double", "Collections": [ "SENTINEL-1", ], }, "continentalIceCover": { "Type": "Integer", "Collections": [ "SENTINEL-3", ], }, "coordinates": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-5P", ], }, "cycleNumber": { "Type": "Integer", "Collections": [ "SENTINEL-1", "SENTINEL-3", "ENVISAT", ], }, "datastripId": { "Type": "String", "Collections": [ "SENTINEL-2", ], }, "datatakeID": { "Type": "Integer", "Collections": [ "SENTINEL-1", ], }, "doi": { "Type": "String", "Collections": [ "SENTINEL-5P", ], }, "freshInlandWaterCover": { "Type": "Double", "Collections": [ "SENTINEL-3", ], }, "geometricRmse": { "Type": "Double", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "geometricXBias": { "Type": "Double", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "geometricXStddev": { "Type": "Double", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "geometricYBias": { "Type": "Double", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "geometricYStddev": { "Type": "Double", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "granuleIdentifier": { "Type": "String", "Collections": [ "SENTINEL-2", ], }, "identifier": { "Type": "String", "Collections": [ "SENTINEL-5P", ], }, "illuminationZenithAngle": { "Type": "Double", "Collections": [ "SENTINEL-2", ], }, "instrumentConfigurationID": { "Type": "Integer", "Collections": [ "SENTINEL-1", ], }, "instrumentShortName": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-5P", "SENTINEL-6", "SENTINEL-1-RTC", "ENVISAT", "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "landCover": { "Type": "Double", "Collections": [ "SENTINEL-3", ], }, "lastOrbitDirection": { "Type": "String", "Collections": [ "SENTINEL-3", ], }, "lastOrbitNumber": { "Type": "Integer", "Collections": [ "SENTINEL-2", "SENTINEL-3", ], }, "lastRelativeOrbitNumber": { "Type": "Integer", "Collections": [ "SENTINEL-3", ], }, "numberOfBands": { "Type": "Integer", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "offNadir": { "Type": "Double", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "openOceanCover": { "Type": "Integer", "Collections": [ "SENTINEL-3", ], }, "operationalMode": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-6", "SENTINEL-1-RTC", "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "orbitDirection": { "Type": "String", "Title": "Orbit direction (ASCENDING or DESCENDING)", "Collections": [ "SENTINEL-1", "SENTINEL-3", "SENTINEL-1-RTC", ], }, "orbitNumber": { "Type": "Integer", "Title": "Absolute orbit number", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-5P", "SENTINEL-6", "SENTINEL-1-RTC", "ENVISAT", "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "origin": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-6", ], }, "parentIdentifier": { "Type": "String", "Collections": [ "SENTINEL-5P", ], }, "pathNumber": { "Type": "Integer", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "phaseNumber": { "Type": "Integer", "Collections": [ "ENVISAT", ], }, "platformSerialIdentifier": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-5P", "SENTINEL-6", "SENTINEL-1-RTC", ], }, "platformShortName": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-5P", "SENTINEL-6", "SENTINEL-1-RTC", "ENVISAT", "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "polarisationChannels": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-1-RTC", ], }, "processingBaseline": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-5P", "SENTINEL-6", ], }, "processingCenter": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-5P", "SENTINEL-6", ], }, "processingDate": { "Type": "DateTimeOffset", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-5P", "SENTINEL-6", ], }, "processingLevel": { "Type": "String", "Title": "Processing level", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-5P", "SENTINEL-6", "SENTINEL-1-RTC", "ENVISAT", "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "processingMode": { "Type": "String", "Collections": [ "SENTINEL-5P", ], }, "processorName": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-3", "SENTINEL-5P", ], }, "processorVersion": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-5P", "SENTINEL-6", ], }, "productClass": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-5P", ], }, "productComposition": { "Type": "String", "Collections": [ "SENTINEL-1", ], }, "productConsolidation": { "Type": "String", "Collections": [ "SENTINEL-1", ], }, "productGeneration": { "Type": "DateTimeOffset", "Collections": [ "SENTINEL-1", ], }, "productGroupId": { "Type": "String", "Collections": [ "SENTINEL-2", ], }, "productType": { "Type": "String", "Title": "Product type", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-5P", "SENTINEL-6", "SENTINEL-1-RTC", "ENVISAT", "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "proj:epsg": { "Type": "Integer", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "projShape": { "Type": "String", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "projTransform": { "Type": "String", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "qualityInfo": { "Type": "Integer", "Collections": [ "SENTINEL-2", ], }, "qualityStatus": { "Type": "String", "Collections": [ "SENTINEL-2", "SENTINEL-5P", ], }, "relativeOrbitNumber": { "Type": "Integer", "Title": "Relative orbit number", "Collections": [ "SENTINEL-1", "SENTINEL-2", "SENTINEL-3", "SENTINEL-6", "SENTINEL-1-RTC", ], }, "rowNumber": { "Type": "Integer", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "salineWaterCover": { "Type": "Double", "Collections": [ "SENTINEL-3", ], }, "sceneId": { "Type": "String", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "segmentStartTime": { "Type": "DateTimeOffset", "Collections": [ "SENTINEL-1", ], }, "sliceNumber": { "Type": "Integer", "Collections": [ "SENTINEL-1", ], }, "sliceProductFlag": { "Type": "Boolean", "Collections": [ "SENTINEL-1", ], }, "snowOrIceCover": { "Type": "Double", "Collections": [ "SENTINEL-3", ], }, "source": { "Type": "String", "Collections": [ "SENTINEL-6", ], }, "sourceProduct": { "Type": "String", "Collections": [ "SENTINEL-2", ], }, "sourceProductOriginDate": { "Type": "String", "Collections": [ "SENTINEL-2", ], }, "spatialResolution": { "Type": "Integer", "Collections": [ "SENTINEL-6", "SENTINEL-1-RTC", "ENVISAT", "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "startTimeFromAscendingNode": { "Type": "Double", "Collections": [ "SENTINEL-1", ], }, "sunAzimuthAngle": { "Type": "Double", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "sunElevationAngle": { "Type": "Double", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "swathIdentifier": { "Type": "String", "Collections": [ "SENTINEL-1", ], }, "tidalRegionCover": { "Type": "Double", "Collections": [ "SENTINEL-3", ], }, "tileId": { "Type": "String", "Collections": [ "SENTINEL-2", ], }, "timeliness": { "Type": "String", "Collections": [ "SENTINEL-1", "SENTINEL-3", "SENTINEL-6", ], }, "totalSlices": { "Type": "Integer", "Collections": [ "SENTINEL-1", ], }, "view:sun_azimuth": { "Type": "Double", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "view:sun_elevation": { "Type": "Double", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "wrsPath": { "Type": "String", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "wrsRow": { "Type": "String", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, "wrsType": { "Type": "String", "Collections": [ "LANDSAT-5", "LANDSAT-7", "LANDSAT-8", "LANDSAT-9", ], }, } CDSETool-0.3.1/src/cdsetool/_processing.py000066400000000000000000000042521517266010700203670ustar00rootroot00000000000000""" This module provides functions for processing data concurrently """ from concurrent.futures import FIRST_COMPLETED, Future, ThreadPoolExecutor, wait from typing import Callable, Generator, Iterable, List, Union from cdsetool.query import FeatureQuery def _concurrent_process( fun: Callable[[FeatureQuery], Union[str, None]], iterable: Iterable, workers: int = 4, ) -> Generator[Union[str, None], None, None]: """ Process items in an iterable concurrently Items are taken from the iterable as soon as a worker becomes available Returns an iterable of the results """ # Futures are submitted in batches instead of all at once to avoid # requesting too many items from the iterable at once, which is important if # the iterable is a generator that is producing items on the fly. # # The 1.5 factor is a small overhead to keep jobs > workers at all times, instead # of jobs == workers, which could cause the workers to be idle while waiting for # the iterable to produce more items. low_water_mark = int(workers * 1.5) iterator = iter(iterable) with ThreadPoolExecutor(max_workers=workers) as executor: futures: List[Future[Union[str, None]]] = [] # pylint: disable=E1136 # Pluck an item from the iterable and submit it to the executor. # If the iterable is exhausted, this function is a no-op. def submit_item() -> None: item = next(iterator, None) if item is not None: futures.append(executor.submit(fun, item)) # Fill the futures list up to the low water mark def fill_futures() -> None: for _ in range(low_water_mark - len(futures)): submit_item() # Submit the first batch of items fill_futures() # Continue until no more futures are queued while futures: # Wait for the first future(s) to complete done, not_done = wait(futures, return_when=FIRST_COMPLETED) futures = list(not_done) for future in done: yield future.result() # Submit items to replace the ones that are done. fill_futures() CDSETool-0.3.1/src/cdsetool/cli.py000066400000000000000000000114031517266010700166170ustar00rootroot00000000000000""" Command line interface """ import json as JSON import os import sys from typing import Annotated, Dict, List, Optional import typer from cdsetool.download import download_features from cdsetool.monitor import StatusMonitor from cdsetool.query import ( SearchTermValue, describe_collection, describe_search_terms, query_features, ) app = typer.Typer(no_args_is_help=True) query_app = typer.Typer(no_args_is_help=True) app.add_typer(query_app, name="query") def _format_attributes(attributes: Dict[str, Dict[str, str]]) -> str: """Format attribute details into a readable string.""" lines = [] for key, attr in attributes.items(): lines.append(f" - {key}") if title := attr.get("title"): lines.append(f" Description: {title}") if attribute_type := attr.get("type"): lines.append(f" Type: {attribute_type}") if example := attr.get("example"): lines.append(f" Example: {example}") return "\n".join(lines) @query_app.command("search-terms") def query_search_terms( collection: Annotated[ Optional[str], typer.Argument( help="Collection name (e.g., SENTINEL-1, SENTINEL-2). " "If omitted, shows only builtin parameters without querying the server." ), ] = None, ) -> None: """ List the available search terms for a collection """ if collection is None: # No collection specified - show only builtin params (no API call) print("Builtin search terms (use with --search-term):") print() print(_format_attributes(describe_search_terms())) print() print("Specify a collection name to see collection-specific attributes.") else: # Collection specified - fetch from API and show all supported params print(f"Search terms for collection {collection}:") print() if search_terms := describe_collection(collection): print(_format_attributes(search_terms)) else: print(" (none)") # TODO: implement limit @query_app.command("search") def query_search( collection: str, search_term: Annotated[ Optional[List[str]], typer.Option( help="Search by term=value pairs. " + "Pass multiple times for multiple search terms" ), ] = None, json: Annotated[bool, typer.Option(help="Output JSON")] = False, ) -> None: """ Search for features matching the search terms """ search_term = search_term or [] features = query_features( collection, _to_dict(search_term), options={"max_attempts": 1} ) for feature in features: if json: print(JSON.dumps(feature)) else: print(feature.get("Name")) # TODO: implement limit @app.command("download") def download( # pylint: disable=[too-many-arguments, too-many-positional-arguments] collection: str, path: str, concurrency: Annotated[ int, typer.Option(help="Number of concurrent connections") ] = 1, overwrite_existing: Annotated[ bool, typer.Option(help="Overwrite already downloaded files") ] = False, search_term: Annotated[ Optional[List[str]], typer.Option( help="Search by term=value pairs. " + "Pass multiple times for multiple search terms" ), ] = None, filter_pattern: Annotated[ Optional[str], typer.Option( help=( "Download specific files within product bundles using OData API's node" " filtering functionality" ) ), ] = None, ) -> None: """ Download all features matching the search terms """ if not os.path.exists(path): print(f"Path {path} does not exist") sys.exit(1) search_term = search_term or [] features = query_features( collection, _to_dict(search_term), options={"max_attempts": 1} ) results = list( download_features( features, path, { "monitor": StatusMonitor(), "concurrency": concurrency, "overwrite_existing": overwrite_existing, "filter_pattern": filter_pattern, "download_attempts": 1, }, ) ) downloaded = sum(1 for item in results if item is not None) print(f"Successfully downloaded {downloaded}/{len(results)} feature(s)") def main(): """ Main entry point """ app() def _to_dict(term_list: List[str]) -> Dict[str, SearchTermValue]: search_terms = {} for item in term_list: key, value = item.split("=", 1) # Split on first = only search_terms[key] = value return search_terms if __name__ == "__main__": main() CDSETool-0.3.1/src/cdsetool/credentials.py000066400000000000000000000215311517266010700203500ustar00rootroot00000000000000""" This module provides a class for handling credentials for the Copernicus Identity and Access Management (IAM) system. """ import netrc import threading from datetime import datetime, timedelta from typing import Any, Dict, List, Union import jwt import requests from requests.adapters import HTTPAdapter from urllib3.util import Retry class NoCredentialsException(Exception): """ Raised when no credentials are found """ class InvalidCredentialsException(Exception): """ Raised when credentials are invalid """ class DeprecatedNoTokenException(Exception): """ Deprecated """ def NoTokenException(*args: object, **kwargs: object): # pylint: disable=invalid-name """ Raised when no token is available """ from warnings import warn # pylint: disable=import-outside-toplevel error_msg = [ "Warning! NoTokenException is deprecated, and will be removed in" "the next major release." ] warn(" ".join(error_msg)) return DeprecatedNoTokenException(*args, **kwargs) class TokenExchangeException(Exception): """ Raised when token exchange fails """ class TokenClientConnectionError(Exception): """ Raised when token connection fails. """ class TokenExpiredSignatureError(Exception): """ Raised when token signature has expired. """ class Credentials: # pylint: disable=too-few-public-methods disable=too-many-instance-attributes """ A class for handling credentials for the Copernicus Identity and Access Management (IAM) system """ RETRY_CODES = frozenset([413, 429, 500, 502, 503]) RETRIES = Retry( total=25, backoff_factor=0.5, raise_on_status=False, status_forcelist=RETRY_CODES, ) def __init__( self, username: Union[str, None] = None, password: Union[str, None] = None, openid_configuration_endpoint: Union[str, None] = None, proxies: Union[Dict[str, str], None] = None, ) -> None: self.__username: Union[str, None] = username self.__password: Union[str, None] = password self.__proxies: Union[Dict[str, str], None] = proxies self.__openid_conf = None self.__jwks = None self.__openid_configuration_endpoint: str = ( openid_configuration_endpoint or "https://identity.dataspace.copernicus.eu" + "/auth/realms/CDSE/.well-known/openid-configuration" ) self.__access_token: Union[str, None] = None self.__refresh_token: Union[str, None] = None self.__access_token_expires: datetime = datetime.now() - timedelta(hours=8) self.__refresh_token_expires: datetime = self.__access_token_expires self.__lock = threading.Lock() if self.__username is None or self.__password is None: self.__read_credentials() self.__ensure_tokens() def get_session(self) -> requests.Session: """ Returns a session with the credentials set as the Authorization header """ return self.make_session(self, True, self.RETRIES, self.__proxies) @staticmethod def make_session( caller, authorization: bool, max_retries: Retry, proxies: Union[Dict[str, str], None], ) -> requests.Session: """ Creates a new session. Authorization is only available from callers that are subclasses of Credentials. """ if authorization: caller.__ensure_tokens() # pylint: disable=protected-access session = requests.Session() session.mount("http://", HTTPAdapter(max_retries=max_retries)) session.mount("https://", HTTPAdapter(max_retries=max_retries)) if proxies is not None: session.proxies.update(proxies) if authorization: token = caller.__access_token # pylint: disable=protected-access session.headers.update({"Authorization": f"Bearer {token}"}) return session def __token_exchange(self, data: Dict[str, str]) -> timedelta: # Make a session that will retry post, respecting the retry-after # header when we get a 503 and a few other temporary failures. session = self.make_session( caller=self, authorization=False, max_retries=Retry( total=15, backoff_factor=0.5, allowed_methods=None, raise_on_status=False, status_forcelist=self.RETRY_CODES, ), proxies=self.__proxies, ) response = session.post(self.__token_endpoint, data=data, timeout=120) if response.status_code == 401: raise InvalidCredentialsException( "Unable to exchange token with " + f"username: {self.__username} and " + f"password: {len(self.__password or '') * '*'}" ) if response.status_code != 200: raise TokenExchangeException(f"Token exchange failed: {response.text}") response = response.json() self.__access_token = response["access_token"] self.__refresh_token = response["refresh_token"] return timedelta(seconds=response["refresh_expires_in"]) def __ensure_tokens(self) -> None: with self.__lock: refresh_expire_delta = None if self.__access_token_expires < datetime.now(): if self.__refresh_token_expires < datetime.now(): data = { "grant_type": "password", "username": self.__username, "password": self.__password, "client_id": "cdse-public", } else: data = { "grant_type": "refresh_token", "refresh_token": self.__refresh_token, "client_id": "cdse-public", } refresh_expire_delta = self.__token_exchange(data) if not self.__access_token: raise InvalidCredentialsException( "Internal error: access token not available" ) try: key = self.__jwk_client.get_signing_key_from_jwt(self.__access_token) except jwt.PyJWKClientConnectionError as e: raise TokenClientConnectionError from e try: data = jwt.decode( self.__access_token, key=key.key, algorithms=self.__id_token_signing_algos, # pylint: disable=protected-access options={"verify_aud": False}, ) except jwt.ExpiredSignatureError as e: raise TokenExpiredSignatureError from e if refresh_expire_delta is not None: self.__access_token_expires = datetime.fromtimestamp(data["exp"]) self.__refresh_token_expires = ( datetime.fromtimestamp(data["iat"]) + refresh_expire_delta ) def __read_credentials(self) -> None: rv = netrc.netrc().authenticators(self.__token_endpoint) if isinstance(rv, tuple): self.__username, _, self.__password = rv else: raise NoCredentialsException("No credentials found") @property def __openid_configuration(self) -> Dict[str, Any]: if self.__openid_conf: return self.__openid_conf session = self.make_session( caller=self, authorization=False, max_retries=self.RETRIES, proxies=self.__proxies, ) response = session.get(self.__openid_configuration_endpoint, timeout=120) response.raise_for_status() self.__openid_conf = response.json() return self.__openid_conf @property def __token_endpoint(self) -> str: return self.__openid_configuration["token_endpoint"] @property def __jwks_uri(self) -> str: return self.__openid_configuration["jwks_uri"] @property def __id_token_signing_algos(self) -> List[str]: return self.__openid_configuration["id_token_signing_alg_values_supported"] @property def __jwk_client(self) -> jwt.PyJWKClient: if self.__jwks: return self.__jwks self.__jwks = jwt.PyJWKClient(self.__jwks_uri) return self.__jwks def validate_credentials( username: Union[str, None] = None, password: Union[str, None] = None ) -> bool: """ This function validates CDSE credentials and returns a bool. If credentials are none, .netrc will be validated """ try: Credentials(username, password) return True except NoCredentialsException: return False except InvalidCredentialsException: return False except TokenExchangeException: return False CDSETool-0.3.1/src/cdsetool/download.py000066400000000000000000000224601517266010700176640ustar00rootroot00000000000000""" Download features from a Copernicus Data Space Ecosystem OData API result Provides a function to download a single feature, a function to download all features in a result set, and a function to download specific files in a given feature using node filtering. """ import fnmatch import os import random import shutil import tempfile import time from pathlib import Path from typing import Any, Dict, Generator, List, Union from xml.etree import ElementTree as ET from requests import Session from requests.exceptions import ChunkedEncodingError from urllib3.exceptions import ProtocolError from cdsetool._processing import _concurrent_process from cdsetool.credentials import ( Credentials, TokenClientConnectionError, TokenExpiredSignatureError, ) from cdsetool.logger import NoopLogger from cdsetool.monitor import NoopMonitor, StatusMonitor from cdsetool.query import FeatureQuery MANIFEST_FILENAMES = { "SENTINEL-1": "manifest.safe", "SENTINEL-2": "manifest.safe", "SENTINEL-3": "xfdumanifest.xml", } def filter_files( manifest_file: Path, pattern: Union[str, None], exclude: bool = False ) -> Union[List[Path], None]: """ Filter a product's files, listed in its manifest, based on a given pattern. Returns a list of file paths within the product bundle. All files not matching the pattern are returned if "exclude" is set to true. """ def read_sentinel_manifest(manifest_file: Path) -> Union[List[Path], None]: xmldoc = ET.parse(manifest_file) section = xmldoc.find("dataObjectSection") if section is None: return None paths = [] for obj in section.iterfind("dataObject"): loc = obj.find("byteStream/fileLocation") if loc is None: return None path = loc.get("href") if path is None: return None paths.append(Path(path)) return paths if pattern is None: return [] paths = read_sentinel_manifest(manifest_file) if paths is None: return None return [path for path in paths if fnmatch.fnmatch(str(path), pattern) ^ exclude] def download_file(url: str, path: Path, options: Dict[str, Any]) -> bool: """ Download a single file. Caller is responsible for ensuring that nothing else writes to path. """ log = _get_logger(options) filename = path.name max_attempts = options.get("download_attempts", 10) with _get_monitor(options).status() as status: status.set_filename(filename) attempts = 0 while attempts < max_attempts: attempts += 1 # Always get a new session, credentials might have expired. try: session = _get_credentials(options).get_session() except TokenClientConnectionError: log.warning("Token client connection failed, retrying..") continue except TokenExpiredSignatureError: log.warning("Token signature expired, retrying..") continue url = _follow_redirect(url, session) with session.get(url, stream=True) as response: if response.status_code != 200: retrying = attempts < max_attempts log.warning( f"Status code {response.status_code}, " f"{'retrying...' if retrying else 'aborting'}" ) if retrying: time.sleep(60 * (1 + (random.random() / 4))) continue status.set_filesize(int(response.headers["Content-Length"])) with open(path, "wb") as outfile: # Server might not send all bytes specified by the # Content-Length header before closing connection. # Log as a warning and try again. try: for chunk in response.iter_content(chunk_size=1024 * 1024 * 5): outfile.write(chunk) status.add_progress(len(chunk)) except ( ChunkedEncodingError, ConnectionResetError, ProtocolError, ) as e: log.warning(e) continue return True log.error(f"Failed to download {filename}") return False def download_feature( # pylint: disable=too-many-return-statements feature, path: str, options: Union[Dict[str, Any], None] = None ) -> Union[str, None]: """ Download a single feature. Returns the feature title. """ options = options or {} log = _get_logger(options) temp_dir_usr = _get_temp_dir(options) title = feature.get("Name") collection = feature.get("Collection") download_full = options.get("filter_pattern") is None try: manifest_filename = "" if download_full else MANIFEST_FILENAMES[collection] except KeyError: log.error( f"No support for downloading individual files in {collection} products" ) return None # Prepare to download full product, or manifest file if filter_pattern is used filename = title + ".zip" if download_full else manifest_filename url = ( _get_feature_url(feature) if download_full else _get_odata_url(feature["Id"], title, filename) ) if not url or not title: log.debug(f"Bad URL ('{url}') or title ('{title}')") return None result_path = os.path.join(path, filename if download_full else title) if not options.get("overwrite_existing", False) and os.path.exists(result_path): log.debug(f"File {result_path} already exists, skipping..") return os.path.basename(result_path) with tempfile.TemporaryDirectory( prefix=f"{title}____", dir=temp_dir_usr ) as temp_dir: temp_file = os.path.join(temp_dir, filename) if not download_file(url, Path(temp_file), options): return None # If filter_pattern is used, list matching files based on manifest contents temp_product_path = os.path.join(temp_dir, title) filtered_files = filter_files(Path(temp_file), options.get("filter_pattern")) if filtered_files is None: log.error(f"Failed to parse manifest file for {title}") return None for file in filtered_files: output_file = os.path.join(temp_product_path, file) os.makedirs(os.path.dirname(output_file), exist_ok=True) if not download_file( _get_odata_url(feature["Id"], title, str(file)), Path(output_file), options, ): log.error(f"Failed to download {file} from {title}") return None # Move downloaded files to output dir if download_full or filtered_files: os.makedirs(path, exist_ok=True) shutil.move(temp_file if download_full else temp_product_path, path) return filename if download_full else title def download_features( features: FeatureQuery, path: str, options: Union[Dict[str, Any], None] = None ) -> Generator[Union[str, None], None, None]: """ Generator function that downloads all features in a result set Feature IDs are yielded as they are downloaded """ options = options or {} options["credentials"] = _get_credentials(options) options["logger"] = _get_logger(options) options["monitor"] = _get_monitor(options) options["monitor"].start() def _download_feature(feature) -> Union[str, None]: return download_feature(feature, path, options) try: yield from _concurrent_process( _download_feature, features, options.get("concurrency", 1) ) finally: options["monitor"].stop() def _get_feature_url(feature) -> str: """ Generate OData download URL for a feature from its ID. """ feature_id = feature.get("Id") return ( ( "https://download.dataspace.copernicus.eu" f"/odata/v1/Products({feature_id})/$value" ) if feature_id else "" ) def _get_odata_url(product_id: str, product_name: str, href: str) -> str: """ Convert href, describing file location in manifest file, to an OData download URL. """ odata_url = "https://download.dataspace.copernicus.eu/odata/v1" path = "/".join([f"Nodes({item})" for item in href.split("/")]) return f"{odata_url}/Products({product_id})/Nodes({product_name})/{path}/$value" def _follow_redirect(url: str, session: Session) -> str: response = session.head(url, allow_redirects=False) while response.status_code in range(300, 400): url = response.headers["Location"] response = session.head(url, allow_redirects=False) return url def _get_logger(options: Dict) -> NoopLogger: return options.get("logger") or NoopLogger() def _get_monitor(options: Dict) -> Union[StatusMonitor, NoopMonitor]: return options.get("monitor") or NoopMonitor() def _get_credentials(options: Dict) -> Credentials: return options.get("credentials") or Credentials( proxies=options.get("proxies", None) ) def _get_temp_dir(options: Dict) -> Union[str, None]: return options.get("tmpdir") or None CDSETool-0.3.1/src/cdsetool/logger.py000066400000000000000000000012461517266010700173330ustar00rootroot00000000000000""" Logging utilities. This module provides a NoopLogger class that outputs nothing. """ from typing import Any class NoopLogger: """ A logger that does nothing. """ def debug(self, msg: object, *args: Any, **kwargs: Any) -> None: """ Log a debug message. """ def error(self, msg: object, *args: Any, **kwargs: Any) -> None: """ Log an error message. """ def info(self, msg: object, *args: Any, **kwargs: Any) -> None: """ Log an info message. """ def warning(self, msg: object, *args: Any, **kwargs: Any) -> None: """ Log a warning message. """ CDSETool-0.3.1/src/cdsetool/monitor.py000066400000000000000000000176531517266010700175540ustar00rootroot00000000000000""" File download monitoring. This module provides a StatusMonitor class that can be used to monitor the progress of file downloads. For non-interactive programs the NoopMonitor class can be used to disable status monitoring. """ import os import shutil import signal import sys import threading import time from typing import List, Tuple, Union IS_IPYTHON = True try: from IPython import get_ipython # type:ignore[reportMissingImports] from IPython.display import clear_output # type:ignore[reportMissingImports] if "IPKernelApp" not in get_ipython().config: IS_IPYTHON = False except ImportError: IS_IPYTHON = False except AttributeError: IS_IPYTHON = False class StatusMonitor(threading.Thread): """ A monitor that prints a status bar for each download Usage: with StatusMonitor() as monitor: with monitor.status() as status: status.set_filename("filename.txt") status.set_filesize(1024) status.add_progress(512) time.sleep(10) status.add_progress(512) """ line_length: int = 80 __is_running: bool = True __progress_lines: int = 0 __download_speed_deltas: List[int] = [] __done = [] __status = [] def start(self) -> None: """ Start the monitor """ def _set_line_length(_signal_num: Union[int, None], _stack) -> None: self.line_length, _ = shutil.get_terminal_size() _set_line_length(None, None) if os.name != "nt": signal.signal(signal.SIGWINCH, _set_line_length) super().start() def stop(self) -> None: """ Stop the monitor """ self.__is_running = False def status(self) -> "Status": """ Returns a status bar for a single download """ status = Status(self) self.__status.append(status) return status def remove_status(self, status: "Status") -> None: """ Remove a status from the monitor, marking it as done """ self.__done.append(status) self.__status.remove(status) def run(self) -> None: """ Main loop for the monitor, printing the status bars every second until stopped """ while True: self.__track_download_speed() if self.__is_running is False: break self.__clear_progress_lines() self.__print_done_lines() self.__draw() print("") @property def __download_speed(self) -> float: if len(self.__download_speed_deltas) < 2: return 0 return sum(self.__download_speed_deltas) / len(self.__download_speed_deltas) def __track_download_speed(self) -> None: speed_t0 = self.__total_downloaded time.sleep(1) speed_t1 = self.__total_downloaded self.__download_speed_deltas.append(speed_t1 - speed_t0) if len(self.__download_speed_deltas) > 10: self.__download_speed_deltas.pop(0) def __print_done_lines(self) -> None: for status in self.__done: print(status.done_line()) def __clear_progress_lines(self) -> None: if IS_IPYTHON: clear_output(wait=True) # type:ignore[reportPossiblyUnboundVariable] return sys.stdout.write("\033[K") for _ in range(self.__progress_lines + 2): sys.stdout.write("\033[F\033[K") for _ in self.__done: sys.stdout.write("\033[F\033[K") print("") print("") def __draw(self) -> None: self.__progress_lines = 1 print( " | ".join( [ "[[ ", f"{len(self.__status)} files in progress", f"{len(self.__done)} files done", f"{bytes_to_human(self.__total_downloaded)} total downloaded", f"{bytes_to_human(int(self.__download_speed))}/s ]]", ] ) ) for status in self.__status: filename_line, progress_line = status.status_lines() print(filename_line.ljust(self.line_length, " ")) print(progress_line.ljust(self.line_length, " ")) self.__progress_lines += 2 @property def __total_downloaded(self) -> int: return sum(status.downloaded for status in self.__status) + sum( status.size for status in self.__done ) def __enter__(self) -> "StatusMonitor": self.start() return self def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: self.stop() class NoopMonitor: """ A monitor that does nothing """ def status(self) -> "Status": """ Returns a status bar for a single download """ return Status(self) def remove_status(self, status: "Status") -> None: """ Remove a status from the monitor """ def start(self) -> None: """ Start the monitor """ def stop(self) -> None: """ Stop the monitor """ def __enter__(self) -> "NoopMonitor": return self def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: pass class Status: """ A status bar for a single download """ __monitor: Union[NoopMonitor, StatusMonitor, None] = None filename: Union[str, None] = None size: int = 0 downloaded: int = 0 def done_line(self) -> str: """ Returns a line to print when the download is complete """ if self.size == 0 and self.downloaded == 0: return f"{self.filename} skipped" return f"{self.filename} ({bytes_to_human(self.size)})" def status_lines(self) -> Tuple[str, str]: """ Returns a tuple of lines to print for the status bar """ if ( not self.__monitor or isinstance(self.__monitor, NoopMonitor) or not self.filename ): return ("", "") line_length = self.__monitor.line_length if self.downloaded == 0: return ( "Thread waiting for connection to start...", f"[{' ' * (line_length - 2)}]", ) progress = self.downloaded / self.size filename_line = ( f"{self.filename[0 : line_length - 6]} " + f"{bytes_to_human(self.size)} ({int(progress * 100)}%)" ) progress_line = ( "[" + f"{'â–ˆ' * int(progress * (line_length - 2))}" + f"{' ' * (line_length - int(progress * (line_length - 2)) - 2)}" + "]" ) return filename_line, progress_line def add_progress(self, chunk_bytes: int) -> None: """ Add to the number of bytes downloaded """ self.downloaded += chunk_bytes def set_filename(self, filename: str) -> None: """ Set the name of the file being downloaded """ self.filename = filename def set_filesize(self, size: int) -> None: """ Set the size of the file being downloaded """ self.size = size def __init__(self, monitor: Union[NoopMonitor, StatusMonitor]) -> None: self.__monitor = monitor def __enter__(self) -> "Status": return self def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: if self.__monitor: self.__monitor.remove_status(self) def bytes_to_human(num_bytes: int) -> str: """ Convert a number of bytes to a human-readable string """ if num_bytes < 1000: return f"{num_bytes} B" if num_bytes < 1000000: return f"{num_bytes / 1000:.2f} KB" if num_bytes < 1000000000: return f"{num_bytes / 1000000:.2f} MB" if num_bytes < 1000000000000: return f"{num_bytes / 1000000000:.2f} GB" return f"{num_bytes / 1000000000000:.2f} TB" CDSETool-0.3.1/src/cdsetool/py.typed000066400000000000000000000000001517266010700171640ustar00rootroot00000000000000CDSETool-0.3.1/src/cdsetool/query.py000066400000000000000000000515131517266010700172230ustar00rootroot00000000000000""" Query the Copernicus Data Space Ecosystem https://documentation.dataspace.copernicus.eu/APIs/OData.html """ import json import re from dataclasses import dataclass from datetime import date, datetime from random import random from time import sleep from typing import Any, Dict, Final, List, Literal, Optional, TypeVar, Union from urllib.parse import quote import geopandas as gpd from requests.exceptions import ChunkedEncodingError from urllib3.exceptions import ProtocolError from cdsetool._attributes import ATTRIBUTES from cdsetool.credentials import Credentials from cdsetool.logger import NoopLogger SearchTermValue = Union[str, int, float, bool, date, datetime] ODataComparisonOp = Literal["eq", "lt", "le", "gt", "ge"] ODataAttributeType = Literal[ "StringAttribute", "IntegerAttribute", "DoubleAttribute", "DateTimeOffsetAttribute", "BooleanAttribute", ] T = TypeVar("T") # API-imposed limits from the Copernicus OData API MAX_BATCH_SIZE: Final = 1000 @dataclass(frozen=True) class DateFilterSpec: """Specification for a date-based filter.""" odata_field: str operator: ODataComparisonOp title: str interval_only: bool _OPERATOR_LABELS: Dict[ODataComparisonOp, str] = { "eq": "equals", "lt": "less than", "le": "less than or equal", "gt": "greater than", "ge": "greater than or equal", } _DATE_FIELD_SPECS: List[tuple[str, str, str]] = [ ("contentDateStart", "ContentDate/Start", "Acquisition start date"), ("contentDateEnd", "ContentDate/End", "Acquisition end date"), ("publicationDate", "PublicationDate", "Publication date"), ] _OPERATOR_SUFFIXES: Dict[str, ODataComparisonOp] = { "Eq": "eq", "Lt": "lt", "Le": "le", "Gt": "gt", "Ge": "ge", } _DATE_FILTERS: Dict[str, DateFilterSpec] = { f"{base}{suffix}": DateFilterSpec( field, op, f"{desc} {_OPERATOR_LABELS[op]} ({field} {op})", interval_only=suffix == "", ) for base, field, desc in _DATE_FIELD_SPECS for suffix, op in [("", "eq"), *_OPERATOR_SUFFIXES.items()] } _BUILTIN_PARAMS: Dict[str, Dict[str, str]] = { "name": { "title": "Filter by product name (substring match)", "example": "S2A_MSIL2A_20240110", }, "geometry": { "title": "WKT geometry for spatial filtering", "example": "POLYGON((lon1 lat1, lon2 lat2, ...))", }, } _INTERNAL_PARAMS = {"top", "skip"} _TYPE_TO_ODATA_ATTR: Dict[str, ODataAttributeType] = { "String": "StringAttribute", "Integer": "IntegerAttribute", "Double": "DoubleAttribute", "DateTimeOffset": "DateTimeOffsetAttribute", "Boolean": "BooleanAttribute", } _DEPRECATED_PARAMS: Dict[str, str] = { "box": ( "The 'box' parameter was only supported in the old OpenSearch API, " "use the 'geometry' parameter with a polygon in WKT format instead. " "Example: geometry='POLYGON((west south, west north, " "east north, east south, west south))'." ), "startDate": ( "The 'startDate' parameter has been renamed. Use 'contentDateStartGt' instead." ), "completionDate": ( "The 'completionDate' parameter has been renamed. " "Use 'contentDateEndLt' instead." ), "publishedAfter": ( "The 'publishedAfter' parameter has been renamed. " "Use 'publicationDateGt' instead." ), "publishedBefore": ( "The 'publishedBefore' parameter has been renamed. " "Use 'publicationDateLt' instead." ), "maxRecords": "The 'maxRecords' parameter has been renamed. Use 'top' instead.", } class _FeatureIterator: def __init__(self, feature_query) -> None: self.index = 0 self.feature_query = feature_query def __len__(self) -> int: return len(self.feature_query) def __iter__(self): return self def __next__(self): try: item = self.feature_query[self.index] self.index += 1 return item except IndexError as exc: raise StopIteration from exc class FeatureQuery: # pylint: disable=too-many-instance-attributes """ An iterator over the features matching the search terms Queries the API in batches (default: MAX_BATCH_SIZE) features, and returns them one by one. Queries the next batch when the current batch is exhausted. """ def __init__( self, collection: str, search_terms: Dict[str, SearchTermValue], proxies: Union[Dict[str, str], None] = None, options: Union[Dict[str, Any], None] = None, ) -> None: opts = options or {} self.total_results = -1 self.features: List[Dict[str, Any]] = [] self.proxies = proxies self._max_attempts = opts.get("max_attempts", 10) self.log = opts.get("logger") or NoopLogger() self.collection = collection self.search_terms = search_terms # Option to expand Attributes for product metadata (default: False) self.expand_attributes = opts.get("expand_attributes", False) self._initial_skip = _to_int(search_terms.get("skip", 0)) self._top = _to_int(search_terms.get("top", MAX_BATCH_SIZE)) if self._top > MAX_BATCH_SIZE: self.log.warning( f"Maximum 'top' value is {MAX_BATCH_SIZE}, setting to {MAX_BATCH_SIZE}" ) self._top = MAX_BATCH_SIZE self.next_url = self._build_query_url(include_count=True) def __iter__(self): return _FeatureIterator(self) def __len__(self) -> int: if self.total_results < 0: self.__fetch_features() return self.total_results def __getitem__(self, index: int): while index >= len(self.features) and self.next_url is not None: self.__fetch_features() return self.features[index] def _build_query_url(self, include_count: bool = False) -> str: """Build query URL with current skip offset""" filter_expr = _build_odata_filter(self.collection, self.search_terms) params = [ f"$filter={quote(filter_expr)}", f"$top={self._top}", # Ordering for consistent pagination "$orderby=ContentDate/Start%20asc", ] if self._initial_skip > 0: params.append(f"$skip={self._initial_skip}") if include_count: params.append("$count=true") # Optionally expand Attributes to get product metadata # (productType, cloudCover, etc.) if self.expand_attributes: params.append("$expand=Attributes") return ( "https://catalogue.dataspace.copernicus.eu/odata/v1/Products?" f"{'&'.join(params)}" ) def __fetch_features(self) -> None: if self.next_url is None: return session = Credentials.make_session( None, False, Credentials.RETRIES, self.proxies ) attempts = 0 while attempts < self._max_attempts: attempts += 1 try: assert self.next_url is not None # for type checker with session.get(self.next_url) as response: if response.status_code != 200: retrying = attempts < self._max_attempts self.log.warning( f"Status code {response.status_code}, " f"{'retrying...' if retrying else 'aborting'}" ) if retrying: sleep(60 * (1 + (random() / 4))) continue odata_response = response.json() products = odata_response.get("value", []) # Add Collection attribute for download_feature() for product in products: product["Collection"] = self.collection self.features.extend(products) total_results = odata_response.get("@odata.count") if total_results is not None: self.total_results = total_results elif self.total_results < 0: self.log.error("Total result count not present in response.") next_link = odata_response.get("@odata.nextLink") self.next_url = ( _strip_odata_count(next_link) if next_link and self._top > 0 else None ) return except (ChunkedEncodingError, ConnectionResetError, ProtocolError) as e: self.log.warning(e) continue self.log.error("Failed to fetch features after %d attempts", attempts) self.next_url = None def query_features( collection: str, search_terms: Dict[str, SearchTermValue], proxies: Union[Dict[str, str], None] = None, options: Union[Dict[str, Any], None] = None, ) -> FeatureQuery: """ Returns an iterator over the features matching the search terms """ return FeatureQuery(collection, search_terms, proxies, options) def shape_to_wkt(shape: str) -> str: """ Convert a shapefile to a WKT string """ # pylint: disable=line-too-long coordinates = list(gpd.read_file(shape).geometry[0].exterior.coords) # pyright:ignore[reportAttributeAccessIssue] return ( "POLYGON((" + ", ".join(" ".join(map(str, coord)) for coord in coordinates) + "))" ) def geojson_to_wkt(geojson_in: Union[str, Dict]) -> str: """ Convert a geojson geometry to a WKT string """ geojson = json.loads(geojson_in) if isinstance(geojson_in, str) else geojson_in if geojson.get("type") == "Feature": geojson = geojson["geometry"] elif geojson.get("type") == "FeatureCollection" and len(geojson["features"]) == 1: geojson = geojson["features"][0]["geometry"] coordinates = str( tuple(item for sublist in geojson["coordinates"][0] for item in sublist) ) paired_coord = ",".join( [ f"{a}{b}" for a, b in zip(coordinates.split(",")[0::2], coordinates.split(",")[1::2]) ] ) return f"POLYGON({paired_coord})" def describe_search_terms() -> Dict[str, Dict[str, str]]: """Get builtin search terms (date filters, geometry) that are always available. Returns only the builtin parameters. To get collection-specific attributes, use describe_collection() with a collection name. """ terms: Dict[str, Dict[str, str]] = { key: {"title": spec.title, "example": "2024-01-01 or 2024-01-01T00:00:00Z"} for key, spec in _DATE_FILTERS.items() if not spec.interval_only } terms.update(_BUILTIN_PARAMS) return terms def _fetch_collection_attributes( collection: str, proxies: Union[Dict[str, str], None] = None, options: Union[Dict[str, Any], None] = None, ) -> Optional[List[Dict[str, str]]]: """Fetch available attributes for a collection from the OData API.""" url = f"https://catalogue.dataspace.copernicus.eu/odata/v1/Attributes({collection})" log = (options or {}).get("logger") or NoopLogger() session = Credentials.make_session(None, False, Credentials.RETRIES, proxies) response = session.get(url, timeout=30) if response.status_code == 404: log.error("Collection '%s' not found", collection) elif response.status_code != 200: log.error( "Failed to fetch attributes for '%s': HTTP status code %s", collection, response.status_code, ) return response.json() if response.status_code == 200 else None def describe_collection( collection: str, proxies: Union[Dict[str, str], None] = None, options: Union[Dict[str, Any], None] = None, ) -> Optional[Dict[str, Dict[str, str]]]: """ Get available filter parameters for a given collection. Fetches available attributes from the OData API's Attributes endpoint. Not all server attributes might be available as search terms at this time. Args: collection: Collection name (e.g., "SENTINEL-2", "SENTINEL-1") proxies: Optional proxy configuration options: Optional options Returns: Dictionary of parameters that can be used in filters (builtin + server attrs) or None if description could not be fetched """ # Start with built-in search terms (base date names only, no Lt/Le/Gt/Ge variants) search_terms: Dict[str, Dict[str, str]] = { base: {"title": desc, "example": "2024-01-01 or 2024-01-01T00:00:00Z"} for base, _, desc in _DATE_FIELD_SPECS } search_terms.update(_BUILTIN_PARAMS) # Fetch attributes for the collection from the server if server_attributes := _fetch_collection_attributes(collection, proxies, options): for attr in server_attributes: if not (name := attr.get("Name")): continue entry: Dict[str, str] = {"type": attr.get("ValueType", "String")} # Use title from ATTRIBUTES if available if name in ATTRIBUTES and (title := ATTRIBUTES[name].get("Title")): entry["title"] = title search_terms[name] = entry return dict(sorted(search_terms.items())) return None def get_product_attribute( product: Dict[str, Any], name: str, default: Optional[T] = None ) -> Optional[T]: """ Get an attribute value from a product's Attributes array. Args: product: Product dictionary name: Attribute name to retrieve (e.g., 'cloudCover', 'productType') default: Value to return if attribute is not found (default: None) Returns: The attribute value if found, default otherwise """ for attr in product.get("Attributes", []): if attr.get("Name") == name: return attr.get("Value") return default def _parse_interval( value: str, ) -> Optional[tuple[str, str, ODataComparisonOp, ODataComparisonOp]]: """Parse interval syntax like [a,b], (a,b), [a,b), (a,b]. Returns: Tuple of (start_value, end_value, start_op, end_op) or None if not an interval. start_op is 'ge' for '[' or 'gt' for '(' end_op is 'le' for ']' or 'lt' for ')' """ value = value.strip() if len(value) < 3: return None start_char = value[0] end_char = value[-1] if start_char not in "[(" or end_char not in "])": return None inner = value[1:-1] parts = inner.split(",") if len(parts) != 2: return None start_value = parts[0].strip() end_value = parts[1].strip() if not start_value or not end_value: return None # Determine operators based on brackets start_op = "ge" if start_char == "[" else "gt" end_op = "le" if end_char == "]" else "lt" return start_value, end_value, start_op, end_op def _parse_operator_suffix(key: str) -> tuple[str, ODataComparisonOp]: """Parse operator suffix from a key like 'cloudCoverLt'.""" for suffix, operator in _OPERATOR_SUFFIXES.items(): if key.endswith(suffix): base_name = key[: -len(suffix)] return base_name, operator return key, "eq" def _build_generic_attribute_filters(key: str, str_value: str) -> List[str]: """Build OData filter expression(s) for a generic attribute parameter.""" # Check if key has operator suffix (e.g., cloudCoverLt, orbitNumberGe) base_name, operator = _parse_operator_suffix(key) if not (attr_info := ATTRIBUTES.get(base_name)): raise ValueError(f"The '{key}' parameter is not supported.") attr_type = attr_info.get("Type", "String") if not (odata_attr_type := _TYPE_TO_ODATA_ATTR.get(attr_type)): raise ValueError( f"Unsupported attribute type '{attr_type}' for parameter '{key}'." ) # String and Boolean attributes only support equality if attr_type in ("String", "Boolean") and operator != "eq": raise ValueError( f"Comparison operators are not supported on {attr_type.lower()} " f"attribute '{base_name}'." ) # Check for interval syntax (only for numeric and date types) if attr_type in ("Integer", "Double", "DateTimeOffset"): has_suffix = key != base_name interval = _parse_interval(str_value) if not has_suffix: if not interval: raise ValueError( f"'{key}' requires interval syntax, e.g. {key}=[a,b]. " f"For an exact match, use '{key}Eq' instead." ) start_str, end_str, start_op, end_op = interval return [ _build_attribute_filter( base_name, start_str, odata_attr_type, start_op ), _build_attribute_filter(base_name, end_str, odata_attr_type, end_op), ] if interval: raise ValueError( f"Interval syntax is not allowed on '{key}'. " f"Use '{base_name}' for intervals instead." ) return [_build_attribute_filter(base_name, str_value, odata_attr_type, operator)] def _build_odata_filter( collection: str, search_terms: Dict[str, SearchTermValue] ) -> str: """Build $filter expression from search terms.""" filters = [f"Collection/Name eq '{collection}'"] for key, value in search_terms.items(): if key in _INTERNAL_PARAMS: continue if deprecated_message := _DEPRECATED_PARAMS.get(key): raise ValueError(deprecated_message) str_value = ( _format_odata_date(value) if isinstance(value, (datetime, date)) else str(value) ) if spec := _DATE_FILTERS.get(key): interval = _parse_interval(str_value) if spec.interval_only: if not interval: raise ValueError( f"'{key}' requires interval syntax, e.g. {key}=[a,b]. " f"For an exact match, use '{key}Eq' instead." ) start_str, end_str, start_op, end_op = interval filters.append(f"{spec.odata_field} {start_op} {start_str}") filters.append(f"{spec.odata_field} {end_op} {end_str}") else: if interval: raise ValueError( f"Interval syntax is not allowed on '{key}'. " f"Use the base name for intervals instead." ) filters.append(f"{spec.odata_field} {spec.operator} {str_value}") elif key in ("name", "nameEq"): filters.append(f"contains(Name,'{str_value}')") elif key in ("geometry", "geometryEq"): filters.append( f"OData.CSC.Intersects(area=geography'SRID=4326;{str_value}')" ) else: filters.extend(_build_generic_attribute_filters(key, str_value)) return " and ".join(filters) def _format_odata_date(date_value: Union[date, datetime]) -> str: """Format date value for OData filter expressions""" if isinstance(date_value, datetime): return date_value.strftime("%Y-%m-%dT%H:%M:%S.000Z") return f"{date_value.strftime('%Y-%m-%d')}T00:00:00.000Z" def _to_odata_value_str( value: str, attr_type: ODataAttributeType, attr_name: str ) -> str: """Convert a string value to its OData string representation.""" if attr_type == "StringAttribute": return f"'{value}'" if attr_type == "DoubleAttribute": return str(float(value)) if attr_type == "IntegerAttribute": return str(int(value)) if attr_type == "DateTimeOffsetAttribute": return value if attr_type == "BooleanAttribute": lower = value.lower() if lower not in ("true", "false"): raise ValueError( f"Invalid boolean value '{value}' for attribute '{attr_name}'. " "Use 'true' or 'false'." ) return lower return value def _build_attribute_filter( attr_name: str, attr_value: str, attr_type: ODataAttributeType, operator: ODataComparisonOp, ) -> str: value_str = _to_odata_value_str(attr_value, attr_type, attr_name) return ( f"Attributes/OData.CSC.{attr_type}/any(att:att/Name eq '{attr_name}' and " f"att/OData.CSC.{attr_type}/Value {operator} {value_str})" ) def _to_int(value: SearchTermValue) -> int: """Convert a search term value to int, accepting only int or str.""" if isinstance(value, (int, str)) and not isinstance(value, bool): return int(value) raise ValueError(f"Expected int or str, got {type(value).__name__}: {value!r}") def _strip_odata_count(url: str) -> str: """Remove $count=true from a URL to avoid requesting count on every page.""" url = re.sub(r"[&?](\$|%24)count=true", "", url, count=1) # If the first param was removed, the next '&' must become '?' if "?" not in url and "&" in url: url = url.replace("&", "?", 1) return url CDSETool-0.3.1/tests/000077500000000000000000000000001517266010700142365ustar00rootroot00000000000000CDSETool-0.3.1/tests/__init__.py000066400000000000000000000001431517266010700163450ustar00rootroot00000000000000# File intentionally left empty, its purpose is to allow relative imports within the tests folder. CDSETool-0.3.1/tests/credentials/000077500000000000000000000000001517266010700165335ustar00rootroot00000000000000CDSETool-0.3.1/tests/credentials/__init__.py000066400000000000000000000001431517266010700206420ustar00rootroot00000000000000# File intentionally left empty, its purpose is to allow relative imports within the tests folder. CDSETool-0.3.1/tests/credentials/credentials_test.py000066400000000000000000000103451517266010700224440ustar00rootroot00000000000000"""Tests for CDSETool's credentials module.""" # pyright: reportAttributeAccessIssue=false import datetime from typing import Any import pytest import requests from cdsetool.credentials import ( Credentials, InvalidCredentialsException, NoCredentialsException, TokenExchangeException, ) from ..mock_auth import mock_jwks, mock_openid, mock_token def test_ensure_tokens(requests_mock: Any, mocker: Any) -> None: mock_openid(requests_mock) mock_token(requests_mock) mock_jwks(mocker) credentials = Credentials("username", "password") assert credentials._Credentials__access_token is not None assert credentials._Credentials__refresh_token is not None credentials._Credentials__access_token_expires = ( datetime.datetime.now() - datetime.timedelta(hours=100) ) spy = mocker.spy(credentials, "_Credentials__token_exchange") credentials._Credentials__ensure_tokens() spy.assert_called_once() credentials._Credentials__access_token_expires = ( datetime.datetime.now() - datetime.timedelta(hours=100) ) credentials._Credentials__refresh_token_expires = ( datetime.datetime.now() - datetime.timedelta(hours=100) ) credentials._Credentials__ensure_tokens() assert spy.call_count == 2 def test_read_credentials(requests_mock: Any, mocker: Any) -> None: mock_openid(requests_mock) mock_token(requests_mock) mock_jwks(mocker) mocker.patch( "netrc.netrc", return_value=mocker.Mock( authenticators=lambda x: ("username", None, "password") ), ) credentials = Credentials() assert credentials._Credentials__username == "username" assert credentials._Credentials__password == "password" mocker.patch("netrc.netrc", return_value=mocker.Mock(authenticators=lambda x: None)) with pytest.raises(NoCredentialsException): credentials = Credentials() def test_refresh_token(requests_mock: Any, mocker: Any) -> None: mock_openid(requests_mock) mock_token(requests_mock) mock_jwks(mocker) credentials = Credentials("username", "password") assert credentials._Credentials__access_token is not None assert credentials._Credentials__refresh_token is not None mock_token(requests_mock) # mock again to return a new token prev_access_token = credentials._Credentials__access_token credentials._Credentials__access_token_expires = datetime.datetime.now() credentials._Credentials__refresh_token_expires = datetime.datetime.now() credentials._Credentials__ensure_tokens() assert credentials._Credentials__access_token is not None assert credentials._Credentials__refresh_token is not None assert prev_access_token != credentials._Credentials__access_token def test_get_session(requests_mock: Any, mocker: Any) -> None: mock_openid(requests_mock) mock_token(requests_mock) mock_jwks(mocker) credentials = Credentials("username", "password") session = credentials.get_session() assert isinstance(session, requests.Session) assert ( session.headers.get("Authorization") == f"Bearer {credentials._Credentials__access_token}" ) def test_token_exchange(requests_mock: Any, mocker: Any) -> None: mock_openid(requests_mock) mock_token(requests_mock) mock_jwks(mocker) credentials = Credentials("myuser123123", "password") data = { "grant_type": "password", "username": credentials._Credentials__username, "password": credentials._Credentials__password, "client_id": "cdse-public", } credentials._Credentials__token_exchange(data) requests_mock.post( "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token", text="{}", status_code=401, ) with pytest.raises( InvalidCredentialsException, match="with username: myuser123123" ): credentials._Credentials__token_exchange(data) requests_mock.post( "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token", text="Failure 123123", status_code=500, ) with pytest.raises(TokenExchangeException, match="Failure 123123"): credentials._Credentials__token_exchange(data) CDSETool-0.3.1/tests/credentials/mock/000077500000000000000000000000001517266010700174645ustar00rootroot00000000000000CDSETool-0.3.1/tests/credentials/mock/openid-configuration.json000066400000000000000000000144751517266010700245150ustar00rootroot00000000000000{"issuer":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE","authorization_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/auth","token_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token","introspection_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token/introspect","userinfo_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/userinfo","end_session_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/logout","frontchannel_logout_session_supported":true,"frontchannel_logout_supported":true,"jwks_uri":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/certs","check_session_iframe":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/login-status-iframe.html","grant_types_supported":["authorization_code","implicit","refresh_token","password","client_credentials","urn:ietf:params:oauth:grant-type:device_code","urn:openid:params:grant-type:ciba","urn:ietf:params:oauth:grant-type:token-exchange"],"acr_values_supported":["0","1"],"response_types_supported":["code","none","id_token","token","id_token token","code id_token","code token","code id_token token"],"subject_types_supported":["public","pairwise"],"id_token_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"id_token_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"userinfo_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"userinfo_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"userinfo_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"request_object_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"request_object_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"request_object_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"response_modes_supported":["query","fragment","form_post","query.jwt","fragment.jwt","form_post.jwt","jwt"],"registration_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/clients-registrations/openid-connect","token_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"token_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"introspection_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"authorization_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"claims_supported":["aud","sub","iss","auth_time","name","given_name","family_name","preferred_username","email","acr"],"claim_types_supported":["normal"],"claims_parameter_supported":true,"scopes_supported":["openid","additional_scope","acr","web-origins","microprofile-jwt","AUDIENCE_PUBLIC","phone","zipper_session_scope","Geographic_origin","address","openid","email","ondemand_processing","offline_access","roles","profile","user-context"],"request_parameter_supported":true,"request_uri_parameter_supported":true,"require_request_uri_registration":true,"code_challenge_methods_supported":["plain","S256"],"tls_client_certificate_bound_access_tokens":true,"revocation_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/revoke","revocation_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"device_authorization_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/auth/device","backchannel_token_delivery_modes_supported":["poll","ping"],"backchannel_authentication_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/ext/ciba/auth","backchannel_authentication_request_signing_alg_values_supported":["PS384","ES384","RS384","ES256","RS256","ES512","PS256","PS512","RS512"],"require_pushed_authorization_requests":false,"pushed_authorization_request_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/ext/par/request","mtls_endpoint_aliases":{"token_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token","revocation_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/revoke","introspection_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token/introspect","device_authorization_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/auth/device","registration_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/clients-registrations/openid-connect","userinfo_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/userinfo","pushed_authorization_request_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/ext/par/request","backchannel_authentication_endpoint":"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/ext/ciba/auth"}}CDSETool-0.3.1/tests/download/000077500000000000000000000000001517266010700160455ustar00rootroot00000000000000CDSETool-0.3.1/tests/download/__init__.py000066400000000000000000000001431517266010700201540ustar00rootroot00000000000000# File intentionally left empty, its purpose is to allow relative imports within the tests folder. CDSETool-0.3.1/tests/download/download_test.py000066400000000000000000000235301517266010700212700ustar00rootroot00000000000000"""Tests for CDSETool's download module.""" import logging import os from pathlib import Path from typing import Any, Dict, List import pytest from cdsetool.credentials import Credentials from cdsetool.download import ( _get_feature_url, _get_odata_url, download_feature, download_file, filter_files, ) from ..mock_auth import mock_jwks, mock_openid, mock_token def mock_download_file(url: str, path: str, options: Dict[str, Any]) -> bool: """Mock the download_file function to create mock files.""" with open(path, "wb") as f: f.write(b"dummy data") return True def test_get_feature_url() -> None: """Test full product OData download URL generation.""" feature = {"Id": "a6215824-704b-46d7-a2ec-efea4e468668"} expected_url = ( "https://download.dataspace.copernicus.eu/odata/v1/" "Products(a6215824-704b-46d7-a2ec-efea4e468668)/$value" ) assert _get_feature_url(feature) == expected_url assert _get_feature_url({}) == "" assert _get_feature_url({"Id": None}) == "" def test_get_odata_url() -> None: """Test individual file OData download URL generation.""" product_id = "a6215824-704b-46d7-a2ec-efea4e468668" product_name = "S2B_MSIL1C_20241209T162609_N0511_R040_T17UPV_20241209T195414.SAFE" href = "path/to/resource.xml" expected_url = ( "https://download.dataspace.copernicus.eu/odata/v1/" "Products(a6215824-704b-46d7-a2ec-efea4e468668)/" f"Nodes({product_name})/" "Nodes(path)/Nodes(to)/Nodes(resource.xml)/$value" ) assert _get_odata_url(product_id, product_name, href) == expected_url @pytest.mark.parametrize( "manifest_file_path, pattern, expected_files", [ ( Path("tests/download/mock/sentinel_1/manifest.safe"), "*/calibration-*.xml", [ Path( "annotation/calibration/calibration-s1a-iw-grd-vh-20241217t061735-20241217t061800-057028-07020f-002.xml" ), Path( "annotation/calibration/calibration-s1a-iw-grd-vv-20241217t061735-20241217t061800-057028-07020f-001.xml" ), ], ), ( Path("tests/download/mock/sentinel_2/manifest.safe"), "*TCI.jp2", [ Path( "GRANULE/L1C_T17UPV_A040535_20241209T162603/IMG_DATA/T17UPV_20241209T162609_TCI.jp2" ) ], ), ( Path("tests/download/mock/sentinel_3/xfdumanifest.xml"), "*Oa02_reflectance.nc", [Path("Oa02_reflectance.nc")], ), ], ) def test_filter_files( manifest_file_path: Path, pattern: str, expected_files: List[str] ) -> None: filtered_files = filter_files(manifest_file_path, pattern) assert filtered_files == expected_files def test_filter_files_with_exclude() -> None: manifest_file_path = Path("tests/download/mock/sentinel_2/manifest.safe") filtered_files = filter_files(manifest_file_path, "*.jp2", exclude=True) assert filtered_files == [ Path("MTD_MSIL1C.xml"), Path("INSPIRE.xml"), Path("HTML/UserProduct_index.html"), Path("HTML/UserProduct_index.xsl"), Path("DATASTRIP/DS_2BPS_20241209T195414_S20241209T162603/MTD_DS.xml"), Path( "DATASTRIP/DS_2BPS_20241209T195414_S20241209T162603/QI_DATA/FORMAT_CORRECTNESS.xml" ), Path( "DATASTRIP/DS_2BPS_20241209T195414_S20241209T162603/QI_DATA/GENERAL_QUALITY.xml" ), Path( "DATASTRIP/DS_2BPS_20241209T195414_S20241209T162603/QI_DATA/GEOMETRIC_QUALITY.xml" ), Path( "DATASTRIP/DS_2BPS_20241209T195414_S20241209T162603/QI_DATA/RADIOMETRIC_QUALITY.xml" ), Path( "DATASTRIP/DS_2BPS_20241209T195414_S20241209T162603/QI_DATA/SENSOR_QUALITY.xml" ), Path("GRANULE/L1C_T17UPV_A040535_20241209T162603/AUX_DATA/AUX_CAMSFO"), Path("GRANULE/L1C_T17UPV_A040535_20241209T162603/AUX_DATA/AUX_ECMWFT"), Path("GRANULE/L1C_T17UPV_A040535_20241209T162603/MTD_TL.xml"), Path( "GRANULE/L1C_T17UPV_A040535_20241209T162603/QI_DATA/FORMAT_CORRECTNESS.xml" ), Path("GRANULE/L1C_T17UPV_A040535_20241209T162603/QI_DATA/GENERAL_QUALITY.xml"), Path( "GRANULE/L1C_T17UPV_A040535_20241209T162603/QI_DATA/GEOMETRIC_QUALITY.xml" ), Path("GRANULE/L1C_T17UPV_A040535_20241209T162603/QI_DATA/SENSOR_QUALITY.xml"), ] def test_filter_files_no_match() -> None: manifest_file_path = Path("tests/download/mock/sentinel_2/manifest.safe") filtered_files = filter_files(manifest_file_path, "Oa1*.nc") assert not filtered_files def test_filter_files_broken_manifest() -> None: manifest_file_path = Path("tests/download/mock/sentinel_2/broken_manifest.safe") filtered_files = filter_files(manifest_file_path, "*TCI.jp2") assert filtered_files is None def test_download_file_success(requests_mock: Any, mocker: Any, tmp_path: Path) -> None: mock_openid(requests_mock) mock_token(requests_mock) mock_jwks(mocker) mock_url = "http://example.com/file" mocker.patch("cdsetool.download._follow_redirect", return_value=mock_url) content = b"data" * 5 requests_mock.get( mock_url, status_code=200, headers={"Content-Length": "100"}, content=content ) mock_file = tmp_path / "mock_file" result = download_file( mock_url, mock_file, {"credentials": Credentials("usr", "pwd")} ) assert result is True # Check that file was written correctly with open(mock_file, "rb") as f: file_content = f.read() assert file_content == content def test_download_file_failure(requests_mock: Any, mocker: Any, tmp_path: Path) -> None: mock_openid(requests_mock) mock_token(requests_mock) mock_jwks(mocker) mock_url = "http://example.com/file" mocker.patch("cdsetool.download._follow_redirect", return_value=mock_url) requests_mock.get(mock_url, status_code=404, headers={"Content-Length": "100"}) mock_file = tmp_path / "mock_file" mocker.patch("time.sleep", return_value=None) # Avoid retry delay result = download_file( mock_url, mock_file, {"credentials": Credentials("usr", "pwd")} ) assert result is False def test_download_feature(mocker: Any, tmp_path: Path) -> None: title = "S2B_MSIL1C_20241209T162609_N0511_R040_T17UPV_20241209T195414.SAFE" mock_feature = { "Id": "a6215824-704b-46d7-a2ec-efea4e468668", "Name": title, "ContentLength": 1000, "Online": True, } mocker.patch("cdsetool.download.download_file", mock_download_file) final_dir = str(tmp_path / "test_download_feature") filename = download_feature(mock_feature, final_dir) assert filename == f"{title}.zip" assert os.path.exists(os.path.join(final_dir, f"{title}.zip")) def test_download_feature_failure(mocker: Any, tmp_path: Path) -> None: title = "S2B_MSIL1C_20241209T162609_N0511_R040_T17UPV_20241209T195414.SAFE" mock_feature = { "Id": "a6215824-704b-46d7-a2ec-efea4e468668", "Name": title, "ContentLength": 1000, "Online": True, } mocker.patch( "cdsetool.download.download_file", side_effect=lambda url, path, options: None ) final_dir = str(tmp_path / "test_download_feature_failure") filename = download_feature(mock_feature, final_dir) assert filename is None def test_download_feature_with_filter(mocker: Any, tmp_path: Path) -> None: options = {"filter_pattern": "*.jp2"} title = "S2B_MSIL1C_20241209T162609_N0511_R040_T17UPV_20241209T195414.SAFE" mock_feature = { "Id": "a6215824-704b-46d7-a2ec-efea4e468668", "Name": title, "ContentLength": 1000, "Online": True, "Collection": "SENTINEL-2", } mocker.patch( "cdsetool.download.filter_files", return_value=[Path("./GRANULE/file1.jp2"), Path("./GRANULE/file2.jp2")], ) mocker.patch("cdsetool.download.download_file", mock_download_file) final_dir = str(tmp_path / "test_download_feature_with_filter") product_name = download_feature(mock_feature, final_dir, options) assert product_name == mock_feature["Name"] assert os.path.exists(os.path.join(final_dir, title, "GRANULE", "file1.jp2")) assert os.path.exists(os.path.join(final_dir, title, "GRANULE", "file2.jp2")) def test_download_feature_with_filter_failure(mocker: Any, tmp_path: Path) -> None: options = {"filter_pattern": "*.jp2"} mock_feature = { "Id": "a6215824-704b-46d7-a2ec-efea4e468668", "Name": "S2B_MSIL1C_20241209T162609_N0511_R040_T17UPV_20241209T195414.SAFE", "ContentLength": 1000, "Online": True, "Collection": "SENTINEL-2", } mocker.patch( "cdsetool.download.filter_files", return_value=["./GRANULE/file1.jp2", "./GRANULE/file2.jp2"], ) mocker.patch( "cdsetool.download.download_file", side_effect=lambda url, path, options: None ) final_dir = str(tmp_path / "test_download_feature_with_filter_failure") product_name = download_feature(mock_feature, final_dir, options) assert product_name is None def test_download_feature_with_filter_unsupported_coll( caplog: Any, tmp_path: Path ) -> None: options = {"logger": logging.getLogger(__name__), "filter_pattern": "*MTL.txt"} mock_feature = { "Id": "a6215824-704b-46d7-a2ec-efea4e468668", "Name": "L8XXX", "ContentLength": 1000, "Online": True, "Collection": "Landsat8", } final_dir = str(tmp_path / "test_download_feature_with_filter_unsupported_coll") product_name = download_feature(mock_feature, final_dir, options) assert product_name is None assert ( "No support for downloading individual files in " f"{mock_feature['Collection']} products" ) in caplog.text CDSETool-0.3.1/tests/download/mock/000077500000000000000000000000001517266010700167765ustar00rootroot00000000000000CDSETool-0.3.1/tests/download/mock/sentinel_1/000077500000000000000000000000001517266010700210375ustar00rootroot00000000000000CDSETool-0.3.1/tests/download/mock/sentinel_1/manifest.safe000066400000000000000000000577241517266010700235240ustar00rootroot00000000000000 2014-016A SENTINEL-1 A Synthetic Aperture Radar IW IW 57028 57028 81 81 340 1 DESCENDING 2024-12-17T05:40:13.187705 7 459279 VV VH S SAR Standard L1 Product Slice GRD NRT-3h true 2024-12-17T06:09:11.775902 21 30 2024-12-17T06:17:35.476248 2024-12-17T06:18:00.475440 2.242288e+06 2.267288e+06 42.366711,-1.164012 42.775124,-4.343804 44.273342,-4.011340 43.865166,-0.752081 5b30ed5fdd5c443779924148cc0ab233 97bbba6752ff6c8481c5359b2dad036f de1b98b98001255ef4301acfa9ddaf71 7861e6ec3cd64bcfece1441f390ff314 1985a1e903bc8a6137dc9d88a4ced035 f828087c6ef5f41f8f33a633ad8ddeb6 02c0dda3f8e409a15e92f9635625316b 42128e0689c0d32be00c71953adccf0e abca46e8ad0eb8cee9a853963a4cc624 ccc01e1335dcaedbfbea083dccc0a140 c6ab33190822b1151862d7a86433b896 f1d13a5bbd57c466f2192fe7246d4472 7a7f2cba6bec280ff36c63b7dfa95697 CDSETool-0.3.1/tests/download/mock/sentinel_2/000077500000000000000000000000001517266010700210405ustar00rootroot00000000000000CDSETool-0.3.1/tests/download/mock/sentinel_2/broken_manifest.safe000066400000000000000000000005411517266010700250460ustar00rootroot00000000000000 91CB5B925086749536F310AC7F5E0F2F892CE36FBAA85D6DF2E696EAA64B8400 CDSETool-0.3.1/tests/download/mock/sentinel_2/manifest.safe000066400000000000000000001370631517266010700235200ustar00rootroot00000000000000 2024-12-09T16:26:09.024Z 040535 040 2015-000A SENTINEL 2B Multi-Spectral Instrument 54.138373318280884 -79.46929874363425 54.10530483528773 -77.79073991512661 53.119880621881606 -77.86467963079592 53.15178549855093 -79.50462041848775 54.138373318280884 -79.46929874363425 91CB5B925086749536F310AC7F5E0F2F892CE36FBAA85D6DF2E696EAA64B8400 1463146EEBA317525C9D5B05CE510C7424D5F3E25CDD8E4CA817E2F637F4B656 037BB08E909E30C92FCF078F2E74FAAF001E0C3447464FFCA2078507304FE52C 19EC86D5D77055C742FBE694EE1658B657FD172FE73875D34735B327F6B4B6C4 3B00AF86D86E5B54BF07253ADC453FB52E8F686151B78E9D414792AB62D580C2 40B8F9E535271BAC5BC10D240657A4467266907251101BFCBCC23680C06E3350 027A1D72F7FF1C6A7DD68CC165C5311E82C40B1FF0A1355CFD04C70F61A23981 09CC3AF3937ECDA150B45A936B8A1519872E71F30F0244A757E55B3E6B2098C1 9A8625D5DF20B1910A9BE16C77D05B4C45CA6AC5FBC5FB717095DBA3D00A03A9 736DE533044527B89308682218D746965C972BEC60DFED06B001BEBDD25FE657 A721E16D3C0886F57EC1FCB7769412FF43C389850B96D2E597A0D3446C19B103 5DC39B632463F835DD0D7CD64786B75F25A5E061C3681FC489203CB1581804A7 F9D700F56D93636E0702346D96EFE62D876485084B1459DCD1EF737727ABBD33 2B25B963D2E77B6C1A0BF88C7A8C91B9C6991F437FE1018B1A5B5B2FB7E87860 873BCC66ED7B95E9EC929B0ECA6801B72FF714ACBEC7121007E91B4A3F34AA71 1C4E18686E6C0DE89D82AB5F8334B77636224215377E672734D8C7E363FDC3CA 821568D85ED5CBF83631BFACD70A8F4BC0FC9CD27D3A1C8A5F6C311CF8C16D7F A135A1BB4EE78A596627F0538B430C18D6CEEAD7BCF4BF9CAFEE31408B7943A6 FDB7B76CEA78CC7BDB9D9E7998F628986F4913482F15A3DBB6A1397B9F29EA85 FD71EA5D823AFF2E79DC81390B52D6B29914FCB5EB343635FFC1F0DB92DE551A F410C5B64234C3D4B0B093174AB99860FE0E7171DFDB69DEDA6CA125697926FD 6417ED6BCF82FC199DD17B3C1AFCA827BDDE3A87D2BFEBF86F81AF6D389F65C8 E48A90B1F8002F2865196C25703894C5341B1207D9F379DC74C5399DAB14660C 25D63C0C440C96A974E5C8A290E85F0E2C8B1A23F491399BAFB98B3457676930 D190062FF5613DA70A68CC5EEF3024399F580D2E193408448B175E80CB92B88A 2EC293A311831382D11F801ACBC0117A7899024DE7EFE41DA7B1722ACC22FA1B A12C9E52A687141D9B668DCE343D1E82F421B8E5A0D5F9478A95E5E6B31985C1 683841A67EC38E189FBCFF65176B358BB8D24FC92192078F5F2CCFC813760E98 A8426B61A8C4174E4F866DD8BC32D90151ECC01A1F03B44D92A9075059C98B3E 554C649431A4825F8A1AB789BB26EDD6C90718E037A9D680A7EE1B77D328789E F277BDBF0D2F71DBC755D873F4FB01CF1512A84B48D7280ACCE1594B794410B3 BB96C6C1E04C521021F41FD2328F2BE55EB7800A90180B558865E5FB956CF87B B2DACCCA2E2745E6C580011A9AC0A84CDDAA1927E73717F5630ADDA9A19972C6 0D19D5DF93CAE1E00FFD366658E7AD5E19AAB8E2DAE6550257608AECBC4148C6 4D37B067C45E6D4447BC666A8DC2E195EABF3049364416ED3BA6E276F0AA5480 61663F550905006074F66C5F7ACA5297FF4E98F5BA62E441EAD75DA0ECFD7113 3B57C6552C93F441E51E8BBDC7DD0C8B043EB4DEBA34FEB165E5F08FC3A4A60B A5AFBD22B1D9D1F31A3AE9B01C82CAD4266942F1D58AC6D26DB4556E3D42AAA1 CF4AEA99127D18E6AA13EE9B6331AA16BB66494526310CF775AD9F11064116F0 BE4AC19749ABCF4BCCCB5B89602364542CB3E51D2BD1318ED35521630A9D6655 74A9ECD581B96479159EE6BCC6ACC335637C038F508B5BE89F01E0D5D57F941F 6466B35D0DB80890B38437D419FA08BBFEC6CEA01A56C00087355936AF7EA153 8DE0EFE9848DD322CA040E0574186B691F171245478013BFF8634BF5D4B3B65D 59C0643A4134DA0548D7B8473F564C2406AF7A9E616ADCE7BC9D55A869314123 F6D6D77B0780A66FBB4F8172243B92517DB6E37E6746D07950B940C194808E69 1D9CCD23CAD257620691C9895B460D6CBB96F0DF1EC886C450D1BDA61C281D20 1D9CCD23CAD257620691C9895B460D6CBB96F0DF1EC886C450D1BDA61C281D20 1D9CCD23CAD257620691C9895B460D6CBB96F0DF1EC886C450D1BDA61C281D20 3FEFAC1F12D8118701BF3B650FFEDC898B1BD6A829F32F1E68C636F8CFBB0D1C 3FEFAC1F12D8118701BF3B650FFEDC898B1BD6A829F32F1E68C636F8CFBB0D1C 3FEFAC1F12D8118701BF3B650FFEDC898B1BD6A829F32F1E68C636F8CFBB0D1C 1D9CCD23CAD257620691C9895B460D6CBB96F0DF1EC886C450D1BDA61C281D20 F6D6D77B0780A66FBB4F8172243B92517DB6E37E6746D07950B940C194808E69 1B758CAF2FC75F5247FEA39C038F458014177047207ED8B36A55ADA1F7230F06 45DC230798AD682DB4ECE344D26AC9B5F4AAF0F286307C3D1A352209E0AF278D 85A15AB0FDF348150D5BE7BF11760627B0C0C81E03758E27733EFC4399529BBF 3FEFAC1F12D8118701BF3B650FFEDC898B1BD6A829F32F1E68C636F8CFBB0D1C 085CCF405B7A5735AC1CAE54325265FBC0CF487DA3CAAB982F926C0169D997F4 9E7FCE6D5B5942C62B05D9644EB35297CA20E794E3D1C3DF6CEF1A4543732B0A CDSETool-0.3.1/tests/download/mock/sentinel_3/000077500000000000000000000000001517266010700210415ustar00rootroot00000000000000CDSETool-0.3.1/tests/download/mock/sentinel_3/xfdumanifest.xml000066400000000000000000011442501517266010700242670ustar00rootroot00000000000000 2024-12-17T09:53:04.547252Z 2024-12-17T09:56:04.547252Z 2018-039A Sentinel-3 B Ocean Land Colour Instrument Earth Observation 41.9628 -2.91463 41.8855 -2.06724 41.8044 -1.25962 41.7151 -0.431305 41.6236 0.385984 41.5227 1.20932 41.4179 2.01602 41.3057 2.82933 41.1887 3.63549 41.0651 4.44499 40.9371 5.2492 40.8031 6.04903 40.6638 6.84423 40.5194 7.64004 40.3687 8.43185 40.2131 9.21817 40.0519 9.99997 39.8854 10.7801 39.7136 11.5562 39.5378 12.3285 42.1353 13.3764 44.7284 14.5088 47.3102 15.7387 49.8787 17.0852 50.0827 16.1752 50.281 15.256 50.4712 14.3324 50.6546 13.4002 50.8298 12.4625 50.9977 11.5164 51.1572 10.5629 51.3096 9.60446 51.4543 8.63883 51.5902 7.66771 51.719 6.69084 51.84 5.70593 51.9528 4.71557 52.0567 3.72378 52.1524 2.7262 52.2366 1.72982 52.3154 0.725536 52.386 -0.28795 52.446 -1.29599 49.8263 -1.69214 47.2049 -2.09383 44.582 -2.50158 41.9628 -2.91463 S3B_OL_2_WFR____20241217T095305_20241217T095605_20241218T170707_0179_101_079_2160_MAR_O_NT_003.SEN3 OL_2_WFR___ NT 003 OL__L2M.003.05.01 20241218T170707 229073837 1 FIRST 2024-12-17T09:51:35.043217Z CGS 2024-12-17T11:22:26.704865Z 2024-12-17T11:22:47.692367Z Operational FRAME 179 2160 PARALLEL 2.000000 0 4091 4865 ANALYSIS 147208476701 true 44002 294 270 1 64 400 15 412.5 10 442.5 10 490 10 510 10 560 10 620 10 665 10 673.75 7.5 681.25 7.5 708.75 10 753.75 7.5 778.75 15 865 20 885 10 1020 40 34616 79 69232 158 101 4 2024-12-17T09:17:41.547252 2024-12-17T09:17:04.547252Z 2024-12-17T09:17:04.596577 -7053536.5159999998 -1360183.953 0.002 -301.98742299999998 1612.5008439999999 7366.681732 2024-12-17T10:58:40.688824 2024-12-17T10:58:03.688824Z 2024-12-17T10:58:03.738150 -6959929.3679999998 1778236.7890000001 0.001 414.607641 1587.337254 7366.6733510000004 PASSED NON_NOMINAL_INPUT 5d0a0d3ee5a73998e382ea78685706ef aa0dc4742ccaf4d4c09f9ea7dd80adc5 2e5a95614bd470ae02c74083f9fd4e0a c3be543e05f0a903c93d86f050ff7870 8784098804754b7139d6af5dd25e9939 06b9a36e49149bffd6f60d6032fee700 19d8a1375c945f7db2cf43fc6eda9c8f 3ca716b024270ed34b0847093d247051 53bfd7138a2732ee744ce0b9a07bb1d0 4ecf08b90208579ddeb647cfdd734453 7e0a5879f1e71734244bbb972a3da2a5 0dd26beda74e214eb32db8b032885be1 73ddc883cdc7dba983ec3e93cd891680 80f769d0712905ee4951e598f93f2ff3 64730095e7b418c719423ab0d10ceaab 6164180b2ab97a4fce84753b60682c21 611cb984248f0c7b71a57414f2eaa423 b088af54aa4f3aaeb31cc758dde16715 d42aa2857503ea0335bf01eff51e9ece 22bcf2d8bc800e2c1b75c46185fac917 fa5f95394992702bb883dec2364cb290 502424d1417eb7ccb145d4f3c7f669dc 90e6c00feadd7571b32df02f1a45e148 5b5123aa9d4c10877b9ee69334fc3046 179a07c5584da7c134a20664b2ba7638 12fa873f3661e41c3bae17d4fa099486 980d2551675690fe9ea2cd518bd96eb7 5a636fa2dc967ce481cc66a721cea7ad d3fef4ea4691bb9df8744b861884ae15 d0d6513d8e53e882c4011173695e42bf c967d2eb880183d5ebca91d6a8769c21 0a2051f0bf292463e16b6eb4b42279b5 CDSETool-0.3.1/tests/logger/000077500000000000000000000000001517266010700155155ustar00rootroot00000000000000CDSETool-0.3.1/tests/logger/logger_test.py000066400000000000000000000016541517266010700204130ustar00rootroot00000000000000"""Tests for CDSETool's logger module.""" from unittest.mock import MagicMock import pytest from cdsetool.download import download_feature from cdsetool.logger import NoopLogger def test_noop_logger_is_default() -> None: NoopLogger.debug = MagicMock() assert NoopLogger.debug.call_count == 0 download_feature( { "bad_object": True, "Name": "myfile.xml", # Missing Id will cause bad URL }, "somewhere", ) assert NoopLogger.debug.call_count == 1 def test_noop_does_not_error() -> None: try: download_feature( { "bad_object": True, "Name": "myfile.xml", # Missing Id will cause bad URL }, "somewhere", ) NoopLogger().debug("NoopLogger did not raise an exception") except Exception as e: pytest.fail(f"Unexpected exception: {e}") CDSETool-0.3.1/tests/mock_auth.py000066400000000000000000000106731517266010700165710ustar00rootroot00000000000000"""Helper functions for mocking CDSE authentication endpoints.""" import base64 import datetime import json from typing import Any import jwt from cryptography.hazmat.primitives.asymmetric import rsa private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) def mock_openid(requests_mock: Any) -> None: with open("tests/credentials/mock/openid-configuration.json") as f: requests_mock.get( "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/.well-known/openid-configuration", text=f.read(), ) def mock_token(requests_mock: Any) -> None: headers = {"alg": "RS256", "typ": "JWT", "kid": "key-9000"} now = datetime.datetime.now() payload = { "exp": now.timestamp() + 3600, "iat": now.timestamp(), "jti": "bb4d3fab-1a39-442c-9a6f-072a167543c0", "iss": "https://identity.dataspace.copernicus.eu/auth/realms/CDSE", "aud": ["CLOUDFERRO_PUBLIC", "account"], "sub": "bfb3df17-4506-4adf-86ab-9bb9d03f11f1", "typ": "Bearer", "azp": "cdse-public", "session_state": "4adfd9e9-27d2-496d-9c10-bd3a54c8f1a3", "allowed-origins": [ "https://localhost:4200", "*", "https://workspace.staging-cdse-data-explorer.apps.staging.intra.cloudferro.com", ], "realm_access": { "roles": [ "offline_access", "uma_authorization", "default-roles-cdas", "copernicus-general", ] }, "resource_access": { "account": { "roles": ["manage-account", "manage-account-links", "view-profile"] } }, "scope": "AUDIENCE_PUBLIC openid email profile user-context", "sid": "03a2986d-ccfa-45e7-8e3a-55982e7e2a6e", "group_membership": [ "/access_groups/user_typology/copernicus_general", "/organizations/default-4f0080be-2b79-4837-b6a2-7f10c2b9ee1d/regular_user", ], "email_verified": True, "organizations": ["default-4f0080be-2b79-4837-b6a2-7f10c2b9ee1d"], "name": "User Full Name", "user_context_id": "b9ab6ae1-83b0-433d-828f-e9e06adcc4a2", "context_roles": {}, "context_groups": [ "/access_groups/user_typology/copernicus_general/", "/organizations/default-4f0080be-2b79-4837-b6a2-7f10c2b9ee1d/regular_user/", ], "preferred_username": "user@example.com", "given_name": "User first name", "user_context": "default-4f0080be-2b79-4837-b6a2-7f10c2b9ee1d", "family_name": "User last name", "email": "user@example.com", } response = json.dumps( { "access_token": jwt.encode( payload, private_key, algorithm="RS256", headers=headers ), "expires_in": 600, "refresh_expires_in": 3600, "refresh_token": "check-for-refresh-token-not-implementation-yet", "token_type": "Bearer", "not-before-policy": 0, "session_state": "4adfd9e9-27d2-496d-9c10-bd3a54c8f1a3", "scope": "AUDIENCE_PUBLIC openid email profile user-context", } ) requests_mock.post( "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token", text=response, ) def mock_jwks(mocker: Any) -> None: class MockResponse: def __init__(self, json_data) -> None: self.json_data = json_data def __enter__(self) -> "MockResponse": return self def read(self) -> bytes: return json.dumps(self.json_data).encode("utf-8") def __exit__( self, exc_type: object, exc_value: object, traceback: object ) -> None: pass n = private_key.public_key().public_numbers().n e = private_key.public_key().public_numbers().e n = base64.urlsafe_b64encode( n.to_bytes((n.bit_length() + 7) // 8, byteorder="big") ).decode("utf-8") e = base64.urlsafe_b64encode( e.to_bytes((e.bit_length() + 7) // 8, byteorder="big") ).decode("utf-8") jwks = { "keys": [ { "kid": "key-9000", "kty": "RSA", "alg": "RS256", "use": "sig", "n": n, "e": e, } ] } mocker.patch("urllib.request.urlopen", return_value=MockResponse(jwks)) CDSETool-0.3.1/tests/query/000077500000000000000000000000001517266010700154035ustar00rootroot00000000000000CDSETool-0.3.1/tests/query/mock/000077500000000000000000000000001517266010700163345ustar00rootroot00000000000000CDSETool-0.3.1/tests/query/mock/sentinel_1/000077500000000000000000000000001517266010700203755ustar00rootroot00000000000000CDSETool-0.3.1/tests/query/mock/sentinel_1/README.md000066400000000000000000000021741517266010700216600ustar00rootroot00000000000000The odata test data has been fetched from Copernicus using the following query to limit the match result: query = query_features("SENTINEL-1", {"top": 10, "contentDateStartGt": "2014-04-05T00:00:00.000Z", "contentDateStartLt": "2014-04-14T00:00:00.000Z"}, options={"expand_attributes": True}) The original urls for first page (with request to get total count): https://catalogue.dataspace.copernicus.eu/odata/v1/Products?$filter=Collection/Name%20eq%20%27SENTINEL-1%27%20and%20ContentDate/Start%20gt%202014-04-05T00%3A00%3A00.000Z%20and%20ContentDate/Start%20lt%202014-04-14T00%3A00%3A00.000Z&$top=10&$count=true&$expand=Attributes&$orderby=ContentDate/Start%20asc The original URL for the second page (without request to get total count): https://catalogue.dataspace.copernicus.eu/odata/v1/Products?$filter=Collection/Name%20eq%20%27SENTINEL-1%27%20and%20ContentDate/Start%20gt%202014-04-05T00:00:00.000Z%20and%20ContentDate/Start%20lt%202014-04-14T00:00:00.000Z&$top=10&$skip=10&$expand=Attributes&$orderby=ContentDate/Start%20asc The original URL for the attributes: https://catalogue.dataspace.copernicus.eu/odata/v1/Attributes(SENTINEL-1) CDSETool-0.3.1/tests/query/mock/sentinel_1/attributes.json000066400000000000000000000041771517266010700234670ustar00rootroot00000000000000[ { "Name": "productType", "ValueType": "String" }, { "Name": "origin", "ValueType": "String" }, { "Name": "datatakeID", "ValueType": "Integer" }, { "Name": "timeliness", "ValueType": "String" }, { "Name": "coordinates", "ValueType": "String" }, { "Name": "cycleNumber", "ValueType": "Integer" }, { "Name": "orbitNumber", "ValueType": "Integer" }, { "Name": "sliceNumber", "ValueType": "Integer" }, { "Name": "totalSlices", "ValueType": "Integer" }, { "Name": "productClass", "ValueType": "String" }, { "Name": "processorName", "ValueType": "String" }, { "Name": "orbitDirection", "ValueType": "String" }, { "Name": "processingDate", "ValueType": "DateTimeOffset" }, { "Name": "operationalMode", "ValueType": "String" }, { "Name": "processingLevel", "ValueType": "String" }, { "Name": "swathIdentifier", "ValueType": "String" }, { "Name": "processingCenter", "ValueType": "String" }, { "Name": "processorVersion", "ValueType": "String" }, { "Name": "segmentStartTime", "ValueType": "DateTimeOffset" }, { "Name": "sliceProductFlag", "ValueType": "Boolean" }, { "Name": "platformShortName", "ValueType": "String" }, { "Name": "productGeneration", "ValueType": "DateTimeOffset" }, { "Name": "processingBaseline", "ValueType": "String" }, { "Name": "productComposition", "ValueType": "String" }, { "Name": "instrumentShortName", "ValueType": "String" }, { "Name": "relativeOrbitNumber", "ValueType": "Integer" }, { "Name": "polarisationChannels", "ValueType": "String" }, { "Name": "productConsolidation", "ValueType": "String" }, { "Name": "platformSerialIdentifier", "ValueType": "String" }, { "Name": "instrumentConfigurationID", "ValueType": "Integer" }, { "Name": "startTimeFromAscendingNode", "ValueType": "Double" }, { "Name": "completionTimeFromAscendingNode", "ValueType": "Double" } ] CDSETool-0.3.1/tests/query/mock/sentinel_1/odata_page_1.json000066400000000000000000000775601517266010700236130ustar00rootroot00000000000000{ "@odata.context": "$metadata#Products(Attributes())", "@odata.count": 47, "value": [ { "@odata.mediaContentType": "application/octet-stream", "Id": "67360f3a-f36b-11ef-9d3f-0050561a7772", "Name": "S1A_AUX_INS_V20140406T010000_G20140409T142540.SAFE", "ContentType": "application/octet-stream", "ContentLength": 728976, "OriginDate": "2024-10-08T18:04:08.818000Z", "PublicationDate": "2025-07-17T11:55:07.282635Z", "ModificationDate": "2025-10-10T17:56:18.034265Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_INS/2014/04/06/S1A_AUX_INS_V20140406T010000_G20140409T142540.SAFE.zip", "Checksum": [ { "Value": "23383d85c5231487ce13d1c10aeb8e7b", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:17.610209Z" }, { "Value": "6a7eacd5bd2ac39a1761270e3194302951d4db3a3ca44d0f2917b4473c45e955", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:17.631542Z" } ], "ContentDate": { "Start": "2014-04-06T01:00:00.000000Z", "End": "2014-04-06T01:00:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2014-04-09T14:25:40.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "Richmond", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2014-04-09T14:25:40.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_INS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T01:00:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T01:00:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "6a466590-9ecc-4861-ab8d-b243c328e906", "Name": "S1A_OPER_AUX_GNSSRD_POD__20171211T085403_V20140406T011619_20140406T235939", "ContentType": "application/octet-stream", "ContentLength": 2406530, "OriginDate": "2021-04-17T08:24:30.362000Z", "PublicationDate": "2023-10-30T07:06:19.506482Z", "ModificationDate": "2025-10-10T17:56:18.484437Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_GNSSRD/2014/04/06/S1A_OPER_AUX_GNSSRD_POD__20171211T085403_V20140406T011619_20140406T235939", "Checksum": [ { "Value": "15b7e274662c235eae380c40c30d02f1", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:18.076397Z" }, { "Value": "2ee0015f0935c66ac85c103853d18ac97567609311c639bf4de5ffa586dbeb80", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:18.099629Z" } ], "ContentDate": { "Start": "2014-04-06T01:16:35.000000Z", "End": "2014-04-06T23:59:55.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_GNSSRD", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T01:16:35.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T23:59:55.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "02680705-cea4-4818-800c-468e396a4009", "Name": "S1A_AUX_PP2_V20140406T133000_G20251021T105030.SAFE", "ContentType": "application/octet-stream", "ContentLength": 17275, "OriginDate": "2025-10-21T10:56:37.721000Z", "PublicationDate": "2025-10-21T10:57:08.538639Z", "ModificationDate": "2025-10-21T10:57:08.538639Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PP2/2014/04/06/S1A_AUX_PP2_V20140406T133000_G20251021T105030.SAFE.zip", "Checksum": [ { "Value": "efb620c026634f299dc63cc997bea784", "Algorithm": "MD5", "ChecksumDate": "2025-10-21T10:57:07.366955Z" }, { "Value": "f8c7bb722bf6f677c88cc576e7028f072e9cd694eb4cdf548e5ca157ddac45c0", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-21T10:57:07.392102Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2025-10-21T10:50:30.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2025-10-21T10:50:30.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PP2", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "244a637a-2edf-11ef-9e13-fa163e75e309", "Name": "S1__AUX_SCS_V20140406T133000_G20240620T081444.SAFE", "ContentType": "application/octet-stream", "ContentLength": 2330531719, "OriginDate": "2024-06-20T08:28:59.320000Z", "PublicationDate": "2024-06-20T08:30:31.114662Z", "ModificationDate": "2025-10-10T17:56:44.222622Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_SCS/2014/04/06/S1__AUX_SCS_V20140406T133000_G20240620T081444.SAFE.zip", "Checksum": [ { "Value": "0a689b1679027b9026f82109d3a0f6ac", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:13.740081Z" }, { "Value": "03fe18c0b8efade9cf24235fe5b3f1baa0e3809481993dbf722e93b669c17440", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:43.845584Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2024-06-20T08:14:44.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2024-06-20T08:14:44.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "_", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 0, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_SCS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "2e0be82c-ab33-11ef-ae3b-fa163e75e309", "Name": "S1A_AUX_PP1_V20140406T133000_G20241125T133722.SAFE", "ContentType": "application/octet-stream", "ContentLength": 30889, "OriginDate": "2024-11-25T13:42:57.597000Z", "PublicationDate": "2024-11-25T13:43:50.353146Z", "ModificationDate": "2025-10-10T17:59:13.744028Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PP1/2014/04/06/S1A_AUX_PP1_V20140406T133000_G20241125T133722.SAFE.zip", "Checksum": [ { "Value": "572fa116b39b63a8b489bfdcb63b72f4", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:59:13.326111Z" }, { "Value": "f0db344bf331239519bf7cbed9b88aa7b887df60972663129d22def9d8bad8b5", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:59:13.347578Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2024-11-25T13:37:22.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2024-11-25T13:37:22.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PP1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "44ca8f86-28c1-11ef-bb0a-fa163e75e309", "Name": "S1__AUX_SCS_V20140406T133000_G20240612T132402.SAFE", "ContentType": "application/octet-stream", "ContentLength": 2330004686, "OriginDate": "2024-06-12T13:40:01.968000Z", "PublicationDate": "2024-06-12T13:41:47.352785Z", "ModificationDate": "2025-10-10T17:57:01.351840Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_SCS/2014/04/06/S1__AUX_SCS_V20140406T133000_G20240612T132402.SAFE.zip", "Checksum": [ { "Value": "7f826324d54ba8861c85487218b5bb66", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:21.428375Z" }, { "Value": "39541b6ae9cad47d0c275d78c41dd2a7f0d969e1418ecc82f8dbe3118556280f", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:57:00.687935Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2024-06-12T13:24:02.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2024-06-12T13:24:02.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "_", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 0, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_SCS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "4f5ab1fe-f36b-11ef-8119-0050561a7772", "Name": "S1A_AUX_INS_V20140406T133000_G20150504T145310.SAFE", "ContentType": "application/octet-stream", "ContentLength": 735152, "OriginDate": "2021-03-15T16:14:09.302000Z", "PublicationDate": "2025-07-17T11:54:56.231238Z", "ModificationDate": "2025-10-10T17:56:16.871906Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_INS/2014/04/06/S1A_AUX_INS_V20140406T133000_G20150504T145310.SAFE.zip", "Checksum": [ { "Value": "2fed2fdfd06d783fbac536cbc4a18293", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:16.480070Z" }, { "Value": "b423bdcf6b36ac33ba2874a6a53282345ba96939a36c59f4906d1c06a69ee852", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:16.503918Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2015-05-04T14:53:10.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2015-05-04T14:53:10.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_INS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "52be1cd2-f36b-11ef-9bbf-0050561a7772", "Name": "S1A_AUX_INS_V20140406T133000_G20160215T161024.SAFE", "ContentType": "application/octet-stream", "ContentLength": 735586, "OriginDate": "2021-03-15T16:26:58.853000Z", "PublicationDate": "2025-07-17T11:55:06.582216Z", "ModificationDate": "2025-10-10T17:56:18.298213Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_INS/2014/04/06/S1A_AUX_INS_V20140406T133000_G20160215T161024.SAFE.zip", "Checksum": [ { "Value": "1e9016e1f2474e05aaa02932a25f65ef", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:17.850040Z" }, { "Value": "472183fcf503f7f55bffd4eb8e15bd5940d3e3456cd9d5f68fcb01e40af0fd9c", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:17.871788Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2016-02-15T16:10:24.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2016-02-15T16:10:24.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_INS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "563d4403-6601-4d77-ae55-b0efe321627a", "Name": "S1A_AUX_ML2_V20140406T133000_G20251015T074324.SAFE", "ContentType": "application/octet-stream", "ContentLength": 131112741, "OriginDate": "2025-10-15T07:45:00.552000Z", "PublicationDate": "2025-10-15T07:46:14.022427Z", "ModificationDate": "2025-10-15T07:46:14.022427Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_ML2/2014/04/06/S1A_AUX_ML2_V20140406T133000_G20251015T074324.SAFE.zip", "Checksum": [ { "Value": "57382b6ef3ba5620c55b42eca5639035", "Algorithm": "MD5", "ChecksumDate": "2025-10-15T07:46:12.378209Z" }, { "Value": "bc089a9cb31322259110e6a93a2211668efd90b43185e59e203201fc81e5650a", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-15T07:46:12.589837Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2025-10-15T07:43:24.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2025-10-15T07:43:24.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_ML2", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "56b4e0c0-ab34-11ef-91a2-fa163e75e309", "Name": "S1A_AUX_PP2_V20140406T133000_G20241125T134251.SAFE", "ContentType": "application/octet-stream", "ContentLength": 14420, "OriginDate": "2024-11-25T13:51:15.307000Z", "PublicationDate": "2024-11-25T13:51:50.821487Z", "ModificationDate": "2025-10-10T17:56:18.251027Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PP2/2014/04/06/S1A_AUX_PP2_V20140406T133000_G20241125T134251.SAFE.zip", "Checksum": [ { "Value": "0c0abaa5364be20f9161a6c25461e57b", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:17.836038Z" }, { "Value": "3dc7d9ad653f89472a145f0a71c204be8c1991a99b3536481eef1c4216949f03", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:17.862899Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2024-11-25T13:42:51.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2024-11-25T13:42:51.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PP2", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] } ], "@odata.nextLink": "https://catalogue.dataspace.copernicus.eu/odata/v1/Products?%24filter=Collection%2FName+eq+%27SENTINEL-1%27+and+ContentDate%2FStart+gt+2014-04-05T00%3A00%3A00.000Z+and+ContentDate%2FStart+lt+2014-04-14T00%3A00%3A00.000000Z&%24top=10&%24count=true&%24expand=Attributes&%24orderby=ContentDate%2FStart+asc&%24skip=10" } CDSETool-0.3.1/tests/query/mock/sentinel_1/odata_page_2.json000066400000000000000000001005201517266010700235730ustar00rootroot00000000000000{ "@odata.context": "$metadata#Products(Attributes())", "value": [ { "@odata.mediaContentType": "application/octet-stream", "Id": "57312ba8-ab34-11ef-8863-fa163e75e309", "Name": "S1A_AUX_ML2_V20140406T133000_G20241125T134854.SAFE", "ContentType": "application/octet-stream", "ContentLength": 83161714, "OriginDate": "2024-11-25T13:51:16.124000Z", "PublicationDate": "2024-11-25T13:52:01.263842Z", "ModificationDate": "2025-10-10T17:59:13.158257Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_ML2/2014/04/06/S1A_AUX_ML2_V20140406T133000_G20241125T134854.SAFE.zip", "Checksum": [ { "Value": "b6fd59ff172092cfd414972300bd1361", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:59:12.617226Z" }, { "Value": "7b00e1daf7c91dff74e13b666bb4ffc3e751109b961223611c1e03f0f9886f5c", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:59:12.758174Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2024-11-25T13:48:54.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2024-11-25T13:48:54.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_ML2", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "5ef325e2-f36b-11ef-9d94-0050561a7772", "Name": "S1A_AUX_INS_V20140406T133000_G20150319T084951.SAFE", "ContentType": "application/octet-stream", "ContentLength": 733819, "OriginDate": "2021-03-15T16:17:12.537000Z", "PublicationDate": "2025-07-17T11:55:07.247997Z", "ModificationDate": "2025-10-10T17:56:17.977432Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_INS/2014/04/06/S1A_AUX_INS_V20140406T133000_G20150319T084951.SAFE.zip", "Checksum": [ { "Value": "da0fe965101ce10c17289f403f969728", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:17.590815Z" }, { "Value": "bf62182fa7c82cdde9952d644f64fd4e32b8434ec95d4525df666c10596f96f3", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:17.614321Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2015-03-19T08:49:51.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2015-03-19T08:49:51.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_INS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "685ab2ee-f36b-11ef-af64-0050561a7772", "Name": "S1A_AUX_INS_V20140406T133000_G20180313T095009.SAFE", "ContentType": "application/octet-stream", "ContentLength": 555685, "OriginDate": "2024-10-08T18:04:09.614000Z", "PublicationDate": "2025-07-17T11:55:06.734481Z", "ModificationDate": "2025-10-10T17:56:18.088426Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_INS/2014/04/06/S1A_AUX_INS_V20140406T133000_G20180313T095009.SAFE.zip", "Checksum": [ { "Value": "15fb07d146e10913d7214e05855d6d23", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:17.690044Z" }, { "Value": "1dbae0fbc610c41ff3dfa85d42398f3a32a757401f8398cf5331f7076f1d2494", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:17.710927Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2018-03-13T09:50:09.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2018-03-13T09:50:09.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_INS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "6cf57a5a-f36b-11ef-94e2-0050561a7772", "Name": "S1A_AUX_INS_V20140406T133000_G20211028T132414.SAFE", "ContentType": "application/octet-stream", "ContentLength": 167370, "OriginDate": "2024-10-10T22:57:42.760000Z", "PublicationDate": "2025-07-17T11:55:29.527670Z", "ModificationDate": "2025-10-10T17:59:15.156668Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_INS/2014/04/06/S1A_AUX_INS_V20140406T133000_G20211028T132414.SAFE.zip", "Checksum": [ { "Value": "1c29ef7593e37a410ad1efa7ebb57821", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:59:14.747728Z" }, { "Value": "42fc73de06215b7cc27256c797ec50cf7d30e86ee66320890473c9f729f378ba", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:59:14.773468Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2021-10-28T13:24:14.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2021-10-28T13:24:14.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_INS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "756292a4-f36b-11ef-830c-0050561a7772", "Name": "S1A_AUX_INS_V20140406T133000_G20150119T143434.SAFE", "ContentType": "application/octet-stream", "ContentLength": 733882, "OriginDate": "2021-03-15T16:20:56.310000Z", "PublicationDate": "2025-07-17T11:55:07.288529Z", "ModificationDate": "2025-10-10T17:59:14.226627Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_INS/2014/04/06/S1A_AUX_INS_V20140406T133000_G20150119T143434.SAFE.zip", "Checksum": [ { "Value": "62c59e74a894ebde0daf6def827565c4", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:59:13.831636Z" }, { "Value": "dee9c39ed39066707bf698981c151b2d48ea81df6331ab5b58a0f23070318dde", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:59:13.853640Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2015-01-19T14:34:34.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2015-01-19T14:34:34.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_INS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "9299c9c6-ab33-11ef-9f13-fa163e75e309", "Name": "S1__AUX_SCS_V20140406T133000_G20241125T132817.SAFE", "ContentType": "application/octet-stream", "ContentLength": 2356954125, "OriginDate": "2024-11-25T13:45:46.299000Z", "PublicationDate": "2024-11-25T13:47:34.541894Z", "ModificationDate": "2025-10-10T17:56:52.168879Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_SCS/2014/04/06/S1__AUX_SCS_V20140406T133000_G20241125T132817.SAFE.zip", "Checksum": [ { "Value": "3f412eaa02ae00d1a8a6c88112c83002", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:21.294963Z" }, { "Value": "103eaebbc68194e3a9f7c8c79e164157b202656f54256e68dd15ce9efb7adbf1", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:51.772207Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2024-11-25T13:28:17.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2024-11-25T13:28:17.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "_", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 0, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_SCS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "a0d5f506-0146-11ef-aae1-fa163e7968e5", "Name": "S1A_AUX_PP1_V20140406T133000_G20240423T074732.SAFE", "ContentType": "application/octet-stream", "ContentLength": 30149, "OriginDate": "2024-04-23T07:53:52.944000Z", "PublicationDate": "2024-04-23T07:54:16.262531Z", "ModificationDate": "2025-10-10T17:59:12.598809Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PP1/2014/04/06/S1A_AUX_PP1_V20140406T133000_G20240423T074732.SAFE.zip", "Checksum": [ { "Value": "c156c81d7494313d2c8297b58d4acd8a", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:59:12.160655Z" }, { "Value": "e7aceb9afc11c4a78af23055f455c90e278618acec07d2cebc282d0cc4fe843d", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:59:12.182110Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2024-04-23T07:47:32.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PP1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "a88b1868-7a17-4384-8c48-ca25dceac520", "Name": "S1A_AUX_PP2_V20140406T133000_G20251024T110615.SAFE", "ContentType": "application/octet-stream", "ContentLength": 17242, "OriginDate": "2025-10-24T11:08:36.026000Z", "PublicationDate": "2025-10-24T11:09:48.427007Z", "ModificationDate": "2025-10-24T11:09:48.427007Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PP2/2014/04/06/S1A_AUX_PP2_V20140406T133000_G20251024T110615.SAFE.zip", "Checksum": [ { "Value": "e53691747a885b4bae37c958595db758", "Algorithm": "MD5", "ChecksumDate": "2025-10-24T11:09:46.922638Z" }, { "Value": "da2bf71a1184f307cddbd06f36829177190686059eb54aedc0a772499a2366cb", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-24T11:09:46.945487Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2025-10-24T11:06:15.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2025-10-24T11:06:15.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PP2", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "b5527ce4-28be-11ef-98e6-fa163e75e309", "Name": "S1A_AUX_PP2_V20140406T133000_G20240612T131427.SAFE", "ContentType": "application/octet-stream", "ContentLength": 13507, "OriginDate": "2024-06-12T13:21:42.265000Z", "PublicationDate": "2024-06-12T13:22:45.761158Z", "ModificationDate": "2025-10-10T17:56:18.337925Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PP2/2014/04/06/S1A_AUX_PP2_V20140406T133000_G20240612T131427.SAFE.zip", "Checksum": [ { "Value": "afe0239d5fb4683f9cee00071986f958", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:17.975725Z" }, { "Value": "98dfee9cd8b3e84ed882da7ba050a3f86a6a175505f049f43a9c99554cb4d591", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:17.996611Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2024-06-12T13:14:27.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2024-06-12T13:14:27.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PP2", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "d9a995f0-0609-11ef-84b6-fa163e7968e5", "Name": "S1A_AUX_ML2_V20140406T133000_G20240429T091835.SAFE", "ContentType": "application/octet-stream", "ContentLength": 91103686, "OriginDate": "2024-04-29T09:21:24.800000Z", "PublicationDate": "2024-05-09T11:56:33.892049Z", "ModificationDate": "2025-10-10T17:59:13.016276Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_ML2/2014/04/06/S1A_AUX_ML2_V20140406T133000_G20240429T091835.SAFE.zip", "Checksum": [ { "Value": "6fb6020b798024d83787293ebcd9c32f", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:59:12.291218Z" }, { "Value": "d699a8bb269f75f68441c544a0936d15224948f33ac0908c1b68e3255433cff3", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:59:12.436223Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2024-04-29T09:18:35.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2024-04-29T09:18:35.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_ML2", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] } ], "@odata.nextLink": "https://catalogue.dataspace.copernicus.eu/odata/v1/Products?%24filter=Collection%2FName+eq+%27SENTINEL-1%27+and+ContentDate%2FStart+gt+2014-04-05T00%3A00%3A00.000Z+and+ContentDate%2FStart+lt+2014-04-14T00%3A00%3A00.000000Z&%24top=10&%24skip=20&%24expand=Attributes&%24orderby=ContentDate%2FStart+asc" } CDSETool-0.3.1/tests/query/mock/sentinel_1/odata_page_3.json000066400000000000000000000704231517266010700236040ustar00rootroot00000000000000{ "@odata.context": "$metadata#Products(Attributes())", "value": [ { "@odata.mediaContentType": "application/octet-stream", "Id": "e61ffcb8-7e44-49bb-a04a-05d749041c68", "Name": "S1A_AUX_INS_V20140406T133000_G20251015T074233.SAFE", "ContentType": "application/octet-stream", "ContentLength": 553396, "OriginDate": "2025-10-15T07:44:58.416000Z", "PublicationDate": "2025-10-15T07:46:02.825578Z", "ModificationDate": "2025-10-15T07:46:02.825578Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_INS/2014/04/06/S1A_AUX_INS_V20140406T133000_G20251015T074233.SAFE.zip", "Checksum": [ { "Value": "bed9edbe1b81da2e2e0684120217433e", "Algorithm": "MD5", "ChecksumDate": "2025-10-15T07:46:01.534169Z" }, { "Value": "76b4b2706183f51e4900810523b8142c2b322ffd461d94122673952b9c31a94a", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-15T07:46:01.575434Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2025-10-15T07:42:33.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2025-10-15T07:42:33.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_INS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "e903ea9f-e42d-40df-9bbd-59cd31fb6ab1", "Name": "S1A_AUX_PP2_V20140406T133000_G20251216T113339.SAFE", "ContentType": "application/octet-stream", "ContentLength": 17297, "OriginDate": "2025-12-16T11:42:22.001000Z", "PublicationDate": "2025-12-16T11:43:26.258403Z", "ModificationDate": "2025-12-16T11:43:26.258403Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PP2/2014/04/06/S1A_AUX_PP2_V20140406T133000_G20251216T113339.SAFE.zip", "Checksum": [ { "Value": "150a486092ad36a194dfe3ed32f0237b", "Algorithm": "MD5", "ChecksumDate": "2025-12-16T11:43:25.006981Z" }, { "Value": "b62180e431db8808b919850e2cbe067e708d5abee5f2db8f980bdea4ed6f6835", "Algorithm": "BLAKE3", "ChecksumDate": "2025-12-16T11:43:25.030172Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2025-12-16T11:33:39.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2025-12-16T11:33:39.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PP2", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "ee3eff40-ca15-42d9-a045-1f7d835d4c72", "Name": "S1A_AUX_PP2_V20140406T133000_G20251015T074259.SAFE", "ContentType": "application/octet-stream", "ContentLength": 17544, "OriginDate": "2025-10-15T07:44:57.992000Z", "PublicationDate": "2025-10-15T07:45:41.326620Z", "ModificationDate": "2025-10-15T07:45:41.326620Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PP2/2014/04/06/S1A_AUX_PP2_V20140406T133000_G20251015T074259.SAFE.zip", "Checksum": [ { "Value": "419c8d2b1c686db3d7d54f40e6cf54ef", "Algorithm": "MD5", "ChecksumDate": "2025-10-15T07:45:39.985502Z" }, { "Value": "a807d2c78e8a80b4a6e1802ac66d48e995d88777c7cda524d4b7ac44196a0b6b", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-15T07:45:40.006949Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2025-10-15T07:42:59.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2025-10-15T07:42:59.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PP2", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "f6e7cdf6-318e-11f0-810a-0050561a7772", "Name": "S1A_AUX_PP1_V20140406T133000_G20250515T131317.SAFE", "ContentType": "application/octet-stream", "ContentLength": 31129, "OriginDate": "2025-05-15T13:17:34.598000Z", "PublicationDate": "2025-05-15T13:18:54.066400Z", "ModificationDate": "2025-10-10T17:56:18.203213Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PP1/2014/04/06/S1A_AUX_PP1_V20140406T133000_G20250515T131317.SAFE.zip", "Checksum": [ { "Value": "ea10c6c9c5dbef94944d816134535994", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:17.786526Z" }, { "Value": "07099f563e85e8c5da6f3b157fb6795249fe2e54afbce662274e083e1bf2c9ab", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:17.807910Z" } ], "ContentDate": { "Start": "2014-04-06T13:30:00.000000Z", "End": "2014-04-06T13:30:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2025-05-15T13:13:17.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "CLS-Brest", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2025-05-15T13:13:17.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PP1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-06T13:30:00.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "946aec7f-5fc2-48d3-9aa2-c45e058e96de", "Name": "S1A_OPER_AUX_POEORB_OPOD_20210301T130653_V20140406T225944_20140408T005944.EOF", "ContentType": "application/octet-stream", "ContentLength": 4417447, "OriginDate": "2021-04-17T10:37:29.692000Z", "PublicationDate": "2023-10-30T07:06:24.329080Z", "ModificationDate": "2025-10-10T17:56:11.517769Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_POEORB/2014/04/06/S1A_OPER_AUX_POEORB_OPOD_20210301T130653_V20140406T225944_20140408T005944.EOF", "Checksum": [ { "Value": "23654fd7eba7527e760b3e8b137ffb96", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:11.132134Z" }, { "Value": "0d75edc6c174a0f6baf04d61b61ffe90fcb9bb6a8669876dfb3ab7e02316f924", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:11.159561Z" } ], "ContentDate": { "Start": "2014-04-06T22:59:44.000000Z", "End": "2014-04-08T00:59:44.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_POEORB", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-06T22:59:44.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-08T00:59:44.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "6de8489e-f6c3-4f28-8599-6d30bf70d7e3", "Name": "S1A_OPER_AUX_PROQUA_POD__20210114T081132_V20140406T235944_20140407T235943", "ContentType": "application/octet-stream", "ContentLength": 1571505, "OriginDate": "2021-04-17T10:17:24.941000Z", "PublicationDate": "2023-10-30T07:06:22.609818Z", "ModificationDate": "2025-10-10T17:56:04.276450Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PROQUA/2014/04/07/S1A_OPER_AUX_PROQUA_POD__20210114T081132_V20140406T235944_20140407T235943", "Checksum": [ { "Value": "df21f45e7b766f54641e663c20dbe36f", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:03.833818Z" }, { "Value": "4cec6c7e564e3e1a6f9ba2c0ad1b2f5b2b9c374d18a6b2efa7a8c80b4833fe83", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:03.856809Z" } ], "ContentDate": { "Start": "2014-04-07T00:00:00.000000Z", "End": "2014-04-07T23:59:59.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PROQUA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-07T00:00:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-07T23:59:59.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "363352bf-8b26-4d6a-81e1-dc320231e267", "Name": "S1A_OPER_AUX_GNSSRD_POD__20171211T085448_V20140406T235949_20140407T235939", "ContentType": "application/octet-stream", "ContentLength": 2170631, "OriginDate": "2021-04-17T08:24:11.849000Z", "PublicationDate": "2023-10-30T07:06:34.283483Z", "ModificationDate": "2025-10-10T17:56:18.888084Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_GNSSRD/2014/04/07/S1A_OPER_AUX_GNSSRD_POD__20171211T085448_V20140406T235949_20140407T235939", "Checksum": [ { "Value": "254025351e8148c7b350c34b7a105c36", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:18.492636Z" }, { "Value": "bbb0ae1116af56cd3786164cff9acfd2b786adba91c86b0e2385fdd1424183bc", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:18.519127Z" } ], "ContentDate": { "Start": "2014-04-07T00:00:05.000000Z", "End": "2014-04-07T23:59:55.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_GNSSRD", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-07T00:00:05.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-07T23:59:55.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "b10c5ae4-aaa4-4b76-a86d-84fc72415975", "Name": "S1A_OPER_AUX_POEORB_OPOD_20210301T132532_V20140407T225944_20140409T005944.EOF", "ContentType": "application/octet-stream", "ContentLength": 4409618, "OriginDate": "2021-04-17T10:37:28.491000Z", "PublicationDate": "2023-10-30T07:06:27.103197Z", "ModificationDate": "2025-10-10T17:56:11.721088Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_POEORB/2014/04/07/S1A_OPER_AUX_POEORB_OPOD_20210301T132532_V20140407T225944_20140409T005944.EOF", "Checksum": [ { "Value": "332333eed80b84d1a475d8815fc6f6ff", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:11.257176Z" }, { "Value": "7c57911f29caf5ee1c58dd531b7c90ee28766d3f1a9e8e405b4f73f09d3f2917", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:11.281675Z" } ], "ContentDate": { "Start": "2014-04-07T22:59:44.000000Z", "End": "2014-04-09T00:59:44.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_POEORB", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-07T22:59:44.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-09T00:59:44.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "74edb571-d47d-41bb-bb2f-ea33ffc3badc", "Name": "S1A_OPER_AUX_PROQUA_POD__20210114T081857_V20140407T235944_20140408T235943", "ContentType": "application/octet-stream", "ContentLength": 1567071, "OriginDate": "2021-04-17T10:17:24.775000Z", "PublicationDate": "2023-10-30T07:06:37.910374Z", "ModificationDate": "2025-10-10T17:56:04.499148Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PROQUA/2014/04/08/S1A_OPER_AUX_PROQUA_POD__20210114T081857_V20140407T235944_20140408T235943", "Checksum": [ { "Value": "767948d99ec0a6ce783941260a50e0a7", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:04.106528Z" }, { "Value": "b60739e91e8b13117cd32ae8dfa20954b69fb57423c8a7b7bbc00c30cdacc514", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:04.131324Z" } ], "ContentDate": { "Start": "2014-04-08T00:00:00.000000Z", "End": "2014-04-08T23:59:59.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PROQUA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-08T00:00:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-08T23:59:59.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "bcdc573d-4dc2-44a5-bf2d-768a44d1f448", "Name": "S1A_OPER_AUX_GNSSRD_POD__20171211T085538_V20140407T235949_20140408T235939", "ContentType": "application/octet-stream", "ContentLength": 2660580, "OriginDate": "2021-04-17T08:24:06.339000Z", "PublicationDate": "2023-10-30T07:06:29.541784Z", "ModificationDate": "2025-10-10T17:56:18.878235Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_GNSSRD/2014/04/08/S1A_OPER_AUX_GNSSRD_POD__20171211T085538_V20140407T235949_20140408T235939", "Checksum": [ { "Value": "7baf50adb404fecf8cdd928047ea797b", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:18.468621Z" }, { "Value": "bf007fbc729c1f2a7dd82447e3e0dcdbfbc483d2999f14fdf9c57ea1818296ba", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:18.492003Z" } ], "ContentDate": { "Start": "2014-04-08T00:00:05.000000Z", "End": "2014-04-08T23:59:55.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_GNSSRD", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-08T00:00:05.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-08T23:59:55.000000Z", "ValueType": "DateTimeOffset" } ] } ], "@odata.nextLink": "https://catalogue.dataspace.copernicus.eu/odata/v1/Products?%24filter=Collection%2FName+eq+%27SENTINEL-1%27+and+ContentDate%2FStart+gt+2014-04-05T00%3A00%3A00.000Z+and+ContentDate%2FStart+lt+2014-04-14T00%3A00%3A00.000000Z&%24top=10&%24skip=30&%24expand=Attributes&%24orderby=ContentDate%2FStart+asc" } CDSETool-0.3.1/tests/query/mock/sentinel_1/odata_page_4.json000066400000000000000000000642141517266010700236060ustar00rootroot00000000000000{ "@odata.context": "$metadata#Products(Attributes())", "value": [ { "@odata.mediaContentType": "application/octet-stream", "Id": "2d6d706a-5048-4bd7-b4b3-98b2e018073e", "Name": "S1A_OPER_AUX_POEORB_OPOD_20210301T134450_V20140408T225944_20140410T005944.EOF", "ContentType": "application/octet-stream", "ContentLength": 4409836, "OriginDate": "2021-04-17T10:37:31.990000Z", "PublicationDate": "2023-10-30T07:06:35.268497Z", "ModificationDate": "2025-10-10T17:56:16.701434Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_POEORB/2014/04/08/S1A_OPER_AUX_POEORB_OPOD_20210301T134450_V20140408T225944_20140410T005944.EOF", "Checksum": [ { "Value": "dce236f8c1eeff55470fb499e9930118", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:16.252710Z" }, { "Value": "762bc17d4ffe8aed82bb1141cce50071eeb9746b89e6fdba8077c5f2b764743b", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:16.279649Z" } ], "ContentDate": { "Start": "2014-04-08T22:59:44.000000Z", "End": "2014-04-10T00:59:44.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_POEORB", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-08T22:59:44.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-10T00:59:44.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "0d24d9eb-afed-4874-9aa7-d617fe324a01", "Name": "S1A_OPER_AUX_PROQUA_POD__20210408T164738_V20140408T235944_20140409T235943", "ContentType": "application/octet-stream", "ContentLength": 1568775, "OriginDate": "2021-04-17T11:01:12.677000Z", "PublicationDate": "2023-10-30T07:06:19.982966Z", "ModificationDate": "2025-10-10T17:56:03.487802Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PROQUA/2014/04/09/S1A_OPER_AUX_PROQUA_POD__20210408T164738_V20140408T235944_20140409T235943", "Checksum": [ { "Value": "7170e43bee6ef02b034cf31149c81d92", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:03.067459Z" }, { "Value": "90e555d4acc5b4202d46e2e12816cf364cf1d02f1970d7bc98ab61b4dd73a0fb", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:03.089902Z" } ], "ContentDate": { "Start": "2014-04-09T00:00:00.000000Z", "End": "2014-04-09T23:59:59.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PROQUA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-09T00:00:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-09T23:59:59.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "de1cd159-1b6d-406c-b428-84b761ba81f1", "Name": "S1A_OPER_AUX_GNSSRD_POD__20171211T085627_V20140408T235949_20140409T235939", "ContentType": "application/octet-stream", "ContentLength": 2655307, "OriginDate": "2021-04-17T08:24:19.077000Z", "PublicationDate": "2023-10-30T07:06:39.406862Z", "ModificationDate": "2025-10-10T17:56:16.952514Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_GNSSRD/2014/04/09/S1A_OPER_AUX_GNSSRD_POD__20171211T085627_V20140408T235949_20140409T235939", "Checksum": [ { "Value": "931e0daa26f5a537f66a3579d70e36c5", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:16.436663Z" }, { "Value": "c2b2a969a9114d113d1f6775a75ea0b42fbfc078a0d5e236707b394eb4bc84f8", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:16.459328Z" } ], "ContentDate": { "Start": "2014-04-09T00:00:05.000000Z", "End": "2014-04-09T23:59:55.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_GNSSRD", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-09T00:00:05.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-09T23:59:55.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "91a1d354-ee59-41b3-a810-bfb78148ec2d", "Name": "S1A_OPER_AUX_POEORB_OPOD_20210301T140441_V20140409T225944_20140411T005944.EOF", "ContentType": "application/octet-stream", "ContentLength": 4410192, "OriginDate": "2021-04-17T10:37:34.585000Z", "PublicationDate": "2023-10-30T07:08:37.769948Z", "ModificationDate": "2025-10-10T17:56:16.670349Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_POEORB/2014/04/09/S1A_OPER_AUX_POEORB_OPOD_20210301T140441_V20140409T225944_20140411T005944.EOF", "Checksum": [ { "Value": "53d1dbcf5a8aa2e73a5fc07513611856", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:16.260133Z" }, { "Value": "e01d812fa98e39164624a563385ca45f00097f161aeb8318388800588e158a74", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:16.289118Z" } ], "ContentDate": { "Start": "2014-04-09T22:59:44.000000Z", "End": "2014-04-11T00:59:44.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_POEORB", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-09T22:59:44.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-11T00:59:44.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "064a3fde-401b-4afa-aae4-632d65d0dc6e", "Name": "S1A_OPER_AUX_PROQUA_POD__20210408T165229_V20140409T235944_20140410T235943", "ContentType": "application/octet-stream", "ContentLength": 1570805, "OriginDate": "2021-04-17T11:02:02.359000Z", "PublicationDate": "2023-10-30T07:06:21.168922Z", "ModificationDate": "2025-10-10T17:56:03.576919Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PROQUA/2014/04/10/S1A_OPER_AUX_PROQUA_POD__20210408T165229_V20140409T235944_20140410T235943", "Checksum": [ { "Value": "c84cd62f08317a2a302a58e8df89b1ba", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:03.176715Z" }, { "Value": "0d42a40022ba6703687503513ac6ee60aefe07f02faf650c15deeb77643831e6", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:03.200325Z" } ], "ContentDate": { "Start": "2014-04-10T00:00:00.000000Z", "End": "2014-04-10T23:59:59.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PROQUA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-10T00:00:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-10T23:59:59.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "82748a14-4e00-4157-b1ff-07618d4c220e", "Name": "S1A_OPER_AUX_GNSSRD_POD__20171211T085718_V20140409T235949_20140410T235939", "ContentType": "application/octet-stream", "ContentLength": 2655218, "OriginDate": "2021-04-17T08:24:21.067000Z", "PublicationDate": "2023-10-30T07:06:36.226339Z", "ModificationDate": "2025-10-10T17:56:19.106974Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_GNSSRD/2014/04/10/S1A_OPER_AUX_GNSSRD_POD__20171211T085718_V20140409T235949_20140410T235939", "Checksum": [ { "Value": "7481c050de2ddbaf608df0c0a7b829ee", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:18.746638Z" }, { "Value": "6068554592cd4d70d9ec6a5f00fbe8147694a32ac73d84ddba8ac49b5e68e6ab", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:18.771380Z" } ], "ContentDate": { "Start": "2014-04-10T00:00:05.000000Z", "End": "2014-04-10T23:59:55.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_GNSSRD", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-10T00:00:05.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-10T23:59:55.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "9de6fddb-e6ee-4b89-b207-3cb8706b6252", "Name": "S1A_OPER_AUX_POEORB_OPOD_20210301T142346_V20140410T225944_20140412T005944.EOF", "ContentType": "application/octet-stream", "ContentLength": 4409856, "OriginDate": "2021-04-17T10:37:35.112000Z", "PublicationDate": "2023-10-30T07:06:30.759241Z", "ModificationDate": "2025-10-10T17:56:10.759086Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_POEORB/2014/04/10/S1A_OPER_AUX_POEORB_OPOD_20210301T142346_V20140410T225944_20140412T005944.EOF", "Checksum": [ { "Value": "9d772f7d1a336481e15b6fcbcd1bfa9d", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:10.311697Z" }, { "Value": "ed9ca5ea6851cb6eb25e8406df8271bea6b383b22acf9e26a3f779c1273f4716", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:10.336358Z" } ], "ContentDate": { "Start": "2014-04-10T22:59:44.000000Z", "End": "2014-04-12T00:59:44.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_POEORB", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-10T22:59:44.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-12T00:59:44.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "e6cf0473-cf59-42b9-9965-db05a77efa53", "Name": "S1A_OPER_AUX_PROQUA_POD__20210408T165710_V20140410T235944_20140411T235943", "ContentType": "application/octet-stream", "ContentLength": 1568818, "OriginDate": "2021-04-17T11:01:41.688000Z", "PublicationDate": "2023-10-30T07:08:21.491203Z", "ModificationDate": "2025-10-10T17:56:03.292591Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PROQUA/2014/04/11/S1A_OPER_AUX_PROQUA_POD__20210408T165710_V20140410T235944_20140411T235943", "Checksum": [ { "Value": "a4d37736a15604f6be05c5a7146af9e0", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:02.783240Z" }, { "Value": "e9c27b8ff6ef1479dd5174d2f9f188110f4cb5e7045b5d7e409de8e73efb752f", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:02.809670Z" } ], "ContentDate": { "Start": "2014-04-11T00:00:00.000000Z", "End": "2014-04-11T23:59:59.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PROQUA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-11T00:00:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-11T23:59:59.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "15d1584c-a759-4246-81c7-9d5e862c078e", "Name": "S1A_OPER_AUX_GNSSRD_POD__20171211T085810_V20140410T235949_20140411T235939", "ContentType": "application/octet-stream", "ContentLength": 2635171, "OriginDate": "2021-04-17T08:24:22.319000Z", "PublicationDate": "2023-10-30T07:08:42.624519Z", "ModificationDate": "2025-10-10T17:56:19.938050Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_GNSSRD/2014/04/11/S1A_OPER_AUX_GNSSRD_POD__20171211T085810_V20140410T235949_20140411T235939", "Checksum": [ { "Value": "b630078660fbddf683b98b95507d2c72", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:19.482888Z" }, { "Value": "a9e38085581715b58baa163cbe0eb7922a97f198e9cc898fd0338a16ff649caf", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:19.507231Z" } ], "ContentDate": { "Start": "2014-04-11T00:00:05.000000Z", "End": "2014-04-11T23:59:55.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_GNSSRD", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-11T00:00:05.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-11T23:59:55.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "74c63558-f36b-11ef-bbb2-0050561a7772", "Name": "S1A_AUX_INS_V20140411T000100_G20140428T135940.SAFE", "ContentType": "application/octet-stream", "ContentLength": 728977, "OriginDate": "2024-11-26T07:49:08.411000Z", "PublicationDate": "2025-07-17T11:55:31.576725Z", "ModificationDate": "2025-10-10T17:59:14.982247Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_INS/2014/04/11/S1A_AUX_INS_V20140411T000100_G20140428T135940.SAFE.zip", "Checksum": [ { "Value": "37eb049d8bb06c80431a83b0f5a076a3", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:59:14.583820Z" }, { "Value": "ac3f083e6b99fce9173dccd1d297acc3937a3f33de1f16d04030026d29fda7a4", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:59:14.608527Z" } ], "ContentDate": { "Start": "2014-04-11T00:01:00.000000Z", "End": "2014-04-11T00:01:00.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "processingDate", "Value": "2014-04-28T13:59:40.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "processingCenter", "Value": "Richmond", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "productGeneration", "Value": "2014-04-28T13:59:40.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "AUX", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.IntegerAttribute", "Name": "instrumentConfigurationID", "Value": 1, "ValueType": "Integer" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_INS", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-11T00:01:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-11T00:01:00.000000Z", "ValueType": "DateTimeOffset" } ] } ], "@odata.nextLink": "https://catalogue.dataspace.copernicus.eu/odata/v1/Products?%24filter=Collection%2FName+eq+%27SENTINEL-1%27+and+ContentDate%2FStart+gt+2014-04-05T00%3A00%3A00.000Z+and+ContentDate%2FStart+lt+2014-04-14T00%3A00%3A00.000000Z&%24top=10&%24skip=40&%24expand=Attributes&%24orderby=ContentDate%2FStart+asc" } CDSETool-0.3.1/tests/query/mock/sentinel_1/odata_page_5.json000066400000000000000000000431531517266010700236060ustar00rootroot00000000000000{ "@odata.context": "$metadata#Products(Attributes())", "value": [ { "@odata.mediaContentType": "application/octet-stream", "Id": "31e61f4f-20ec-4ae6-929d-73690998751c", "Name": "S1A_OPER_AUX_POEORB_OPOD_20210301T144249_V20140411T225944_20140413T005944.EOF", "ContentType": "application/octet-stream", "ContentLength": 4409583, "OriginDate": "2021-04-17T10:37:28.842000Z", "PublicationDate": "2023-10-30T07:08:35.200348Z", "ModificationDate": "2025-10-10T17:56:16.712708Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_POEORB/2014/04/11/S1A_OPER_AUX_POEORB_OPOD_20210301T144249_V20140411T225944_20140413T005944.EOF", "Checksum": [ { "Value": "d470c043abf8496fb91f8d16734d2c69", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:16.265154Z" }, { "Value": "5aa9d47ca94c4bdbb7ba0298ac40ce2cd8c6515af176a1ee2bdc5ad481fb21d8", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:16.291117Z" } ], "ContentDate": { "Start": "2014-04-11T22:59:44.000000Z", "End": "2014-04-13T00:59:44.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_POEORB", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-11T22:59:44.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-13T00:59:44.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "44744965-3f71-43cd-947c-d40e79b77ca1", "Name": "S1A_OPER_AUX_PROQUA_POD__20210408T170200_V20140411T235944_20140412T235943", "ContentType": "application/octet-stream", "ContentLength": 1567311, "OriginDate": "2021-04-17T11:02:00.923000Z", "PublicationDate": "2023-10-30T07:06:16.366568Z", "ModificationDate": "2025-10-10T17:56:03.372880Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PROQUA/2014/04/12/S1A_OPER_AUX_PROQUA_POD__20210408T170200_V20140411T235944_20140412T235943", "Checksum": [ { "Value": "61d09b7c4efbe8a63c88e6e30a732be0", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:02.858851Z" }, { "Value": "28b3d4f32fed814267c92152935c336a82a94ac7628a53454310dc6bcff77e1f", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:02.881030Z" } ], "ContentDate": { "Start": "2014-04-12T00:00:00.000000Z", "End": "2014-04-12T23:59:59.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PROQUA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-12T00:00:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-12T23:59:59.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "a0f8d8ac-2312-4baa-b749-7fa033023630", "Name": "S1A_OPER_AUX_GNSSRD_POD__20171211T085859_V20140411T235949_20140412T235939", "ContentType": "application/octet-stream", "ContentLength": 2650762, "OriginDate": "2021-04-17T08:24:06.802000Z", "PublicationDate": "2023-10-30T07:08:28.610665Z", "ModificationDate": "2025-10-10T17:56:19.846103Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_GNSSRD/2014/04/12/S1A_OPER_AUX_GNSSRD_POD__20171211T085859_V20140411T235949_20140412T235939", "Checksum": [ { "Value": "2484b422fd7fce255f3b705a89de4f1e", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:19.422932Z" }, { "Value": "99e15c2a7c4435fdfb92c182645cc4dadbbba0492b0384f10a1c54fea4afdfc4", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:19.448880Z" } ], "ContentDate": { "Start": "2014-04-12T00:00:05.000000Z", "End": "2014-04-12T23:59:55.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_GNSSRD", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-12T00:00:05.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-12T23:59:55.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "d05bf83a-4cde-44ab-ab5f-3dd2f468d97b", "Name": "S1A_OPER_AUX_POEORB_OPOD_20210301T150153_V20140412T225944_20140414T005944.EOF", "ContentType": "application/octet-stream", "ContentLength": 4409571, "OriginDate": "2021-04-17T10:37:41.054000Z", "PublicationDate": "2023-10-30T07:06:29.015116Z", "ModificationDate": "2025-10-10T17:56:11.186319Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_POEORB/2014/04/12/S1A_OPER_AUX_POEORB_OPOD_20210301T150153_V20140412T225944_20140414T005944.EOF", "Checksum": [ { "Value": "238393eeebc8e9e9b834211a0135e190", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:10.395618Z" }, { "Value": "f564ee5ff3baf1451be15c7decb7a89d62336f65165c78163f7abd02b8f632e1", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:10.422372Z" } ], "ContentDate": { "Start": "2014-04-12T22:59:44.000000Z", "End": "2014-04-14T00:59:44.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_POEORB", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-12T22:59:44.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-14T00:59:44.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "e108d33a-867b-41c0-ac7b-a1c55d2a3d1b", "Name": "S1A_OPER_AUX_PROQUA_POD__20210408T170639_V20140412T235944_20140413T235943", "ContentType": "application/octet-stream", "ContentLength": 1569653, "OriginDate": "2021-04-17T11:01:59.095000Z", "PublicationDate": "2023-10-30T07:08:40.107246Z", "ModificationDate": "2025-10-10T17:56:03.620937Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_PROQUA/2014/04/13/S1A_OPER_AUX_PROQUA_POD__20210408T170639_V20140412T235944_20140413T235943", "Checksum": [ { "Value": "a213f20b605cb8ed6b141823a03514b4", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:03.258211Z" }, { "Value": "d55f61a742d783a9548ff25c0901354ddd14ac702346ea38b1ec2316c9c6dd6a", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:03.281649Z" } ], "ContentDate": { "Start": "2014-04-13T00:00:00.000000Z", "End": "2014-04-13T23:59:59.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_PROQUA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-13T00:00:00.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-13T23:59:59.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "3422ecfa-c628-47c5-8b4f-d3379b69af40", "Name": "S1A_OPER_AUX_GNSSRD_POD__20171211T085957_V20140412T235949_20140413T235939", "ContentType": "application/octet-stream", "ContentLength": 2668526, "OriginDate": "2021-04-17T08:25:07.822000Z", "PublicationDate": "2023-10-30T07:06:27.998752Z", "ModificationDate": "2025-10-10T17:56:19.443200Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_GNSSRD/2014/04/13/S1A_OPER_AUX_GNSSRD_POD__20171211T085957_V20140412T235949_20140413T235939", "Checksum": [ { "Value": "886aa4f23e97366899ea3fee76a8ee5a", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:19.075582Z" }, { "Value": "fe483270251d3c217ee4e71e8544404734568c3af02b113fbed4a58f90343854", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:19.100523Z" } ], "ContentDate": { "Start": "2014-04-13T00:00:05.000000Z", "End": "2014-04-13T23:59:55.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_GNSSRD", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-13T00:00:05.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-13T23:59:55.000000Z", "ValueType": "DateTimeOffset" } ] }, { "@odata.mediaContentType": "application/octet-stream", "Id": "111b5209-3f9b-4695-ac93-4c9174960ebf", "Name": "S1A_OPER_AUX_POEORB_OPOD_20210301T152129_V20140413T225944_20140415T005944.EOF", "ContentType": "application/octet-stream", "ContentLength": 4409867, "OriginDate": "2021-04-17T10:38:02.930000Z", "PublicationDate": "2023-10-30T07:08:51.100816Z", "ModificationDate": "2025-10-10T17:56:10.294138Z", "Online": true, "EvictionDate": "9999-12-31T23:59:59.999999Z", "S3Path": "/eodata/Sentinel-1/AUX/AUX_POEORB/2014/04/13/S1A_OPER_AUX_POEORB_OPOD_20210301T152129_V20140413T225944_20140415T005944.EOF", "Checksum": [ { "Value": "3b95e72684160d5a9c48d1c4e78b7cc3", "Algorithm": "MD5", "ChecksumDate": "2025-10-10T17:56:09.862765Z" }, { "Value": "c6834f52221344328b99e1b0767bc372eb7fb3fd033c00abf1611bd58f8a6267", "Algorithm": "BLAKE3", "ChecksumDate": "2025-10-10T17:56:09.887826Z" } ], "ContentDate": { "Start": "2014-04-13T22:59:44.000000Z", "End": "2014-04-15T00:59:44.000000Z" }, "Footprint": null, "GeoFootprint": null, "Attributes": [ { "@odata.type": "#OData.CSC.StringAttribute", "Name": "origin", "Value": "ESA", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformShortName", "Value": "SENTINEL-1", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "instrumentShortName", "Value": "SAR", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "platformSerialIdentifier", "Value": "A", "ValueType": "String" }, { "@odata.type": "#OData.CSC.StringAttribute", "Name": "productType", "Value": "AUX_POEORB", "ValueType": "String" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "beginningDateTime", "Value": "2014-04-13T22:59:44.000000Z", "ValueType": "DateTimeOffset" }, { "@odata.type": "#OData.CSC.DateTimeOffsetAttribute", "Name": "endingDateTime", "Value": "2014-04-15T00:59:44.000000Z", "ValueType": "DateTimeOffset" } ] } ] } CDSETool-0.3.1/tests/query/query_features_test.py000066400000000000000000000063471517266010700220710ustar00rootroot00000000000000"""Tests for CDSETool's query_features module.""" import json from typing import Any from cdsetool.query import _strip_odata_count, query_features def _mock_describe(requests_mock: Any) -> None: url = "https://catalogue.dataspace.copernicus.eu/odata/v1/Attributes(SENTINEL-1)" with open( "tests/query/mock/sentinel_1/attributes.json", "r", encoding="utf-8" ) as file: requests_mock.get(url, text=file.read()) def _mock_sentinel_1(requests_mock: Any) -> None: """Mock OData API responses for SENTINEL-1 queries""" filenames = [ "odata_page_1.json", "odata_page_2.json", "odata_page_3.json", "odata_page_4.json", "odata_page_5.json", ] page_data = [] for filename in filenames: with open( f"tests/query/mock/sentinel_1/{filename}", "r", encoding="utf-8" ) as f: page_data.append(json.loads(f.read())) # First page URL is built by the client first_url = ( "https://catalogue.dataspace.copernicus.eu/odata/v1/Products?" "$filter=Collection/Name%20eq%20%27SENTINEL-1%27&$top=10&$count=true" "&$expand=Attributes&$orderby=ContentDate/Start%20asc" ) requests_mock.get(first_url, json=page_data[0]) # Subsequent pages: register the nextLink from each page (with $count stripped) for i in range(len(page_data) - 1): next_link = _strip_odata_count(page_data[i]["@odata.nextLink"]) requests_mock.get(next_link, json=page_data[i + 1]) def test_query_features_length(requests_mock: Any) -> None: """Test that query returns correct total count""" _mock_sentinel_1(requests_mock) query = query_features( "SENTINEL-1", {"top": 10}, options={"expand_attributes": True} ) assert len(query) == 47 manual_count = 0 for _ in query: manual_count += 1 assert manual_count == 47 def test_query_features_reusable(requests_mock: Any) -> None: """Test that query can be iterated multiple times""" _mock_sentinel_1(requests_mock) query = query_features( "SENTINEL-1", {"top": 10}, options={"expand_attributes": True} ) assert len(query) == len(query) assert len(query) == 47 # query is not exhausted after first len call assert list(query) == list(query) # query is not exhausted after first iteration def test_query_features_random_access(requests_mock: Any) -> None: """Test random access to products with proper lazy loading""" _mock_sentinel_1(requests_mock) query = query_features( "SENTINEL-1", {"top": 10}, options={"expand_attributes": True} ) assert query[0]["Name"] == "S1A_AUX_INS_V20140406T010000_G20140409T142540.SAFE" assert len(query.features) == 10 assert query[9]["Name"] == "S1A_AUX_PP2_V20140406T133000_G20241125T134251.SAFE" assert len(query.features) == 10 assert query[13]["Name"] == "S1A_AUX_INS_V20140406T133000_G20211028T132414.SAFE" assert len(query.features) == 20 assert query[2]["Name"] == "S1A_AUX_PP2_V20140406T133000_G20251021T105030.SAFE" assert len(query.features) == 20 assert ( query[34]["Name"] == "S1A_OPER_AUX_PROQUA_POD__20210408T165229_V20140409T235944_20140410T235943" ) assert len(query.features) == 40 CDSETool-0.3.1/tests/query/query_test.py000066400000000000000000000301601517266010700201610ustar00rootroot00000000000000"""Tests for CDSETool's query module.""" from typing import Any from cdsetool.query import ( _build_attribute_filter, _build_odata_filter, _fetch_collection_attributes, _parse_interval, _parse_operator_suffix, describe_collection, geojson_to_wkt, shape_to_wkt, ) def test_build_attribute_filter() -> None: """Test OData attribute filter building""" result = _build_attribute_filter("productType", "S2MSI2A", "StringAttribute", "eq") assert "Attributes/OData.CSC.StringAttribute/any(" in result assert "att/Name eq 'productType'" in result assert "att/OData.CSC.StringAttribute/Value eq 'S2MSI2A'" in result result = _build_attribute_filter("cloudCover", "40.5", "DoubleAttribute", "le") assert "Attributes/OData.CSC.DoubleAttribute/any(" in result assert "att/Name eq 'cloudCover'" in result assert "att/OData.CSC.DoubleAttribute/Value le 40.5" in result result = _build_attribute_filter( "relativeOrbitNumber", "123", "IntegerAttribute", "eq" ) assert "Attributes/OData.CSC.IntegerAttribute/any(" in result assert "att/Name eq 'relativeOrbitNumber'" in result assert "att/OData.CSC.IntegerAttribute/Value eq 123" in result result = _build_attribute_filter( "processingDate", "2024-01-15T10:30:00Z", "DateTimeOffsetAttribute", "eq" ) assert "Attributes/OData.CSC.DateTimeOffsetAttribute/any(" in result assert "att/Name eq 'processingDate'" in result assert ( "att/OData.CSC.DateTimeOffsetAttribute/Value eq 2024-01-15T10:30:00Z" in result ) result = _build_attribute_filter( "sliceProductFlag", "true", "BooleanAttribute", "eq" ) assert "Attributes/OData.CSC.BooleanAttribute/any(" in result assert "att/Name eq 'sliceProductFlag'" in result assert "att/OData.CSC.BooleanAttribute/Value eq true" in result result = _build_attribute_filter( "sliceProductFlag", "False", "BooleanAttribute", "eq" ) assert "att/OData.CSC.BooleanAttribute/Value eq false" in result def test_parse_interval() -> None: """Test interval syntax parsing""" result = _parse_interval("[10,22]") assert result == ("10", "22", "ge", "le") result = _parse_interval("(10,22)") assert result == ("10", "22", "gt", "lt") result = _parse_interval("[10,22)") assert result == ("10", "22", "ge", "lt") result = _parse_interval("(10,22]") assert result == ("10", "22", "gt", "le") result = _parse_interval("[ 10 , 22 ]") assert result == ("10", "22", "ge", "le") result = _parse_interval("[2024-01-01,2024-01-31]") assert result == ("2024-01-01", "2024-01-31", "ge", "le") assert _parse_interval("30") is None assert _parse_interval("abc") is None assert _parse_interval("[10]") is None def test_parse_operator_suffix() -> None: """Test operator suffix parsing""" assert _parse_operator_suffix("cloudCoverLt") == ("cloudCover", "lt") assert _parse_operator_suffix("cloudCoverLe") == ("cloudCover", "le") assert _parse_operator_suffix("cloudCoverGt") == ("cloudCover", "gt") assert _parse_operator_suffix("cloudCoverGe") == ("cloudCover", "ge") assert _parse_operator_suffix("cloudCover") == ("cloudCover", "eq") assert _parse_operator_suffix("orbitNumberLt") == ("orbitNumber", "lt") assert _parse_operator_suffix("processingDateGe") == ("processingDate", "ge") def test_build_odata_filter() -> None: """Test full OData filter expression building""" result = _build_odata_filter( "SENTINEL-2", {"contentDateStartGt": "2020-01-01", "contentDateEndLt": "2020-01-10"}, ) assert "Collection/Name eq 'SENTINEL-2'" in result assert "ContentDate/Start gt 2020-01-01" in result assert "ContentDate/End lt 2020-01-10" in result result = _build_odata_filter( "SENTINEL-2", {"cloudCoverEq": 40, "productType": "S2MSI2A"} ) assert "Collection/Name eq 'SENTINEL-2'" in result assert "cloudCover" in result assert "Value eq 40.0" in result assert "productType" in result result = _build_odata_filter("SENTINEL-2", {"cloudCoverLe": 30}) assert "Collection/Name eq 'SENTINEL-2'" in result assert "cloudCover" in result assert "Value le 30.0" in result result = _build_odata_filter("SENTINEL-2", {"cloudCover": "[10,22]"}) assert "Collection/Name eq 'SENTINEL-2'" in result assert result.count("DoubleAttribute/any(") == 2 assert "att/Name eq 'cloudCover'" in result assert "Value ge 10.0" in result assert "Value le 22.0" in result result = _build_odata_filter("SENTINEL-2", {"cloudCover": "(10,22)"}) assert result.count("DoubleAttribute/any(") == 2 assert "Value gt 10.0" in result assert "Value lt 22.0" in result result = _build_odata_filter("SENTINEL-2", {"cloudCover": "[10,22)"}) assert result.count("DoubleAttribute/any(") == 2 assert "Value ge 10.0" in result assert "Value lt 22.0" in result def test_build_odata_filter_new_attribute_types() -> None: """Test OData filter building with new attribute types from ATTRIBUTES""" result = _build_odata_filter("SENTINEL-3", {"brightCoverEq": 50.5}) assert "Collection/Name eq 'SENTINEL-3'" in result assert "Attributes/OData.CSC.DoubleAttribute/any(" in result assert "att/Name eq 'brightCover'" in result assert "att/OData.CSC.DoubleAttribute/Value eq 50.5" in result result = _build_odata_filter( "SENTINEL-1", {"processingDateEq": "2024-06-15T12:00:00Z"} ) assert "Attributes/OData.CSC.DateTimeOffsetAttribute/any(" in result assert "att/Name eq 'processingDate'" in result assert "2024-06-15T12:00:00Z" in result result = _build_odata_filter("SENTINEL-1", {"sliceProductFlag": "true"}) assert "Attributes/OData.CSC.BooleanAttribute/any(" in result assert "att/Name eq 'sliceProductFlag'" in result assert "att/OData.CSC.BooleanAttribute/Value eq true" in result result = _build_odata_filter("SENTINEL-1", {"cycleNumberEq": 42}) assert "Attributes/OData.CSC.IntegerAttribute/any(" in result assert "att/Name eq 'cycleNumber'" in result assert "att/OData.CSC.IntegerAttribute/Value eq 42" in result result = _build_odata_filter("SENTINEL-1", {"timeliness": "NRT"}) assert "Attributes/OData.CSC.StringAttribute/any(" in result assert "att/Name eq 'timeliness'" in result assert "att/OData.CSC.StringAttribute/Value eq 'NRT'" in result def test_build_odata_filter_operator_suffixes() -> None: """Test OData filter building with operator suffixes""" result = _build_odata_filter("SENTINEL-1", {"orbitNumberLt": 100}) assert "att/Name eq 'orbitNumber'" in result assert "att/OData.CSC.IntegerAttribute/Value lt 100" in result result = _build_odata_filter("SENTINEL-1", {"orbitNumberGe": 50}) assert "att/Name eq 'orbitNumber'" in result assert "att/OData.CSC.IntegerAttribute/Value ge 50" in result result = _build_odata_filter("SENTINEL-2", {"cloudCoverLe": 25.5}) assert "att/Name eq 'cloudCover'" in result assert "att/OData.CSC.DoubleAttribute/Value le 25.5" in result result = _build_odata_filter( "SENTINEL-1", {"processingDateGt": "2024-01-01T00:00:00Z"} ) assert "att/Name eq 'processingDate'" in result assert ( "att/OData.CSC.DateTimeOffsetAttribute/Value gt 2024-01-01T00:00:00Z" in result ) def test_build_odata_filter_date_intervals() -> None: """Test OData filter building with date interval syntax""" result = _build_odata_filter( "SENTINEL-2", {"contentDateStart": "[2024-01-01,2024-01-31]"} ) assert "ContentDate/Start ge 2024-01-01" in result assert "ContentDate/Start le 2024-01-31" in result result = _build_odata_filter( "SENTINEL-2", {"contentDateEnd": "(2024-01-01,2024-01-31)"} ) assert "ContentDate/End gt 2024-01-01" in result assert "ContentDate/End lt 2024-01-31" in result result = _build_odata_filter( "SENTINEL-1", {"processingDate": "[2024-01-01T00:00:00Z,2024-01-31T23:59:59Z]"} ) assert result.count("DateTimeOffsetAttribute/any(") == 2 assert "att/Name eq 'processingDate'" in result assert "Value ge 2024-01-01T00:00:00Z" in result assert "Value le 2024-01-31T23:59:59Z" in result def test_shape_to_wkt() -> None: wkt = "POLYGON((10.172406299744779 55.48259118004532, 10.172406299744779 55.38234270718456, 10.42371976928382 55.38234270718456, 10.42371976928382 55.48259118004532, 10.172406299744779 55.48259118004532))" assert shape_to_wkt("tests/shape/POLYGON.shp") == wkt def test_geojson_to_wkt() -> None: wkt = "POLYGON((10.172406299744779 55.48259118004532, 10.172406299744779 55.38234270718456, 10.42371976928382 55.38234270718456, 10.42371976928382 55.48259118004532, 10.172406299744779 55.48259118004532))" geojson = '{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 10.172406299744779, 55.482591180045318 ], [ 10.172406299744779, 55.382342707184563 ], [ 10.423719769283821, 55.382342707184563 ], [ 10.423719769283821, 55.482591180045318 ], [ 10.172406299744779, 55.482591180045318 ] ] ] } }' assert geojson_to_wkt(geojson) == wkt geojson = '{ "type": "Polygon", "coordinates": [ [ [ 10.172406299744779, 55.482591180045318 ], [ 10.172406299744779, 55.382342707184563 ], [ 10.423719769283821, 55.382342707184563 ], [ 10.423719769283821, 55.482591180045318 ], [ 10.172406299744779, 55.482591180045318 ] ] ] }' assert geojson_to_wkt(geojson) == wkt wkt = "POLYGON((17.58127378553624 59.88489715357605, 17.58127378553624 59.80687027682205, 17.73996723627809 59.80687027682205, 17.73996723627809 59.88489715357605, 17.58127378553624 59.88489715357605))" geojson = '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{ },"geometry":{"coordinates":[[[17.58127378553624,59.88489715357605],[17.58127378553624,59.80687027682205],[17.73996723627809,59.80687027682205],[17.73996723627809,59.88489715357605],[17.58127378553624,59.88489715357605]]],"type":"Polygon" } } ] }' assert geojson_to_wkt(geojson) == wkt def _mock_sentinel_1_attributes(requests_mock: Any) -> None: """Mock the Attributes endpoint for SENTINEL-1""" with open( "tests/query/mock/sentinel_1/attributes.json", "r", encoding="utf-8" ) as f: requests_mock.get( "https://catalogue.dataspace.copernicus.eu/odata/v1/Attributes(SENTINEL-1)", text=f.read(), ) def test_fetch_collection_attributes(requests_mock: Any) -> None: """Test fetching attributes from OData API""" _mock_sentinel_1_attributes(requests_mock) attrs = _fetch_collection_attributes("SENTINEL-1") assert attrs is not None attr_names = [a["Name"] for a in attrs] assert "productType" in attr_names assert "orbitNumber" in attr_names assert "relativeOrbitNumber" in attr_names product_type = next(a for a in attrs if a["Name"] == "productType") assert product_type["ValueType"] == "String" orbit_number = next(a for a in attrs if a["Name"] == "orbitNumber") assert orbit_number["ValueType"] == "Integer" def test_fetch_collection_attributes_not_found(requests_mock: Any) -> None: """Test fetching attributes for non-existent collection""" requests_mock.get( "https://catalogue.dataspace.copernicus.eu/odata/v1/Attributes(INVALID)", status_code=400, ) assert _fetch_collection_attributes("INVALID") is None def test_describe_collection_returns_all_server_attrs_as_supported( requests_mock: Any, ) -> None: """Test that describe_collection returns all server attributes as supported""" _mock_sentinel_1_attributes(requests_mock) result = describe_collection("SENTINEL-1") assert result is not None assert "contentDateStart" in result assert "contentDateEnd" in result assert "geometry" in result assert "productType" in result assert "orbitDirection" in result assert "relativeOrbitNumber" in result assert "orbitNumber" in result assert "datatakeID" in result assert "cycleNumber" in result assert "sliceNumber" in result assert "processorName" in result assert "processingDate" in result assert "sliceProductFlag" in result assert "startTimeFromAscendingNode" in result CDSETool-0.3.1/tests/shape/000077500000000000000000000000001517266010700153365ustar00rootroot00000000000000CDSETool-0.3.1/tests/shape/POLYGON.dbf000066400000000000000000000000431517266010700171370ustar00rootroot00000000000000{ ! CDSETool-0.3.1/tests/shape/POLYGON.prj000066400000000000000000000002171517266010700172020ustar00rootroot00000000000000GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]CDSETool-0.3.1/tests/shape/POLYGON.shp000066400000000000000000000003541517266010700172030ustar00rootroot00000000000000' vèpv£EX$@dœ›ð°K@@|/ÌñØ$@üÐ;ŒÅ½K@@pv£EX$@dœ›ð°K@@|/ÌñØ$@üÐ;ŒÅ½K@pv£EX$@üÐ;ŒÅ½K@pv£EX$@dœ›ð°K@@|/ÌñØ$@dœ›ð°K@@|/ÌñØ$@üÐ;ŒÅ½K@pv£EX$@üÐ;ŒÅ½K@CDSETool-0.3.1/tests/shape/POLYGON.shx000066400000000000000000000001541517266010700172110ustar00rootroot00000000000000' 6èpv£EX$@dœ›ð°K@@|/ÌñØ$@üÐ;ŒÅ½K@2@