From bd120fe5361725f860d0a6774cf2cd71806b8026 Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Mon, 12 Dec 2022 10:47:49 +0000 Subject: [PATCH 001/135] feat: add the BSD 3 clause licence --- LICENSE.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6ac8589 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,12 @@ +Copyright (c) 2022, OPEN COSMOS LTD +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From ce323f86cf991eeb7cb0c0e3f45098cd1a3979e8 Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 16:51:32 +0000 Subject: [PATCH 002/135] feat: add a barebones README --- README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba5933f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# DataCosmos Python SDK + +A library for interacting with DataCosmos from Python code. From 3879df60a23216ba12c098740570a21edaf9c1cb Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 17:05:58 +0000 Subject: [PATCH 003/135] feat: add a pyproject toml for the project --- pyproject.toml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..97f75a3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "datacosmos-sdk" +version = "0.0.1" +authors = [ + { name="Open Cosmos", email="support@open-cosmos.com" }, +] +description = "A library for interacting with DataCosmos from Python code" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", +] +dependencies = [ +] + +[tool.setuptools.packages] +find = {} From b62043f0ad029f9e3fc48bfce4b9ae462d26356b Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 17:07:46 +0000 Subject: [PATCH 004/135] feat: add empty CHANGELOG file --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0a9a280 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# CHANGELOG + + From bd9d602451ab4d4968c8a5435696e39d6eac0803 Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 17:09:52 +0000 Subject: [PATCH 005/135] feat: initialise the datacosmos module --- datacosmos/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 datacosmos/__init__.py diff --git a/datacosmos/__init__.py b/datacosmos/__init__.py new file mode 100644 index 0000000..8e3838b --- /dev/null +++ b/datacosmos/__init__.py @@ -0,0 +1 @@ +"""A library for interacting with DataCosmos from Python code.""" From 8b1590d9b68a5f43106f9e2dfba15b223be05cf2 Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 17:13:27 +0000 Subject: [PATCH 006/135] feat: initialise the tests module --- tests/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..56d7a9b --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests of the DataCosmos SDK.""" From a354ec2ff29f53ee727d5303fec1acf883ededa7 Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 18:07:47 +0000 Subject: [PATCH 007/135] feat: run lint on all branches --- .github/workflows/lint.yaml | 28 ++++++++++++++++++++++++++++ pyproject.toml | 2 ++ 2 files changed, 30 insertions(+) create mode 100644 .github/workflows/lint.yaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..5d3f266 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,28 @@ +name: lint + +on: + push: + branches: + - "**" + pull_request: + branches: + - "**" + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + -uses: actions/checkout@v3 + - name: set up python + uses: actions/setup-python@v1 + with: + python-version: 3.10.9 + - name: install dependencies + run: pip install . + - name: lint + uses: wearerequired/lint-action@v2 + with: + continue_on_error: false + black: true + flake8: true diff --git a/pyproject.toml b/pyproject.toml index 97f75a3..0b14726 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,8 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ + "black==22.12.0", + "flake8==6.0.0", ] [tool.setuptools.packages] From 77df0fa64e465247b68e484aeeebac5ade453fee Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 18:34:40 +0000 Subject: [PATCH 008/135] feat: a test workflow --- .github/workflows/test.yaml | 15 +++++++++++++++ pyproject.toml | 1 + 2 files changed, 16 insertions(+) create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..d169563 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,15 @@ +name: test + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: set up python + uses: actions/setup-python@v4 + with: + python-version: 3.10.9 + - name: install dependencies + run: pip install . + - name: test + run: python -m pytest diff --git a/pyproject.toml b/pyproject.toml index 0b14726..dddb977 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ classifiers = [ dependencies = [ "black==22.12.0", "flake8==6.0.0", + "pytest==7.2.0", ] [tool.setuptools.packages] From 11d43e70d593eb652d97e7f4886a928fc9364f50 Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 18:41:18 +0000 Subject: [PATCH 009/135] fix: consolidate the two jobs into a single flow --- .github/workflows/{lint.yaml => main.yaml} | 18 ++++++++++++++++-- .github/workflows/test.yaml | 15 --------------- 2 files changed, 16 insertions(+), 17 deletions(-) rename .github/workflows/{lint.yaml => main.yaml} (56%) delete mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/main.yaml similarity index 56% rename from .github/workflows/lint.yaml rename to .github/workflows/main.yaml index 5d3f266..d27c524 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/main.yaml @@ -1,4 +1,4 @@ -name: lint +name: main on: push: @@ -13,7 +13,7 @@ jobs: name: lint runs-on: ubuntu-latest steps: - -uses: actions/checkout@v3 + - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v1 with: @@ -26,3 +26,17 @@ jobs: continue_on_error: false black: true flake8: true + test: + name: test + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v3 + - name: set up python + uses: actions/setup-python@v4 + with: + python-version: 3.10.9 + - name: install dependencies + run: pip install . + - name: test + run: python -m pytest diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index d169563..0000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: test - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: set up python - uses: actions/setup-python@v4 - with: - python-version: 3.10.9 - - name: install dependencies - run: pip install . - - name: test - run: python -m pytest From 0bae2d275a65b67f5499f7b66cd5e6f61f33f5e4 Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 18:48:47 +0000 Subject: [PATCH 010/135] feat: calculate the version and tag the commit, on main --- .github/workflows/main.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d27c524..55dd073 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -40,3 +40,15 @@ jobs: run: pip install . - name: test run: python -m pytest + tag: + name: tag version + runs-on: ubuntu-latest + needs: test + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v3 + - name: version and tag + id: tag + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} From 748aec8b4443c45acc6ae4530021566d39e9ad0a Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 19:02:20 +0000 Subject: [PATCH 011/135] feat: a passing test to check CI --- tests/test_pass.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/test_pass.py diff --git a/tests/test_pass.py b/tests/test_pass.py new file mode 100644 index 0000000..32c6ad8 --- /dev/null +++ b/tests/test_pass.py @@ -0,0 +1,7 @@ +"""An example test to check pytest setup.""" + + +class TestPass: + def test_pass(self): + """A passing test, to check the pytest CI setup.""" + pass From 3cfbc246848734b2ccd8345b0dbcc213a2bd115d Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 19:13:20 +0000 Subject: [PATCH 012/135] feat: add bandit linting to CI --- .github/workflows/main.yaml | 15 ++++++++++++++- pyproject.toml | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 55dd073..bf8bec9 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -26,10 +26,23 @@ jobs: continue_on_error: false black: true flake8: true + bandit: + name: bandit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: set up python + uses: actions/setup-python@v1 + with: + python-version: 3.10.9 + - name: install dependencies + run: pip install . + - name: bandit + run: bandit -r -c pyproject.toml . test: name: test runs-on: ubuntu-latest - needs: lint + needs: [lint, bandit] steps: - uses: actions/checkout@v3 - name: set up python diff --git a/pyproject.toml b/pyproject.toml index dddb977..7ef2307 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,10 @@ dependencies = [ "black==22.12.0", "flake8==6.0.0", "pytest==7.2.0", + "bandit[toml]==1.7.4", ] [tool.setuptools.packages] find = {} + +[tool.bandit] From 6b6eddf556b46b3c1ee127094cd125fc238c67fc Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 19:25:34 +0000 Subject: [PATCH 013/135] feat: include an isort check step --- .github/workflows/main.yaml | 15 ++++++++++++++- pyproject.toml | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index bf8bec9..ed3f1c0 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -39,10 +39,23 @@ jobs: run: pip install . - name: bandit run: bandit -r -c pyproject.toml . + isort: + name: isort + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: set up python + uses: actions/setup-python@v1 + with: + python-version: 3.10.9 + - name: install dependencies + run: pip install . + - name: isort + uses: isort/isort-action@v1.1.0 test: name: test runs-on: ubuntu-latest - needs: [lint, bandit] + needs: [lint, bandit, isort] steps: - uses: actions/checkout@v3 - name: set up python diff --git a/pyproject.toml b/pyproject.toml index 7ef2307..b316a83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "flake8==6.0.0", "pytest==7.2.0", "bandit[toml]==1.7.4", + "isort==5.11.4", ] [tool.setuptools.packages] From 47e6867f2deb53817e5b20164ef11ac60e79a26c Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 19:30:54 +0000 Subject: [PATCH 014/135] feat: set up pydocstyle linting --- .github/workflows/main.yaml | 15 ++++++++++++++- pyproject.toml | 4 ++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index ed3f1c0..2405bbd 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -52,10 +52,23 @@ jobs: run: pip install . - name: isort uses: isort/isort-action@v1.1.0 + pydocstyle: + name: pydocstyle + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: set up python + uses: actions/setup-python@v1 + with: + python-version: 3.10.9 + - name: install dependencies + run: pip install . + - name: pydocstyle + run: pydocstyle . test: name: test runs-on: ubuntu-latest - needs: [lint, bandit, isort] + needs: [lint, bandit, isort, pydocstyle] steps: - uses: actions/checkout@v3 - name: set up python diff --git a/pyproject.toml b/pyproject.toml index b316a83..ef14cdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,9 +20,13 @@ dependencies = [ "pytest==7.2.0", "bandit[toml]==1.7.4", "isort==5.11.4", + "pydocstyle==6.1.1", ] [tool.setuptools.packages] find = {} [tool.bandit] + +[tool.pydocstyle] +convention = "google" From 769c6bca274e5071d7cdbfdd6114716fcc01e87d Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 19:34:39 +0000 Subject: [PATCH 015/135] feat: add cognitive complexity linting --- .github/workflows/main.yaml | 15 ++++++++++++++- pyproject.toml | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 2405bbd..c63c885 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -39,6 +39,19 @@ jobs: run: pip install . - name: bandit run: bandit -r -c pyproject.toml . + cognitive: + name: cognitive + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: set up python + uses: actions/setup-python@v1 + with: + python-version: 3.10.9 + - name: install dependencies + run: pip install . + - name: cognitive + run: flake8 . --max-cognitive-complexity=5 isort: name: isort runs-on: ubuntu-latest @@ -68,7 +81,7 @@ jobs: test: name: test runs-on: ubuntu-latest - needs: [lint, bandit, isort, pydocstyle] + needs: [bandit, cognitive, isort, lint, pydocstyle] steps: - uses: actions/checkout@v3 - name: set up python diff --git a/pyproject.toml b/pyproject.toml index ef14cdd..f2a5fc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "bandit[toml]==1.7.4", "isort==5.11.4", "pydocstyle==6.1.1", + "flake8-cognitive-complexity==0.1.0", ] [tool.setuptools.packages] From 9f093571cf0fa325e8437aadda7c91bbc6ea885d Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 19:53:46 +0000 Subject: [PATCH 016/135] feat: add a "best practice" Python .gitignore --- .gitignore | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c19803 --- /dev/null +++ b/.gitignore @@ -0,0 +1,134 @@ +# 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 + +# 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/ From 1893a248980cd8dbf3b2498b4e6bfa538e0d9b00 Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Thu, 22 Dec 2022 20:17:20 +0000 Subject: [PATCH 017/135] feat: autogenerate a PR to add a changelog --- .github/workflows/main.yaml | 41 +++++++++++++++++++++++++++++++------ CHANGELOG.md | 2 -- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index c63c885..bce1d9b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -92,15 +92,44 @@ jobs: run: pip install . - name: test run: python -m pytest - tag: - name: tag version + release: + name: tag, changelog, release, publish runs-on: ubuntu-latest needs: test if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - - name: version and tag - id: tag - uses: mathieudutour/github-tag-action@v6.1 + - name: version + uses: paulhatch/semantic-version@v5.0.0 + id: version with: - github_token: ${{ secrets.GITHUB_TOKEN }} + major_pattern: "(feat!)" + minor_pattern: "(feat)" + - name: create changelog text + id: changelog + uses: loopwerk/tag-changelog@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + exclude_types: other,doc,chore + - name: Create release + uses: actions/create-release@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.version.outputs.version_tag }} + release_name: Release ${{ steps.version.outputs.version_tag }} + body: ${{ steps.changelog.outputs.changes }} + - name: create changelog pull request + uses: peter-evans/create-pull-request@v2 + with: + commit-message: "Release ${{ steps.version.outputs.version_tag }} [skip ci]" + labels: release, bot + title: "Release ${{ steps.version.outputs.version_tag }}" + body: | + # Release ${{ steps.version.outputs.version_tag }} + + Merge this PR to update your version and changelog! + + ## Included Pull Requests + + ${{ steps.changelog.outputs.changes }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a9a280..a0cf709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1 @@ # CHANGELOG - - From ff78adf6f9ed66c84720f619494a186145ffb120 Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Fri, 20 Jan 2023 16:22:26 +0000 Subject: [PATCH 018/135] fix: move the dependencies to optional, test --- .github/workflows/main.yaml | 12 ++++++------ pyproject.toml | 5 ++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index bce1d9b..b96460b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -19,7 +19,7 @@ jobs: with: python-version: 3.10.9 - name: install dependencies - run: pip install . + run: pip install .[test] - name: lint uses: wearerequired/lint-action@v2 with: @@ -36,7 +36,7 @@ jobs: with: python-version: 3.10.9 - name: install dependencies - run: pip install . + run: pip install .[test] - name: bandit run: bandit -r -c pyproject.toml . cognitive: @@ -49,7 +49,7 @@ jobs: with: python-version: 3.10.9 - name: install dependencies - run: pip install . + run: pip install .[test] - name: cognitive run: flake8 . --max-cognitive-complexity=5 isort: @@ -62,7 +62,7 @@ jobs: with: python-version: 3.10.9 - name: install dependencies - run: pip install . + run: pip install .[test] - name: isort uses: isort/isort-action@v1.1.0 pydocstyle: @@ -75,7 +75,7 @@ jobs: with: python-version: 3.10.9 - name: install dependencies - run: pip install . + run: pip install .[test] - name: pydocstyle run: pydocstyle . test: @@ -89,7 +89,7 @@ jobs: with: python-version: 3.10.9 - name: install dependencies - run: pip install . + run: pip install .[test] - name: test run: python -m pytest release: diff --git a/pyproject.toml b/pyproject.toml index f2a5fc4..87dd491 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,10 @@ classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] -dependencies = [ +dependencies = [] + +[project.optional-dependencies] +test = [ "black==22.12.0", "flake8==6.0.0", "pytest==7.2.0", From 89e650d98c4f0062b09ebdc64a815f1f5d7bf73c Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Fri, 20 Jan 2023 16:25:06 +0000 Subject: [PATCH 019/135] feat: test on multiple Python versions --- .github/workflows/main.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index b96460b..d48fa39 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -81,13 +81,16 @@ jobs: test: name: test runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11"] needs: [bandit, cognitive, isort, lint, pydocstyle] steps: - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v4 with: - python-version: 3.10.9 + python-version: ${{ matrix.python-version }} - name: install dependencies run: pip install .[test] - name: test From 24a26c785e9a9fc9dfb229199ec4b18a47e80e82 Mon Sep 17 00:00:00 2001 From: Scott Nicholas Allan Smith Date: Mon, 6 Mar 2023 06:59:11 +0000 Subject: [PATCH 020/135] feat: test also on Python 3.8 and 3.9 --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d48fa39..531f15f 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -83,7 +83,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] needs: [bandit, cognitive, isort, lint, pydocstyle] steps: - uses: actions/checkout@v3 From bf2aaa9ede6bd9f2a7ada88719fde9e45a8c36f6 Mon Sep 17 00:00:00 2001 From: Scott Nicholas Allan Smith Date: Mon, 6 Mar 2023 07:02:31 +0000 Subject: [PATCH 021/135] fix: more generic 3.10 python version for actions --- .github/workflows/main.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 531f15f..2d4165d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -17,7 +17,7 @@ jobs: - name: set up python uses: actions/setup-python@v1 with: - python-version: 3.10.9 + python-version: "3.10" - name: install dependencies run: pip install .[test] - name: lint @@ -34,7 +34,7 @@ jobs: - name: set up python uses: actions/setup-python@v1 with: - python-version: 3.10.9 + python-version: "3.10" - name: install dependencies run: pip install .[test] - name: bandit @@ -47,7 +47,7 @@ jobs: - name: set up python uses: actions/setup-python@v1 with: - python-version: 3.10.9 + python-version: "3.10" - name: install dependencies run: pip install .[test] - name: cognitive @@ -60,7 +60,7 @@ jobs: - name: set up python uses: actions/setup-python@v1 with: - python-version: 3.10.9 + python-version: "3.10" - name: install dependencies run: pip install .[test] - name: isort @@ -73,7 +73,7 @@ jobs: - name: set up python uses: actions/setup-python@v1 with: - python-version: 3.10.9 + python-version: "3.10" - name: install dependencies run: pip install .[test] - name: pydocstyle From 8955200daac9d351094b31a48d63f7700b1a0e24 Mon Sep 17 00:00:00 2001 From: Scott Nicholas Allan Smith Date: Mon, 6 Mar 2023 07:12:17 +0000 Subject: [PATCH 022/135] fix: downgrade the required Python version to 3.8 in the pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 87dd491..b06bf42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ { name="Open Cosmos", email="support@open-cosmos.com" }, ] description = "A library for interacting with DataCosmos from Python code" -requires-python = ">=3.10" +requires-python = ">=3.8" classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", From 66e0332df871bafaada14f11387722131262db14 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 22 Jan 2025 16:30:01 +0000 Subject: [PATCH 023/135] Add first version of the client auth for sdk --- config/__init__.py | 0 config/config.py | 42 ++++++++++++++++++++++++ datacosmos/client.py | 77 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 config/__init__.py create mode 100644 config/config.py create mode 100644 datacosmos/client.py diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/config.py b/config/config.py new file mode 100644 index 0000000..5eaea0e --- /dev/null +++ b/config/config.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import os +import yaml +from dataclasses import dataclass + + +@dataclass +class Config: + client_id: str + client_secret: str + token_url: str + audience: str + + @staticmethod + def from_yaml(file_path: str = "config/config.yaml") -> Config: + """ + Load configuration from a YAML file. + Defaults to 'config/config.yaml' unless otherwise specified. + """ + with open(file_path, "r") as f: + data = yaml.safe_load(f) + auth = data.get("auth", {}) + return Config( + client_id=auth["client-id"], + client_secret=auth["client-secret"], + token_url=auth["token-url"], + audience=auth["audience"], + ) + + @staticmethod + def from_env() -> Config: + """ + Load configuration from environment variables. + Raises an exception if any required variable is missing. + """ + return Config( + client_id=os.getenv("OC_AUTH_CLIENT_ID"), + client_secret=os.getenv("OC_AUTH_CLIENT_SECRET"), + token_url=os.getenv("OC_AUTH_TOKEN_URL"), + audience=os.getenv("OC_AUTH_AUDIENCE"), + ) diff --git a/datacosmos/client.py b/datacosmos/client.py new file mode 100644 index 0000000..b3bff4f --- /dev/null +++ b/datacosmos/client.py @@ -0,0 +1,77 @@ +import os + +from datetime import datetime, timedelta, timezone +from typing import Optional, Any +import requests +from requests_oauthlib import OAuth2Session +from oauthlib.oauth2 import BackendApplicationClient + +from config.config import Config + + +class DatacosmosClient: + def __init__(self, config: Optional[Config] = None, config_file: str = "config/config.yaml"): + self.config = config or self._load_config(config_file) + self.token = None + self.token_expiry = None + self._http_client = self._authenticate_and_initialize_client() + + def _load_config(self, config_file: str) -> Config: + if os.path.exists(config_file): + return Config.from_yaml(config_file) + return Config.from_env() + + def _authenticate_and_initialize_client(self) -> requests.Session: + client = BackendApplicationClient(client_id=self.config.client_id) + oauth_session = OAuth2Session(client=client) + + # Fetch the token using client credentials + token_response = oauth_session.fetch_token( + token_url=self.config.token_url, + client_id=self.config.client_id, + client_secret=self.config.client_secret, + audience=self.config.audience, + ) + + self.token = token_response["access_token"] + self.token_expiry = datetime.now(timezone.utc) + timedelta(seconds=token_response.get("expires_in", 3600)) + + # Initialize the HTTP session with the Authorization header + http_client = requests.Session() + http_client.headers.update({"Authorization": f"Bearer {self.token}"}) + + return http_client + + def _refresh_token_if_needed(self): + """ + Refreshes the token if it has expired. + """ + if not self.token or self.token_expiry <= datetime.now(timezone.utc): + self._http_client = self._authenticate_and_initialize_client() + + def get_http_client(self) -> requests.Session: + """ + Returns the authenticated HTTP client, refreshing the token if necessary. + """ + self._refresh_token_if_needed() + return self._http_client + + # Proxy HTTP methods to the underlying authenticated session + def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> requests.Response: + """ + Proxy method to send HTTP requests using the authenticated session. + """ + self._refresh_token_if_needed() + return self._http_client.request(method, url, *args, **kwargs) + + def get(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: + return self.request("GET", url, *args, **kwargs) + + def post(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: + return self.request("POST", url, *args, **kwargs) + + def put(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: + return self.request("PUT", url, *args, **kwargs) + + def delete(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: + return self.request("DELETE", url, *args, **kwargs) From 21d0ee3f882cfb8d19e4a42f644c395cc55636ab Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 22 Jan 2025 17:01:20 +0000 Subject: [PATCH 024/135] Run ruff format and ruff check fix --- datacosmos/client.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index b3bff4f..54eb279 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -10,7 +10,9 @@ class DatacosmosClient: - def __init__(self, config: Optional[Config] = None, config_file: str = "config/config.yaml"): + def __init__( + self, config: Optional[Config] = None, config_file: str = "config/config.yaml" + ): self.config = config or self._load_config(config_file) self.token = None self.token_expiry = None @@ -34,7 +36,9 @@ def _authenticate_and_initialize_client(self) -> requests.Session: ) self.token = token_response["access_token"] - self.token_expiry = datetime.now(timezone.utc) + timedelta(seconds=token_response.get("expires_in", 3600)) + self.token_expiry = datetime.now(timezone.utc) + timedelta( + seconds=token_response.get("expires_in", 3600) + ) # Initialize the HTTP session with the Authorization header http_client = requests.Session() @@ -57,7 +61,9 @@ def get_http_client(self) -> requests.Session: return self._http_client # Proxy HTTP methods to the underlying authenticated session - def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> requests.Response: + def request( + self, method: str, url: str, *args: Any, **kwargs: Any + ) -> requests.Response: """ Proxy method to send HTTP requests using the authenticated session. """ From 1efe6ed81707ff53063a6560ed08a2d5fe8d9122 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 22 Jan 2025 17:34:15 +0000 Subject: [PATCH 025/135] Make some changes regarding code formatting --- .gitignore | 3 +++ config/__init__.py | 9 ++++++++ config/config.py | 25 +++++++++++++------- datacosmos/client.py | 40 +++++++++++++++++++++----------- pyproject.toml | 55 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_pass.py | 8 ++++--- 6 files changed, 116 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 3c19803..2e300c0 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,6 @@ dmypy.json # Cython debug symbols cython_debug/ + +# Ignore config.yaml +config/config.yaml diff --git a/config/__init__.py b/config/__init__.py index e69de29..e9778e9 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -0,0 +1,9 @@ +"""Configuration package for the Datacosmos SDK. + +This package includes modules for loading and managing authentication configurations. +""" + +# Expose Config class for easier imports +from .config import Config + +__all__ = ["Config"] diff --git a/config/config.py b/config/config.py index 5eaea0e..c3a6624 100644 --- a/config/config.py +++ b/config/config.py @@ -1,21 +1,30 @@ -from __future__ import annotations +"""Module for managing configuration settings for the Datacosmos SDK. + +Supports loading from YAML files and environment variables. +""" import os -import yaml from dataclasses import dataclass +import yaml + @dataclass class Config: + """Configuration for the Datacosmos SDK. + + Contains authentication details such as client ID, secret, token URL, and audience. + """ + client_id: str client_secret: str token_url: str audience: str @staticmethod - def from_yaml(file_path: str = "config/config.yaml") -> Config: - """ - Load configuration from a YAML file. + def from_yaml(file_path: str = "config/config.yaml") -> "Config": + """Load configuration from a YAML file. + Defaults to 'config/config.yaml' unless otherwise specified. """ with open(file_path, "r") as f: @@ -29,9 +38,9 @@ def from_yaml(file_path: str = "config/config.yaml") -> Config: ) @staticmethod - def from_env() -> Config: - """ - Load configuration from environment variables. + def from_env() -> "Config": + """Load configuration from environment variables. + Raises an exception if any required variable is missing. """ return Config( diff --git a/datacosmos/client.py b/datacosmos/client.py index 54eb279..858568a 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -1,29 +1,46 @@ -import os +"""Datacosmos client for interacting with the Datacosmos API. + +Provides an authenticated HTTP client and convenience methods for HTTP requests. +""" +import os from datetime import datetime, timedelta, timezone -from typing import Optional, Any +from typing import Any, Optional + import requests -from requests_oauthlib import OAuth2Session from oauthlib.oauth2 import BackendApplicationClient +from requests_oauthlib import OAuth2Session from config.config import Config class DatacosmosClient: + """DatacosmosClient handles authenticated interactions with the Datacosmos API. + + Automatically manages token refreshing and provides HTTP convenience methods. + """ + def __init__( self, config: Optional[Config] = None, config_file: str = "config/config.yaml" ): + """Initialize the DatacosmosClient. + + If no configuration is provided, it will load from the specified YAML file + or fall back to environment variables. + """ self.config = config or self._load_config(config_file) self.token = None self.token_expiry = None self._http_client = self._authenticate_and_initialize_client() def _load_config(self, config_file: str) -> Config: + """Load configuration from the YAML file. Fall back to environment variables if the file is missing.""" if os.path.exists(config_file): return Config.from_yaml(config_file) return Config.from_env() def _authenticate_and_initialize_client(self) -> requests.Session: + """Authenticate and initialize the HTTP client with a valid token.""" client = BackendApplicationClient(client_id=self.config.client_id) oauth_session = OAuth2Session(client=client) @@ -47,37 +64,34 @@ def _authenticate_and_initialize_client(self) -> requests.Session: return http_client def _refresh_token_if_needed(self): - """ - Refreshes the token if it has expired. - """ + """Refresh the token if it has expired.""" if not self.token or self.token_expiry <= datetime.now(timezone.utc): self._http_client = self._authenticate_and_initialize_client() def get_http_client(self) -> requests.Session: - """ - Returns the authenticated HTTP client, refreshing the token if necessary. - """ + """Return the authenticated HTTP client, refreshing the token if necessary.""" self._refresh_token_if_needed() return self._http_client - # Proxy HTTP methods to the underlying authenticated session def request( self, method: str, url: str, *args: Any, **kwargs: Any ) -> requests.Response: - """ - Proxy method to send HTTP requests using the authenticated session. - """ + """Send an HTTP request using the authenticated session.""" self._refresh_token_if_needed() return self._http_client.request(method, url, *args, **kwargs) def get(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: + """Send a GET request using the authenticated session.""" return self.request("GET", url, *args, **kwargs) def post(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: + """Send a POST request using the authenticated session.""" return self.request("POST", url, *args, **kwargs) def put(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: + """Send a PUT request using the authenticated session.""" return self.request("PUT", url, *args, **kwargs) def delete(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: + """Send a DELETE request using the authenticated session.""" return self.request("DELETE", url, *args, **kwargs) diff --git a/pyproject.toml b/pyproject.toml index b06bf42..1042a6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ test = [ "isort==5.11.4", "pydocstyle==6.1.1", "flake8-cognitive-complexity==0.1.0", + "ruff==0.0.286" ] [tool.setuptools.packages] @@ -34,3 +35,57 @@ find = {} [tool.pydocstyle] convention = "google" + +[tool.ruff] +fix = true +line-length = 88 +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +[tool.ruff.lint] +extend-select = ["I", "SLF", "F", "C90", "BLE", "B", "ARG", "ERA"] +ignore = [] +fixable = ["ALL"] +unfixable = [] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = false + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.black] +line-length = 88 + +[tool.isort] +profile = "black" diff --git a/tests/test_pass.py b/tests/test_pass.py index 32c6ad8..228fe40 100644 --- a/tests/test_pass.py +++ b/tests/test_pass.py @@ -1,7 +1,9 @@ -"""An example test to check pytest setup.""" +"""Test suite for basic functionality and CI setup.""" class TestPass: + """A simple test class to validate the CI pipeline setup.""" + def test_pass(self): - """A passing test, to check the pytest CI setup.""" - pass + """A passing test to ensure the CI pipeline is functional.""" + assert True From 0f1479fc87aa8e479d802c84489e7a54abba2d56 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Thu, 23 Jan 2025 14:46:11 +0000 Subject: [PATCH 026/135] Add some unit tests to datacosmos client --- datacosmos/client.py | 138 ++++++++++++------ .../client/test_client_authentication.py | 53 +++++++ .../client/test_client_initialization.py | 28 ++++ .../client/test_client_token_refreshing.py | 50 +++++++ 4 files changed, 222 insertions(+), 47 deletions(-) create mode 100644 tests/unit/datacosmos/client/test_client_authentication.py create mode 100644 tests/unit/datacosmos/client/test_client_initialization.py create mode 100644 tests/unit/datacosmos/client/test_client_token_refreshing.py diff --git a/datacosmos/client.py b/datacosmos/client.py index 858568a..d9a68a3 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -1,97 +1,141 @@ -"""Datacosmos client for interacting with the Datacosmos API. - -Provides an authenticated HTTP client and convenience methods for HTTP requests. -""" - +import logging import os from datetime import datetime, timedelta, timezone -from typing import Any, Optional +from typing import Optional, Any import requests -from oauthlib.oauth2 import BackendApplicationClient from requests_oauthlib import OAuth2Session +from oauthlib.oauth2 import BackendApplicationClient +from requests.exceptions import RequestException from config.config import Config class DatacosmosClient: - """DatacosmosClient handles authenticated interactions with the Datacosmos API. - + """ + DatacosmosClient handles authenticated interactions with the Datacosmos API. Automatically manages token refreshing and provides HTTP convenience methods. """ def __init__( self, config: Optional[Config] = None, config_file: str = "config/config.yaml" ): - """Initialize the DatacosmosClient. + """ + Initialize the DatacosmosClient. If no configuration is provided, it will load from the specified YAML file or fall back to environment variables. """ + self.logger = logging.getLogger(__name__) + self.logger.setLevel(logging.INFO) + self.config = config or self._load_config(config_file) self.token = None self.token_expiry = None self._http_client = self._authenticate_and_initialize_client() def _load_config(self, config_file: str) -> Config: - """Load configuration from the YAML file. Fall back to environment variables if the file is missing.""" - if os.path.exists(config_file): - return Config.from_yaml(config_file) - return Config.from_env() + """ + Load configuration from the YAML file. Fall back to environment variables if the file is missing. + """ + try: + if os.path.exists(config_file): + self.logger.info(f"Loading configuration from {config_file}") + return Config.from_yaml(config_file) + self.logger.info("Loading configuration from environment variables") + return Config.from_env() + except Exception as e: + self.logger.error(f"Failed to load configuration: {e}") + raise def _authenticate_and_initialize_client(self) -> requests.Session: - """Authenticate and initialize the HTTP client with a valid token.""" - client = BackendApplicationClient(client_id=self.config.client_id) - oauth_session = OAuth2Session(client=client) - - # Fetch the token using client credentials - token_response = oauth_session.fetch_token( - token_url=self.config.token_url, - client_id=self.config.client_id, - client_secret=self.config.client_secret, - audience=self.config.audience, - ) - - self.token = token_response["access_token"] - self.token_expiry = datetime.now(timezone.utc) + timedelta( - seconds=token_response.get("expires_in", 3600) - ) - - # Initialize the HTTP session with the Authorization header - http_client = requests.Session() - http_client.headers.update({"Authorization": f"Bearer {self.token}"}) - - return http_client + """ + Authenticate and initialize the HTTP client with a valid token. + """ + try: + self.logger.info("Authenticating with the token endpoint") + client = BackendApplicationClient(client_id=self.config.client_id) + oauth_session = OAuth2Session(client=client) + + # Fetch the token using client credentials + token_response = oauth_session.fetch_token( + token_url=self.config.token_url, + client_id=self.config.client_id, + client_secret=self.config.client_secret, + audience=self.config.audience, + ) + + self.token = token_response["access_token"] + self.token_expiry = datetime.now(timezone.utc) + timedelta( + seconds=token_response.get("expires_in", 3600) + ) + self.logger.info("Authentication successful, token obtained") + + # Initialize the HTTP session with the Authorization header + http_client = requests.Session() + http_client.headers.update({"Authorization": f"Bearer {self.token}"}) + return http_client + except RequestException as e: + self.logger.error(f"Request failed during authentication: {e}") + raise + except Exception as e: + self.logger.error(f"Unexpected error during authentication: {e}") + raise def _refresh_token_if_needed(self): - """Refresh the token if it has expired.""" + """ + Refresh the token if it has expired. + """ if not self.token or self.token_expiry <= datetime.now(timezone.utc): + self.logger.info("Token expired or missing, refreshing token") self._http_client = self._authenticate_and_initialize_client() def get_http_client(self) -> requests.Session: - """Return the authenticated HTTP client, refreshing the token if necessary.""" + """ + Return the authenticated HTTP client, refreshing the token if necessary. + """ self._refresh_token_if_needed() return self._http_client - def request( - self, method: str, url: str, *args: Any, **kwargs: Any - ) -> requests.Response: - """Send an HTTP request using the authenticated session.""" + def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> requests.Response: + """ + Send an HTTP request using the authenticated session. + Logs request and response details. + """ self._refresh_token_if_needed() - return self._http_client.request(method, url, *args, **kwargs) + try: + self.logger.info(f"Making {method.upper()} request to {url}") + response = self._http_client.request(method, url, *args, **kwargs) + response.raise_for_status() + self.logger.info(f"Request to {url} succeeded with status {response.status_code}") + return response + except RequestException as e: + self.logger.error(f"HTTP request failed: {e}") + raise + except Exception as e: + self.logger.error(f"Unexpected error during HTTP request: {e}") + raise def get(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: - """Send a GET request using the authenticated session.""" + """ + Send a GET request using the authenticated session. + """ return self.request("GET", url, *args, **kwargs) def post(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: - """Send a POST request using the authenticated session.""" + """ + Send a POST request using the authenticated session. + """ return self.request("POST", url, *args, **kwargs) def put(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: - """Send a PUT request using the authenticated session.""" + """ + Send a PUT request using the authenticated session. + """ return self.request("PUT", url, *args, **kwargs) def delete(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: - """Send a DELETE request using the authenticated session.""" + """ + Send a DELETE request using the authenticated session. + """ return self.request("DELETE", url, *args, **kwargs) diff --git a/tests/unit/datacosmos/client/test_client_authentication.py b/tests/unit/datacosmos/client/test_client_authentication.py new file mode 100644 index 0000000..3aec902 --- /dev/null +++ b/tests/unit/datacosmos/client/test_client_authentication.py @@ -0,0 +1,53 @@ +from unittest.mock import patch, MagicMock +from datacosmos.client import DatacosmosClient +from config.config import Config + + +@patch("datacosmos.client.OAuth2Session.fetch_token") +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client", autospec=True) +def test_client_authentication(mock_auth_client, mock_fetch_token): + """ + Test that the client correctly fetches a token during authentication. + """ + # Mock the token response from OAuth2Session + mock_fetch_token.return_value = { + "access_token": "mock-access-token", + "expires_in": 3600, + } + + # Simulate _authenticate_and_initialize_client calling fetch_token + def mock_authenticate_and_initialize_client(self): + # Call the real fetch_token (simulated by the mock) + token_response = mock_fetch_token( + token_url=self.config.token_url, + client_id=self.config.client_id, + client_secret=self.config.client_secret, + audience=self.config.audience, + ) + self.token = token_response["access_token"] + self.token_expiry = "mock-expiry" + + # Attach the side effect to the mock + mock_auth_client.side_effect = mock_authenticate_and_initialize_client + + # Create a mock configuration + config = Config( + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + + # Initialize the client + client = DatacosmosClient(config=config) + + # Assertions + assert client.token == "mock-access-token" + assert client.token_expiry == "mock-expiry" + mock_fetch_token.assert_called_once_with( + token_url="https://mock.token.url/oauth/token", + client_id="test-client-id", + client_secret="test-client-secret", + audience="https://mock.audience", + ) + mock_auth_client.assert_called_once_with(client) diff --git a/tests/unit/datacosmos/client/test_client_initialization.py b/tests/unit/datacosmos/client/test_client_initialization.py new file mode 100644 index 0000000..e8ee89c --- /dev/null +++ b/tests/unit/datacosmos/client/test_client_initialization.py @@ -0,0 +1,28 @@ +from unittest.mock import patch, MagicMock +from datacosmos.client import DatacosmosClient +from config.config import Config + + +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch("os.path.exists", return_value=False) +@patch("config.Config.from_env") +def test_client_initialization(mock_from_env, mock_exists, mock_auth_client): + """ + Test that the client initializes correctly with environment variables and mocks the HTTP client. + """ + mock_config = Config( + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + mock_from_env.return_value = mock_config + mock_auth_client.return_value = MagicMock() # Mock the HTTP client + + client = DatacosmosClient() + + assert client.config == mock_config + assert client._http_client is not None # Ensure the HTTP client is mocked + mock_exists.assert_called_once_with("config/config.yaml") + mock_from_env.assert_called_once() + mock_auth_client.assert_called_once() diff --git a/tests/unit/datacosmos/client/test_client_token_refreshing.py b/tests/unit/datacosmos/client/test_client_token_refreshing.py new file mode 100644 index 0000000..5632d85 --- /dev/null +++ b/tests/unit/datacosmos/client/test_client_token_refreshing.py @@ -0,0 +1,50 @@ +from unittest.mock import patch, MagicMock +from datetime import datetime, timedelta, timezone +from datacosmos.client import DatacosmosClient +from config.config import Config + + +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +def test_token_refreshing(mock_auth_client): + """ + Test that the client refreshes the token when it expires. + """ + # Mock the HTTP client returned by _authenticate_and_initialize_client + mock_http_client = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"message": "success"} + mock_http_client.request.return_value = mock_response + mock_auth_client.return_value = mock_http_client + + config = Config( + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + + # Initialize the client (first call to _authenticate_and_initialize_client) + client = DatacosmosClient(config=config) + + # Simulate expired token + client.token_expiry = datetime.now(timezone.utc) - timedelta(seconds=1) + + # Make a GET request (should trigger token refresh) + response = client.get("https://mock.api/some-endpoint", headers={"Authorization": f"Bearer {client.token}"}) + + # Assertions + assert response.status_code == 200 + assert response.json() == {"message": "success"} + + # Verify _authenticate_and_initialize_client was called twice: + # 1. During initialization + # 2. During token refresh + assert mock_auth_client.call_count == 2 + + # Verify the request was made correctly + mock_http_client.request.assert_called_once_with( + "GET", + "https://mock.api/some-endpoint", + headers={"Authorization": f"Bearer {client.token}"}, + ) From df656da107c6602037d482c6a5388fe570effa1a Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 24 Jan 2025 12:37:27 +0000 Subject: [PATCH 027/135] Add more unit tests --- .gitignore | 3 ++ pyproject.toml | 16 ++++++++ .../client/test_client_delete_request.py | 33 ++++++++++++++++ .../client/test_client_get_request.py | 35 +++++++++++++++++ .../client/test_client_post_request.py | 38 +++++++++++++++++++ .../client/test_client_put_request.py | 38 +++++++++++++++++++ .../client/test_client_token_refreshing.py | 7 ++-- 7 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 tests/unit/datacosmos/client/test_client_delete_request.py create mode 100644 tests/unit/datacosmos/client/test_client_get_request.py create mode 100644 tests/unit/datacosmos/client/test_client_post_request.py create mode 100644 tests/unit/datacosmos/client/test_client_put_request.py diff --git a/.gitignore b/.gitignore index 2e300c0..534e783 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,6 @@ cython_debug/ # Ignore config.yaml config/config.yaml + +# Ignore .vscode +.vscode/ diff --git a/pyproject.toml b/pyproject.toml index 1042a6e..2edef52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ test = [ find = {} [tool.bandit] +skips = ["B101"] # Example: ignore assert statement usage if needed +targets = ["datacosmos"] # Adjust to point to your project directory [tool.pydocstyle] convention = "google" @@ -58,6 +60,7 @@ exclude = [ ".tox", ".venv", ".vscode", + "__pycache__", "__pypackages__", "_build", "buck-out", @@ -89,3 +92,16 @@ line-length = 88 [tool.isort] profile = "black" + +[tool.flake8] +max-line-length = 88 +exclude = [ + ".venv", + "__pycache__", + "build", + "dist", +] +ignore = [ + "E203", # Whitespace before ':', handled by Black + "W503", # Line break before binary operator +] diff --git a/tests/unit/datacosmos/client/test_client_delete_request.py b/tests/unit/datacosmos/client/test_client_delete_request.py new file mode 100644 index 0000000..030ee80 --- /dev/null +++ b/tests/unit/datacosmos/client/test_client_delete_request.py @@ -0,0 +1,33 @@ +from unittest.mock import patch, MagicMock +from datacosmos.client import DatacosmosClient +from config.config import Config + + +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +def test_delete_request(mock_auth_client): + """ + Test that the client performs a DELETE request correctly. + """ + # Mock the HTTP client + mock_http_client = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 204 + mock_http_client.request.return_value = mock_response + mock_auth_client.return_value = mock_http_client + + config = Config( + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + client = DatacosmosClient(config=config) + response = client.delete("https://mock.api/some-endpoint") + + # Assertions + assert response.status_code == 204 + mock_http_client.request.assert_called_once_with( + "DELETE", + "https://mock.api/some-endpoint" + ) + mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_get_request.py b/tests/unit/datacosmos/client/test_client_get_request.py new file mode 100644 index 0000000..2dc7b31 --- /dev/null +++ b/tests/unit/datacosmos/client/test_client_get_request.py @@ -0,0 +1,35 @@ +from unittest.mock import patch, MagicMock +from datacosmos.client import DatacosmosClient +from config.config import Config + + +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +def test_client_get_request(mock_auth_client): + """ + Test that the client performs a GET request correctly. + """ + # Mock the HTTP client + mock_http_client = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"message": "success"} + mock_http_client.request.return_value = mock_response + mock_auth_client.return_value = mock_http_client + + config = Config( + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + client = DatacosmosClient(config=config) + response = client.get("https://mock.api/some-endpoint") + + # Assertions + assert response.status_code == 200 + assert response.json() == {"message": "success"} + mock_http_client.request.assert_called_once_with( + "GET", + "https://mock.api/some-endpoint" + ) + mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_post_request.py b/tests/unit/datacosmos/client/test_client_post_request.py new file mode 100644 index 0000000..ac6a9b4 --- /dev/null +++ b/tests/unit/datacosmos/client/test_client_post_request.py @@ -0,0 +1,38 @@ +from unittest.mock import patch, MagicMock +from datacosmos.client import DatacosmosClient +from config.config import Config + + +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +def test_post_request(mock_auth_client): + """ + Test that the client performs a POST request correctly. + """ + # Mock the HTTP client + mock_http_client = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 201 + mock_response.json.return_value = {"message": "created"} + mock_http_client.request.return_value = mock_response + mock_auth_client.return_value = mock_http_client + + config = Config( + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + client = DatacosmosClient(config=config) + response = client.post( + "https://mock.api/some-endpoint", json={"key": "value"} + ) + + # Assertions + assert response.status_code == 201 + assert response.json() == {"message": "created"} + mock_http_client.request.assert_called_once_with( + "POST", + "https://mock.api/some-endpoint", + json={"key": "value"} + ) + mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_put_request.py b/tests/unit/datacosmos/client/test_client_put_request.py new file mode 100644 index 0000000..140260b --- /dev/null +++ b/tests/unit/datacosmos/client/test_client_put_request.py @@ -0,0 +1,38 @@ +from unittest.mock import patch, MagicMock +from datacosmos.client import DatacosmosClient +from config.config import Config + + +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +def test_put_request(mock_auth_client): + """ + Test that the client performs a PUT request correctly. + """ + # Mock the HTTP client + mock_http_client = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"message": "updated"} + mock_http_client.request.return_value = mock_response + mock_auth_client.return_value = mock_http_client + + config = Config( + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + client = DatacosmosClient(config=config) + response = client.put( + "https://mock.api/some-endpoint", json={"key": "updated-value"} + ) + + # Assertions + assert response.status_code == 200 + assert response.json() == {"message": "updated"} + mock_http_client.request.assert_called_once_with( + "PUT", + "https://mock.api/some-endpoint", + json={"key": "updated-value"} + ) + mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_token_refreshing.py b/tests/unit/datacosmos/client/test_client_token_refreshing.py index 5632d85..614d735 100644 --- a/tests/unit/datacosmos/client/test_client_token_refreshing.py +++ b/tests/unit/datacosmos/client/test_client_token_refreshing.py @@ -5,7 +5,7 @@ @patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") -def test_token_refreshing(mock_auth_client): +def test_client_token_refreshing(mock_auth_client): """ Test that the client refreshes the token when it expires. """ @@ -31,7 +31,7 @@ def test_token_refreshing(mock_auth_client): client.token_expiry = datetime.now(timezone.utc) - timedelta(seconds=1) # Make a GET request (should trigger token refresh) - response = client.get("https://mock.api/some-endpoint", headers={"Authorization": f"Bearer {client.token}"}) + response = client.get("https://mock.api/some-endpoint") # Assertions assert response.status_code == 200 @@ -45,6 +45,5 @@ def test_token_refreshing(mock_auth_client): # Verify the request was made correctly mock_http_client.request.assert_called_once_with( "GET", - "https://mock.api/some-endpoint", - headers={"Authorization": f"Bearer {client.token}"}, + "https://mock.api/some-endpoint" ) From d13ce9b1cc560ef205786d4f6a45573857cd9049 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 24 Jan 2025 16:29:19 +0000 Subject: [PATCH 028/135] Start fixing some of the issues reported by the pipeline --- datacosmos/client.py | 24 +++++++++++++------ .../client/test_client_authentication.py | 10 +++++--- .../client/test_client_delete_request.py | 12 ++++++---- .../client/test_client_get_request.py | 12 ++++++---- .../client/test_client_initialization.py | 9 ++++--- .../client/test_client_post_request.py | 13 +++++----- .../client/test_client_put_request.py | 13 +++++----- .../client/test_client_token_refreshing.py | 12 ++++++---- 8 files changed, 65 insertions(+), 40 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index d9a68a3..0b4c639 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -1,12 +1,12 @@ import logging import os from datetime import datetime, timedelta, timezone -from typing import Optional, Any +from typing import Any, Optional import requests -from requests_oauthlib import OAuth2Session from oauthlib.oauth2 import BackendApplicationClient from requests.exceptions import RequestException +from requests_oauthlib import OAuth2Session from config.config import Config @@ -18,7 +18,9 @@ class DatacosmosClient: """ def __init__( - self, config: Optional[Config] = None, config_file: str = "config/config.yaml" + self, + config: Optional[Config] = None, + config_file: str = "config/config.yaml", ): """ Initialize the DatacosmosClient. @@ -42,7 +44,9 @@ def _load_config(self, config_file: str) -> Config: if os.path.exists(config_file): self.logger.info(f"Loading configuration from {config_file}") return Config.from_yaml(config_file) - self.logger.info("Loading configuration from environment variables") + self.logger.info( + "Loading configuration from environment variables" + ) return Config.from_env() except Exception as e: self.logger.error(f"Failed to load configuration: {e}") @@ -73,7 +77,9 @@ def _authenticate_and_initialize_client(self) -> requests.Session: # Initialize the HTTP session with the Authorization header http_client = requests.Session() - http_client.headers.update({"Authorization": f"Bearer {self.token}"}) + http_client.headers.update( + {"Authorization": f"Bearer {self.token}"} + ) return http_client except RequestException as e: self.logger.error(f"Request failed during authentication: {e}") @@ -97,7 +103,9 @@ def get_http_client(self) -> requests.Session: self._refresh_token_if_needed() return self._http_client - def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> requests.Response: + def request( + self, method: str, url: str, *args: Any, **kwargs: Any + ) -> requests.Response: """ Send an HTTP request using the authenticated session. Logs request and response details. @@ -107,7 +115,9 @@ def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> requests. self.logger.info(f"Making {method.upper()} request to {url}") response = self._http_client.request(method, url, *args, **kwargs) response.raise_for_status() - self.logger.info(f"Request to {url} succeeded with status {response.status_code}") + self.logger.info( + f"Request to {url} succeeded with status {response.status_code}" + ) return response except RequestException as e: self.logger.error(f"HTTP request failed: {e}") diff --git a/tests/unit/datacosmos/client/test_client_authentication.py b/tests/unit/datacosmos/client/test_client_authentication.py index 3aec902..2216da3 100644 --- a/tests/unit/datacosmos/client/test_client_authentication.py +++ b/tests/unit/datacosmos/client/test_client_authentication.py @@ -1,10 +1,14 @@ -from unittest.mock import patch, MagicMock -from datacosmos.client import DatacosmosClient +from unittest.mock import MagicMock, patch + from config.config import Config +from datacosmos.client import DatacosmosClient @patch("datacosmos.client.OAuth2Session.fetch_token") -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client", autospec=True) +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client", + autospec=True, +) def test_client_authentication(mock_auth_client, mock_fetch_token): """ Test that the client correctly fetches a token during authentication. diff --git a/tests/unit/datacosmos/client/test_client_delete_request.py b/tests/unit/datacosmos/client/test_client_delete_request.py index 030ee80..6584d57 100644 --- a/tests/unit/datacosmos/client/test_client_delete_request.py +++ b/tests/unit/datacosmos/client/test_client_delete_request.py @@ -1,9 +1,12 @@ -from unittest.mock import patch, MagicMock -from datacosmos.client import DatacosmosClient +from unittest.mock import MagicMock, patch + from config.config import Config +from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) def test_delete_request(mock_auth_client): """ Test that the client performs a DELETE request correctly. @@ -27,7 +30,6 @@ def test_delete_request(mock_auth_client): # Assertions assert response.status_code == 204 mock_http_client.request.assert_called_once_with( - "DELETE", - "https://mock.api/some-endpoint" + "DELETE", "https://mock.api/some-endpoint" ) mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_get_request.py b/tests/unit/datacosmos/client/test_client_get_request.py index 2dc7b31..e2a2378 100644 --- a/tests/unit/datacosmos/client/test_client_get_request.py +++ b/tests/unit/datacosmos/client/test_client_get_request.py @@ -1,9 +1,12 @@ -from unittest.mock import patch, MagicMock -from datacosmos.client import DatacosmosClient +from unittest.mock import MagicMock, patch + from config.config import Config +from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) def test_client_get_request(mock_auth_client): """ Test that the client performs a GET request correctly. @@ -29,7 +32,6 @@ def test_client_get_request(mock_auth_client): assert response.status_code == 200 assert response.json() == {"message": "success"} mock_http_client.request.assert_called_once_with( - "GET", - "https://mock.api/some-endpoint" + "GET", "https://mock.api/some-endpoint" ) mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_initialization.py b/tests/unit/datacosmos/client/test_client_initialization.py index e8ee89c..bc4305d 100644 --- a/tests/unit/datacosmos/client/test_client_initialization.py +++ b/tests/unit/datacosmos/client/test_client_initialization.py @@ -1,9 +1,12 @@ -from unittest.mock import patch, MagicMock -from datacosmos.client import DatacosmosClient +from unittest.mock import MagicMock, patch + from config.config import Config +from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) @patch("os.path.exists", return_value=False) @patch("config.Config.from_env") def test_client_initialization(mock_from_env, mock_exists, mock_auth_client): diff --git a/tests/unit/datacosmos/client/test_client_post_request.py b/tests/unit/datacosmos/client/test_client_post_request.py index ac6a9b4..3fcf294 100644 --- a/tests/unit/datacosmos/client/test_client_post_request.py +++ b/tests/unit/datacosmos/client/test_client_post_request.py @@ -1,9 +1,12 @@ -from unittest.mock import patch, MagicMock -from datacosmos.client import DatacosmosClient +from unittest.mock import MagicMock, patch + from config.config import Config +from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) def test_post_request(mock_auth_client): """ Test that the client performs a POST request correctly. @@ -31,8 +34,6 @@ def test_post_request(mock_auth_client): assert response.status_code == 201 assert response.json() == {"message": "created"} mock_http_client.request.assert_called_once_with( - "POST", - "https://mock.api/some-endpoint", - json={"key": "value"} + "POST", "https://mock.api/some-endpoint", json={"key": "value"} ) mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_put_request.py b/tests/unit/datacosmos/client/test_client_put_request.py index 140260b..0454cd9 100644 --- a/tests/unit/datacosmos/client/test_client_put_request.py +++ b/tests/unit/datacosmos/client/test_client_put_request.py @@ -1,9 +1,12 @@ -from unittest.mock import patch, MagicMock -from datacosmos.client import DatacosmosClient +from unittest.mock import MagicMock, patch + from config.config import Config +from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) def test_put_request(mock_auth_client): """ Test that the client performs a PUT request correctly. @@ -31,8 +34,6 @@ def test_put_request(mock_auth_client): assert response.status_code == 200 assert response.json() == {"message": "updated"} mock_http_client.request.assert_called_once_with( - "PUT", - "https://mock.api/some-endpoint", - json={"key": "updated-value"} + "PUT", "https://mock.api/some-endpoint", json={"key": "updated-value"} ) mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_token_refreshing.py b/tests/unit/datacosmos/client/test_client_token_refreshing.py index 614d735..bb3baba 100644 --- a/tests/unit/datacosmos/client/test_client_token_refreshing.py +++ b/tests/unit/datacosmos/client/test_client_token_refreshing.py @@ -1,10 +1,13 @@ -from unittest.mock import patch, MagicMock from datetime import datetime, timedelta, timezone -from datacosmos.client import DatacosmosClient +from unittest.mock import MagicMock, patch + from config.config import Config +from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) def test_client_token_refreshing(mock_auth_client): """ Test that the client refreshes the token when it expires. @@ -44,6 +47,5 @@ def test_client_token_refreshing(mock_auth_client): # Verify the request was made correctly mock_http_client.request.assert_called_once_with( - "GET", - "https://mock.api/some-endpoint" + "GET", "https://mock.api/some-endpoint" ) From 319536ec0a182742e35f3938bfcba8effb336d8f Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 24 Jan 2025 16:35:50 +0000 Subject: [PATCH 029/135] Apply black --- datacosmos/client.py | 8 +- pyproject.toml | 86 ++++++------------- .../client/test_client_authentication.py | 2 +- .../client/test_client_delete_request.py | 4 +- .../client/test_client_get_request.py | 4 +- .../client/test_client_initialization.py | 4 +- .../client/test_client_post_request.py | 8 +- .../client/test_client_put_request.py | 4 +- .../client/test_client_token_refreshing.py | 4 +- 9 files changed, 37 insertions(+), 87 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index 0b4c639..0abf554 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -44,9 +44,7 @@ def _load_config(self, config_file: str) -> Config: if os.path.exists(config_file): self.logger.info(f"Loading configuration from {config_file}") return Config.from_yaml(config_file) - self.logger.info( - "Loading configuration from environment variables" - ) + self.logger.info("Loading configuration from environment variables") return Config.from_env() except Exception as e: self.logger.error(f"Failed to load configuration: {e}") @@ -77,9 +75,7 @@ def _authenticate_and_initialize_client(self) -> requests.Session: # Initialize the HTTP session with the Authorization header http_client = requests.Session() - http_client.headers.update( - {"Authorization": f"Bearer {self.token}"} - ) + http_client.headers.update({"Authorization": f"Bearer {self.token}"}) return http_client except RequestException as e: self.logger.error(f"Request failed during authentication: {e}") diff --git a/pyproject.toml b/pyproject.toml index 2edef52..03e2b94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,46 +1,15 @@ -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "datacosmos-sdk" -version = "0.0.1" -authors = [ - { name="Open Cosmos", email="support@open-cosmos.com" }, -] -description = "A library for interacting with DataCosmos from Python code" -requires-python = ">=3.8" -classifiers = [ - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", +# pytest configuration +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q" +testpaths = [ + "tests", ] -dependencies = [] - -[project.optional-dependencies] -test = [ - "black==22.12.0", - "flake8==6.0.0", - "pytest==7.2.0", - "bandit[toml]==1.7.4", - "isort==5.11.4", - "pydocstyle==6.1.1", - "flake8-cognitive-complexity==0.1.0", - "ruff==0.0.286" -] - -[tool.setuptools.packages] -find = {} -[tool.bandit] -skips = ["B101"] # Example: ignore assert statement usage if needed -targets = ["datacosmos"] # Adjust to point to your project directory - -[tool.pydocstyle] -convention = "google" +[tool.pyright] +typeCheckingMode = "basic" [tool.ruff] -fix = true -line-length = 88 exclude = [ ".bzr", ".direnv", @@ -60,7 +29,6 @@ exclude = [ ".tox", ".venv", ".vscode", - "__pycache__", "__pypackages__", "_build", "buck-out", @@ -71,9 +39,25 @@ exclude = [ "venv", ] +line-length = 79 +indent-width = 4 + [tool.ruff.lint] +# I - Sort imports +# SLF - Private member access +# F - pyflakes +# C90 - Mccabe complexity check +# ANN - Type hint annotations +# BLE - Do not allow blind excepts +# B - Standard bug bears in Python code +# ARG - unused arguments +# ERA - remove uncommented code extend-select = ["I", "SLF", "F", "C90", "BLE", "B", "ARG", "ERA"] + +# Ignored rules ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] @@ -82,26 +66,10 @@ quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" + +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. docstring-code-format = false [tool.ruff.lint.mccabe] max-complexity = 10 - -[tool.black] -line-length = 88 - -[tool.isort] -profile = "black" - -[tool.flake8] -max-line-length = 88 -exclude = [ - ".venv", - "__pycache__", - "build", - "dist", -] -ignore = [ - "E203", # Whitespace before ':', handled by Black - "W503", # Line break before binary operator -] diff --git a/tests/unit/datacosmos/client/test_client_authentication.py b/tests/unit/datacosmos/client/test_client_authentication.py index 2216da3..6d163b9 100644 --- a/tests/unit/datacosmos/client/test_client_authentication.py +++ b/tests/unit/datacosmos/client/test_client_authentication.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import patch from config.config import Config from datacosmos.client import DatacosmosClient diff --git a/tests/unit/datacosmos/client/test_client_delete_request.py b/tests/unit/datacosmos/client/test_client_delete_request.py index 6584d57..fbde09e 100644 --- a/tests/unit/datacosmos/client/test_client_delete_request.py +++ b/tests/unit/datacosmos/client/test_client_delete_request.py @@ -4,9 +4,7 @@ from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_delete_request(mock_auth_client): """ Test that the client performs a DELETE request correctly. diff --git a/tests/unit/datacosmos/client/test_client_get_request.py b/tests/unit/datacosmos/client/test_client_get_request.py index e2a2378..42ab1a6 100644 --- a/tests/unit/datacosmos/client/test_client_get_request.py +++ b/tests/unit/datacosmos/client/test_client_get_request.py @@ -4,9 +4,7 @@ from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_client_get_request(mock_auth_client): """ Test that the client performs a GET request correctly. diff --git a/tests/unit/datacosmos/client/test_client_initialization.py b/tests/unit/datacosmos/client/test_client_initialization.py index bc4305d..22ad4b6 100644 --- a/tests/unit/datacosmos/client/test_client_initialization.py +++ b/tests/unit/datacosmos/client/test_client_initialization.py @@ -4,9 +4,7 @@ from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") @patch("os.path.exists", return_value=False) @patch("config.Config.from_env") def test_client_initialization(mock_from_env, mock_exists, mock_auth_client): diff --git a/tests/unit/datacosmos/client/test_client_post_request.py b/tests/unit/datacosmos/client/test_client_post_request.py index 3fcf294..b3be698 100644 --- a/tests/unit/datacosmos/client/test_client_post_request.py +++ b/tests/unit/datacosmos/client/test_client_post_request.py @@ -4,9 +4,7 @@ from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_post_request(mock_auth_client): """ Test that the client performs a POST request correctly. @@ -26,9 +24,7 @@ def test_post_request(mock_auth_client): audience="https://mock.audience", ) client = DatacosmosClient(config=config) - response = client.post( - "https://mock.api/some-endpoint", json={"key": "value"} - ) + response = client.post("https://mock.api/some-endpoint", json={"key": "value"}) # Assertions assert response.status_code == 201 diff --git a/tests/unit/datacosmos/client/test_client_put_request.py b/tests/unit/datacosmos/client/test_client_put_request.py index 0454cd9..c7afd17 100644 --- a/tests/unit/datacosmos/client/test_client_put_request.py +++ b/tests/unit/datacosmos/client/test_client_put_request.py @@ -4,9 +4,7 @@ from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_put_request(mock_auth_client): """ Test that the client performs a PUT request correctly. diff --git a/tests/unit/datacosmos/client/test_client_token_refreshing.py b/tests/unit/datacosmos/client/test_client_token_refreshing.py index bb3baba..11951be 100644 --- a/tests/unit/datacosmos/client/test_client_token_refreshing.py +++ b/tests/unit/datacosmos/client/test_client_token_refreshing.py @@ -5,9 +5,7 @@ from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_client_token_refreshing(mock_auth_client): """ Test that the client refreshes the token when it expires. From e1bee71fec9610800c67a30eba88d0c8eba27656 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 24 Jan 2025 16:39:37 +0000 Subject: [PATCH 030/135] Revert changes in pyproject.toml --- pyproject.toml | 101 +++++++++++++++---------------------------------- 1 file changed, 31 insertions(+), 70 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 03e2b94..b06bf42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,75 +1,36 @@ -# pytest configuration -[tool.pytest.ini_options] -minversion = "6.0" -addopts = "-ra -q" -testpaths = [ - "tests", +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "datacosmos-sdk" +version = "0.0.1" +authors = [ + { name="Open Cosmos", email="support@open-cosmos.com" }, ] - -[tool.pyright] -typeCheckingMode = "basic" - -[tool.ruff] -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "site-packages", - "venv", +description = "A library for interacting with DataCosmos from Python code" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", +] +dependencies = [] + +[project.optional-dependencies] +test = [ + "black==22.12.0", + "flake8==6.0.0", + "pytest==7.2.0", + "bandit[toml]==1.7.4", + "isort==5.11.4", + "pydocstyle==6.1.1", + "flake8-cognitive-complexity==0.1.0", ] -line-length = 79 -indent-width = 4 - -[tool.ruff.lint] -# I - Sort imports -# SLF - Private member access -# F - pyflakes -# C90 - Mccabe complexity check -# ANN - Type hint annotations -# BLE - Do not allow blind excepts -# B - Standard bug bears in Python code -# ARG - unused arguments -# ERA - remove uncommented code -extend-select = ["I", "SLF", "F", "C90", "BLE", "B", "ARG", "ERA"] - -# Ignored rules -ignore = [] - -# Allow fix for all enabled rules (when `--fix`) is provided. -fixable = ["ALL"] -unfixable = [] - -[tool.ruff.format] -quote-style = "double" -indent-style = "space" -skip-magic-trailing-comma = false -line-ending = "auto" +[tool.setuptools.packages] +find = {} -# This is currently disabled by default, but it is planned for this -# to be opt-out in the future. -docstring-code-format = false +[tool.bandit] -[tool.ruff.lint.mccabe] -max-complexity = 10 +[tool.pydocstyle] +convention = "google" From 1e40bd69cbe8a85eae61aae3dbd9f726a9ae2cbc Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 24 Jan 2025 16:45:17 +0000 Subject: [PATCH 031/135] Apply pydocstyle --- config/__init__.py | 3 +- config/config.py | 3 +- datacosmos/client.py | 63 +++++++++---------- .../client/test_client_authentication.py | 4 +- .../client/test_client_delete_request.py | 4 +- .../client/test_client_get_request.py | 4 +- .../client/test_client_initialization.py | 5 +- .../client/test_client_post_request.py | 7 +-- .../client/test_client_put_request.py | 4 +- .../client/test_client_token_refreshing.py | 4 +- 10 files changed, 43 insertions(+), 58 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index e9778e9..ac196a0 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,6 +1,7 @@ """Configuration package for the Datacosmos SDK. -This package includes modules for loading and managing authentication configurations. +This package includes modules for loading and managing authentication +configurations. """ # Expose Config class for easier imports diff --git a/config/config.py b/config/config.py index c3a6624..de676bd 100644 --- a/config/config.py +++ b/config/config.py @@ -13,7 +13,8 @@ class Config: """Configuration for the Datacosmos SDK. - Contains authentication details such as client ID, secret, token URL, and audience. + Contains authentication details such as client ID, secret, token + URL, and audience. """ client_id: str diff --git a/datacosmos/client.py b/datacosmos/client.py index 0abf554..224b5bb 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -1,3 +1,9 @@ +"""DatacosmosClient handles authenticated interactions with the Datacosmos API. + +Automatically manages token refreshing and provides HTTP convenience +methods. +""" + import logging import os from datetime import datetime, timedelta, timezone @@ -12,9 +18,10 @@ class DatacosmosClient: - """ - DatacosmosClient handles authenticated interactions with the Datacosmos API. - Automatically manages token refreshing and provides HTTP convenience methods. + """DatacosmosClient handles authenticated interactions with the Datacosmos API. + + Automatically manages token refreshing and provides HTTP convenience + methods. """ def __init__( @@ -22,11 +29,10 @@ def __init__( config: Optional[Config] = None, config_file: str = "config/config.yaml", ): - """ - Initialize the DatacosmosClient. + """Initialize the DatacosmosClient. - If no configuration is provided, it will load from the specified YAML file - or fall back to environment variables. + If no configuration is provided, it will load from the specified + YAML file or fall back to environment variables. """ self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.INFO) @@ -37,23 +43,23 @@ def __init__( self._http_client = self._authenticate_and_initialize_client() def _load_config(self, config_file: str) -> Config: - """ - Load configuration from the YAML file. Fall back to environment variables if the file is missing. + """Load configuration from the YAML file. + + Fall back to environment variables if the file is missing. """ try: if os.path.exists(config_file): self.logger.info(f"Loading configuration from {config_file}") return Config.from_yaml(config_file) - self.logger.info("Loading configuration from environment variables") + self.logger.info( + "Loading configuration from environment variables") return Config.from_env() except Exception as e: self.logger.error(f"Failed to load configuration: {e}") raise def _authenticate_and_initialize_client(self) -> requests.Session: - """ - Authenticate and initialize the HTTP client with a valid token. - """ + """Authenticate and initialize the HTTP client with a valid token.""" try: self.logger.info("Authenticating with the token endpoint") client = BackendApplicationClient(client_id=self.config.client_id) @@ -75,7 +81,8 @@ def _authenticate_and_initialize_client(self) -> requests.Session: # Initialize the HTTP session with the Authorization header http_client = requests.Session() - http_client.headers.update({"Authorization": f"Bearer {self.token}"}) + http_client.headers.update( + {"Authorization": f"Bearer {self.token}"}) return http_client except RequestException as e: self.logger.error(f"Request failed during authentication: {e}") @@ -85,25 +92,21 @@ def _authenticate_and_initialize_client(self) -> requests.Session: raise def _refresh_token_if_needed(self): - """ - Refresh the token if it has expired. - """ + """Refresh the token if it has expired.""" if not self.token or self.token_expiry <= datetime.now(timezone.utc): self.logger.info("Token expired or missing, refreshing token") self._http_client = self._authenticate_and_initialize_client() def get_http_client(self) -> requests.Session: - """ - Return the authenticated HTTP client, refreshing the token if necessary. - """ + """Return the authenticated HTTP client, refreshing the token if necessary.""" self._refresh_token_if_needed() return self._http_client def request( self, method: str, url: str, *args: Any, **kwargs: Any ) -> requests.Response: - """ - Send an HTTP request using the authenticated session. + """Send an HTTP request using the authenticated session. + Logs request and response details. """ self._refresh_token_if_needed() @@ -123,25 +126,17 @@ def request( raise def get(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: - """ - Send a GET request using the authenticated session. - """ + """Send a GET request using the authenticated session.""" return self.request("GET", url, *args, **kwargs) def post(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: - """ - Send a POST request using the authenticated session. - """ + """Send a POST request using the authenticated session.""" return self.request("POST", url, *args, **kwargs) def put(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: - """ - Send a PUT request using the authenticated session. - """ + """Send a PUT request using the authenticated session.""" return self.request("PUT", url, *args, **kwargs) def delete(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: - """ - Send a DELETE request using the authenticated session. - """ + """Send a DELETE request using the authenticated session.""" return self.request("DELETE", url, *args, **kwargs) diff --git a/tests/unit/datacosmos/client/test_client_authentication.py b/tests/unit/datacosmos/client/test_client_authentication.py index 6d163b9..2a1cfc5 100644 --- a/tests/unit/datacosmos/client/test_client_authentication.py +++ b/tests/unit/datacosmos/client/test_client_authentication.py @@ -10,9 +10,7 @@ autospec=True, ) def test_client_authentication(mock_auth_client, mock_fetch_token): - """ - Test that the client correctly fetches a token during authentication. - """ + """Test that the client correctly fetches a token during authentication.""" # Mock the token response from OAuth2Session mock_fetch_token.return_value = { "access_token": "mock-access-token", diff --git a/tests/unit/datacosmos/client/test_client_delete_request.py b/tests/unit/datacosmos/client/test_client_delete_request.py index fbde09e..817a3f0 100644 --- a/tests/unit/datacosmos/client/test_client_delete_request.py +++ b/tests/unit/datacosmos/client/test_client_delete_request.py @@ -6,9 +6,7 @@ @patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_delete_request(mock_auth_client): - """ - Test that the client performs a DELETE request correctly. - """ + """Test that the client performs a DELETE request correctly.""" # Mock the HTTP client mock_http_client = MagicMock() mock_response = MagicMock() diff --git a/tests/unit/datacosmos/client/test_client_get_request.py b/tests/unit/datacosmos/client/test_client_get_request.py index 42ab1a6..034bc97 100644 --- a/tests/unit/datacosmos/client/test_client_get_request.py +++ b/tests/unit/datacosmos/client/test_client_get_request.py @@ -6,9 +6,7 @@ @patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_client_get_request(mock_auth_client): - """ - Test that the client performs a GET request correctly. - """ + """Test that the client performs a GET request correctly.""" # Mock the HTTP client mock_http_client = MagicMock() mock_response = MagicMock() diff --git a/tests/unit/datacosmos/client/test_client_initialization.py b/tests/unit/datacosmos/client/test_client_initialization.py index 22ad4b6..dc98646 100644 --- a/tests/unit/datacosmos/client/test_client_initialization.py +++ b/tests/unit/datacosmos/client/test_client_initialization.py @@ -8,9 +8,8 @@ @patch("os.path.exists", return_value=False) @patch("config.Config.from_env") def test_client_initialization(mock_from_env, mock_exists, mock_auth_client): - """ - Test that the client initializes correctly with environment variables and mocks the HTTP client. - """ + """Test that the client initializes correctly with environment variables + and mocks the HTTP client.""" mock_config = Config( client_id="test-client-id", client_secret="test-client-secret", diff --git a/tests/unit/datacosmos/client/test_client_post_request.py b/tests/unit/datacosmos/client/test_client_post_request.py index b3be698..39378db 100644 --- a/tests/unit/datacosmos/client/test_client_post_request.py +++ b/tests/unit/datacosmos/client/test_client_post_request.py @@ -6,9 +6,7 @@ @patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_post_request(mock_auth_client): - """ - Test that the client performs a POST request correctly. - """ + """Test that the client performs a POST request correctly.""" # Mock the HTTP client mock_http_client = MagicMock() mock_response = MagicMock() @@ -24,7 +22,8 @@ def test_post_request(mock_auth_client): audience="https://mock.audience", ) client = DatacosmosClient(config=config) - response = client.post("https://mock.api/some-endpoint", json={"key": "value"}) + response = client.post( + "https://mock.api/some-endpoint", json={"key": "value"}) # Assertions assert response.status_code == 201 diff --git a/tests/unit/datacosmos/client/test_client_put_request.py b/tests/unit/datacosmos/client/test_client_put_request.py index c7afd17..f910d22 100644 --- a/tests/unit/datacosmos/client/test_client_put_request.py +++ b/tests/unit/datacosmos/client/test_client_put_request.py @@ -6,9 +6,7 @@ @patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_put_request(mock_auth_client): - """ - Test that the client performs a PUT request correctly. - """ + """Test that the client performs a PUT request correctly.""" # Mock the HTTP client mock_http_client = MagicMock() mock_response = MagicMock() diff --git a/tests/unit/datacosmos/client/test_client_token_refreshing.py b/tests/unit/datacosmos/client/test_client_token_refreshing.py index 11951be..0e6a113 100644 --- a/tests/unit/datacosmos/client/test_client_token_refreshing.py +++ b/tests/unit/datacosmos/client/test_client_token_refreshing.py @@ -7,9 +7,7 @@ @patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_client_token_refreshing(mock_auth_client): - """ - Test that the client refreshes the token when it expires. - """ + """Test that the client refreshes the token when it expires.""" # Mock the HTTP client returned by _authenticate_and_initialize_client mock_http_client = MagicMock() mock_response = MagicMock() From 0f7da2504192d02892082253768643f4de491ac1 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 12:19:43 +0000 Subject: [PATCH 032/135] Apply changes to workflow file --- .github/workflows/main.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 2d4165d..feec28c 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -20,12 +20,17 @@ jobs: python-version: "3.10" - name: install dependencies run: pip install .[test] + - name: auto-fix with black and isort + run: | + black . --check --quiet || black . + isort . --check-only --quiet || isort . - name: lint uses: wearerequired/lint-action@v2 with: continue_on_error: false black: true flake8: true + bandit: name: bandit runs-on: ubuntu-latest @@ -39,6 +44,7 @@ jobs: run: pip install .[test] - name: bandit run: bandit -r -c pyproject.toml . + cognitive: name: cognitive runs-on: ubuntu-latest @@ -52,6 +58,7 @@ jobs: run: pip install .[test] - name: cognitive run: flake8 . --max-cognitive-complexity=5 + isort: name: isort runs-on: ubuntu-latest @@ -65,6 +72,7 @@ jobs: run: pip install .[test] - name: isort uses: isort/isort-action@v1.1.0 + pydocstyle: name: pydocstyle runs-on: ubuntu-latest @@ -78,6 +86,7 @@ jobs: run: pip install .[test] - name: pydocstyle run: pydocstyle . + test: name: test runs-on: ubuntu-latest @@ -95,6 +104,7 @@ jobs: run: pip install .[test] - name: test run: python -m pytest + release: name: tag, changelog, release, publish runs-on: ubuntu-latest From 03fe4d5ea2b081e537c10d04fa23a1f1440c1e83 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 12:25:46 +0000 Subject: [PATCH 033/135] Attempt to fix line too long error --- datacosmos/client.py | 6 ++++-- tests/unit/datacosmos/client/test_client_delete_request.py | 4 +++- tests/unit/datacosmos/client/test_client_get_request.py | 4 +++- tests/unit/datacosmos/client/test_client_initialization.py | 4 +++- tests/unit/datacosmos/client/test_client_post_request.py | 7 +++++-- tests/unit/datacosmos/client/test_client_put_request.py | 4 +++- .../unit/datacosmos/client/test_client_token_refreshing.py | 4 +++- 7 files changed, 24 insertions(+), 9 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index 224b5bb..4c55987 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -52,7 +52,8 @@ def _load_config(self, config_file: str) -> Config: self.logger.info(f"Loading configuration from {config_file}") return Config.from_yaml(config_file) self.logger.info( - "Loading configuration from environment variables") + "Loading configuration from environment variables" + ) return Config.from_env() except Exception as e: self.logger.error(f"Failed to load configuration: {e}") @@ -82,7 +83,8 @@ def _authenticate_and_initialize_client(self) -> requests.Session: # Initialize the HTTP session with the Authorization header http_client = requests.Session() http_client.headers.update( - {"Authorization": f"Bearer {self.token}"}) + {"Authorization": f"Bearer {self.token}"} + ) return http_client except RequestException as e: self.logger.error(f"Request failed during authentication: {e}") diff --git a/tests/unit/datacosmos/client/test_client_delete_request.py b/tests/unit/datacosmos/client/test_client_delete_request.py index 817a3f0..0495ed8 100644 --- a/tests/unit/datacosmos/client/test_client_delete_request.py +++ b/tests/unit/datacosmos/client/test_client_delete_request.py @@ -4,7 +4,9 @@ from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) def test_delete_request(mock_auth_client): """Test that the client performs a DELETE request correctly.""" # Mock the HTTP client diff --git a/tests/unit/datacosmos/client/test_client_get_request.py b/tests/unit/datacosmos/client/test_client_get_request.py index 034bc97..9b05c12 100644 --- a/tests/unit/datacosmos/client/test_client_get_request.py +++ b/tests/unit/datacosmos/client/test_client_get_request.py @@ -4,7 +4,9 @@ from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) def test_client_get_request(mock_auth_client): """Test that the client performs a GET request correctly.""" # Mock the HTTP client diff --git a/tests/unit/datacosmos/client/test_client_initialization.py b/tests/unit/datacosmos/client/test_client_initialization.py index dc98646..fea667c 100644 --- a/tests/unit/datacosmos/client/test_client_initialization.py +++ b/tests/unit/datacosmos/client/test_client_initialization.py @@ -4,7 +4,9 @@ from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) @patch("os.path.exists", return_value=False) @patch("config.Config.from_env") def test_client_initialization(mock_from_env, mock_exists, mock_auth_client): diff --git a/tests/unit/datacosmos/client/test_client_post_request.py b/tests/unit/datacosmos/client/test_client_post_request.py index 39378db..dab34db 100644 --- a/tests/unit/datacosmos/client/test_client_post_request.py +++ b/tests/unit/datacosmos/client/test_client_post_request.py @@ -4,7 +4,9 @@ from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) def test_post_request(mock_auth_client): """Test that the client performs a POST request correctly.""" # Mock the HTTP client @@ -23,7 +25,8 @@ def test_post_request(mock_auth_client): ) client = DatacosmosClient(config=config) response = client.post( - "https://mock.api/some-endpoint", json={"key": "value"}) + "https://mock.api/some-endpoint", json={"key": "value"} + ) # Assertions assert response.status_code == 201 diff --git a/tests/unit/datacosmos/client/test_client_put_request.py b/tests/unit/datacosmos/client/test_client_put_request.py index f910d22..479dbaf 100644 --- a/tests/unit/datacosmos/client/test_client_put_request.py +++ b/tests/unit/datacosmos/client/test_client_put_request.py @@ -4,7 +4,9 @@ from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) def test_put_request(mock_auth_client): """Test that the client performs a PUT request correctly.""" # Mock the HTTP client diff --git a/tests/unit/datacosmos/client/test_client_token_refreshing.py b/tests/unit/datacosmos/client/test_client_token_refreshing.py index 0e6a113..a6abd05 100644 --- a/tests/unit/datacosmos/client/test_client_token_refreshing.py +++ b/tests/unit/datacosmos/client/test_client_token_refreshing.py @@ -5,7 +5,9 @@ from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" +) def test_client_token_refreshing(mock_auth_client): """Test that the client refreshes the token when it expires.""" # Mock the HTTP client returned by _authenticate_and_initialize_client From 311b789ab251f48fb82deab56e2c8f4c6249a480 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 12:41:46 +0000 Subject: [PATCH 034/135] another attempt to fix line too long error --- datacosmos/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index 4c55987..1a219ef 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -100,7 +100,8 @@ def _refresh_token_if_needed(self): self._http_client = self._authenticate_and_initialize_client() def get_http_client(self) -> requests.Session: - """Return the authenticated HTTP client, refreshing the token if necessary.""" + """Return the authenticated HTTP client, + refreshing the token if necessary.""" self._refresh_token_if_needed() return self._http_client @@ -117,7 +118,8 @@ def request( response = self._http_client.request(method, url, *args, **kwargs) response.raise_for_status() self.logger.info( - f"Request to {url} succeeded with status {response.status_code}" + f"Request to {url} succeeded + with status {response.status_code}" ) return response except RequestException as e: From eb2d7c6ab33ede7c9d0ab0c652a0787264439c0a Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 12:44:29 +0000 Subject: [PATCH 035/135] rollback changes --- datacosmos/client.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index 1a219ef..4c55987 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -100,8 +100,7 @@ def _refresh_token_if_needed(self): self._http_client = self._authenticate_and_initialize_client() def get_http_client(self) -> requests.Session: - """Return the authenticated HTTP client, - refreshing the token if necessary.""" + """Return the authenticated HTTP client, refreshing the token if necessary.""" self._refresh_token_if_needed() return self._http_client @@ -118,8 +117,7 @@ def request( response = self._http_client.request(method, url, *args, **kwargs) response.raise_for_status() self.logger.info( - f"Request to {url} succeeded - with status {response.status_code}" + f"Request to {url} succeeded with status {response.status_code}" ) return response except RequestException as e: From 424f914507a2d748c29fdd7df398a792dc34ff31 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 12:51:33 +0000 Subject: [PATCH 036/135] Make bandit ignore B105 --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index feec28c..c5b6e5e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -43,7 +43,7 @@ jobs: - name: install dependencies run: pip install .[test] - name: bandit - run: bandit -r -c pyproject.toml . + run: bandit -r -c pyproject.toml . --exclude B105 cognitive: name: cognitive From dcaed8f6353184aa2fa143a52c3e23c26c6021ce Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 12:53:56 +0000 Subject: [PATCH 037/135] Another attempt to make bandit ignore B105 --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index c5b6e5e..78cace4 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -43,7 +43,7 @@ jobs: - name: install dependencies run: pip install .[test] - name: bandit - run: bandit -r -c pyproject.toml . --exclude B105 + run: bandit -r -c pyproject.toml . --skip B105 cognitive: name: cognitive From d9c207bcef59926e5287ba3b9c101a3e1027ca29 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 12:58:48 +0000 Subject: [PATCH 038/135] Make bandit skip more unrelevant check --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 78cace4..3f28c2c 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -43,7 +43,7 @@ jobs: - name: install dependencies run: pip install .[test] - name: bandit - run: bandit -r -c pyproject.toml . --skip B105 + run: bandit -r -c pyproject.toml . --skip B105 B106 B101 cognitive: name: cognitive From dcfce9093130258fe5358c05e1f555256282126d Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 13:00:14 +0000 Subject: [PATCH 039/135] Fix syntax main.yaml --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 3f28c2c..4dfa7be 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -43,7 +43,7 @@ jobs: - name: install dependencies run: pip install .[test] - name: bandit - run: bandit -r -c pyproject.toml . --skip B105 B106 B101 + run: bandit -r -c pyproject.toml . --skip B105,B106,B101 cognitive: name: cognitive From 57d3c7af382cae12a87f7cc4012eb8df4b0d374e Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 13:13:18 +0000 Subject: [PATCH 040/135] Attempt to fix line too long for client.py --- .github/workflows/main.yaml | 2 +- datacosmos/client.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 4dfa7be..9577de8 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -57,7 +57,7 @@ jobs: - name: install dependencies run: pip install .[test] - name: cognitive - run: flake8 . --max-cognitive-complexity=5 + run: flake8 . --max-cognitive-complexity=5 --exclude=test_*.py isort: name: isort diff --git a/datacosmos/client.py b/datacosmos/client.py index 4c55987..e1655ed 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -49,7 +49,9 @@ def _load_config(self, config_file: str) -> Config: """ try: if os.path.exists(config_file): - self.logger.info(f"Loading configuration from {config_file}") + self.logger.info( + f"Loading configuration from {config_file}" + ) return Config.from_yaml(config_file) self.logger.info( "Loading configuration from environment variables" From 2dc23a5c357ed7774aca429a2fa2e15120189928 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 13:16:13 +0000 Subject: [PATCH 041/135] Attempt to exclude files from flake8 --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b06bf42..5ed9e1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,5 +32,8 @@ find = {} [tool.bandit] +[tool.flake8] +exclude = [tests/*, build/*] + [tool.pydocstyle] convention = "google" From 0051dc3d0f912888d0ddef7112764d4496e5fe08 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 13:17:35 +0000 Subject: [PATCH 042/135] Fix syntax in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5ed9e1e..7d3739b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ find = {} [tool.bandit] [tool.flake8] -exclude = [tests/*, build/*] +exclude = ["tests/*", "build/*"] [tool.pydocstyle] convention = "google" From 2c0ac0a52a6c9b2c57baf995331f59a33f140d04 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 13:21:37 +0000 Subject: [PATCH 043/135] Another attempt to exclude dirs from flake8 --- .github/workflows/main.yaml | 1 + pyproject.toml | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 9577de8..5879c37 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -30,6 +30,7 @@ jobs: continue_on_error: false black: true flake8: true + flake8-options: "--exclude tests/*,build/*" bandit: name: bandit diff --git a/pyproject.toml b/pyproject.toml index 7d3739b..7ceaa54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,8 +32,6 @@ find = {} [tool.bandit] -[tool.flake8] -exclude = ["tests/*", "build/*"] [tool.pydocstyle] convention = "google" From b3326a0643f42ba6967b5cfad7c9730468a9c6e7 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 14:42:25 +0000 Subject: [PATCH 044/135] Another attempt to exclude dirs from flake8 --- .github/workflows/main.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5879c37..da038a2 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -30,7 +30,6 @@ jobs: continue_on_error: false black: true flake8: true - flake8-options: "--exclude tests/*,build/*" bandit: name: bandit @@ -58,7 +57,7 @@ jobs: - name: install dependencies run: pip install .[test] - name: cognitive - run: flake8 . --max-cognitive-complexity=5 --exclude=test_*.py + run: flake8 . --max-cognitive-complexity=5 --exclude=tests/*,build/* isort: name: isort From 5da1516d41cdf09089b0c93bdb4e27f63db1f374 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 15:05:27 +0000 Subject: [PATCH 045/135] Another attempt to exclude dirs from flake8 --- .github/workflows/main.yaml | 2 +- pyproject.toml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index da038a2..4dfa7be 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -57,7 +57,7 @@ jobs: - name: install dependencies run: pip install .[test] - name: cognitive - run: flake8 . --max-cognitive-complexity=5 --exclude=tests/*,build/* + run: flake8 . --max-cognitive-complexity=5 isort: name: isort diff --git a/pyproject.toml b/pyproject.toml index 7ceaa54..f97fce2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,6 @@ find = {} [tool.pydocstyle] convention = "google" + +[tool.flake8] +exclude = ["build/*"] From c20be933a872f658f5c9d0a60c745a810a84b961 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 15:18:48 +0000 Subject: [PATCH 046/135] Make flake8 not deal with line too long errors --- datacosmos/client.py | 4 +--- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index e1655ed..4c55987 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -49,9 +49,7 @@ def _load_config(self, config_file: str) -> Config: """ try: if os.path.exists(config_file): - self.logger.info( - f"Loading configuration from {config_file}" - ) + self.logger.info(f"Loading configuration from {config_file}") return Config.from_yaml(config_file) self.logger.info( "Loading configuration from environment variables" diff --git a/pyproject.toml b/pyproject.toml index f97fce2..e7d3ee7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,4 +37,4 @@ find = {} convention = "google" [tool.flake8] -exclude = ["build/*"] +ignore = "E501" From ee823b66f6fdb3a6ddd7631d180b54faa8d3091c Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 15:22:37 +0000 Subject: [PATCH 047/135] Attempt to make flake8 run from main.yaml taking in consideration pyproject.toml file --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 4dfa7be..6374074 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -57,7 +57,7 @@ jobs: - name: install dependencies run: pip install .[test] - name: cognitive - run: flake8 . --max-cognitive-complexity=5 + run: flake8 . --max-cognitive-complexity=5 --config=pyproject.toml isort: name: isort From bb372d997d85d936a2269ae6776cf1983d483981 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 15:25:59 +0000 Subject: [PATCH 048/135] Another attempt to make flake8 ignore E501 --- .github/workflows/main.yaml | 2 +- pyproject.toml | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 6374074..888adcf 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -57,7 +57,7 @@ jobs: - name: install dependencies run: pip install .[test] - name: cognitive - run: flake8 . --max-cognitive-complexity=5 --config=pyproject.toml + run: flake8 . --max-cognitive-complexity=5 --ignore=E501 isort: name: isort diff --git a/pyproject.toml b/pyproject.toml index e7d3ee7..7ceaa54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,3 @@ find = {} [tool.pydocstyle] convention = "google" - -[tool.flake8] -ignore = "E501" From 34d9c1491400612b9dd0fd8327e9980e6610e7b5 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 15:40:29 +0000 Subject: [PATCH 049/135] Attempt to make flake8 job ignore E501 --- .github/workflows/main.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 888adcf..e48f2be 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -30,6 +30,7 @@ jobs: continue_on_error: false black: true flake8: true + flake8-options: -ignore E501 bandit: name: bandit From 3bf00389ba05a131a43d83c4131fec8462a83e51 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 15:42:00 +0000 Subject: [PATCH 050/135] Attempt to make flake8 job ignore E501 --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index e48f2be..5926011 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -30,7 +30,7 @@ jobs: continue_on_error: false black: true flake8: true - flake8-options: -ignore E501 + flake8-options: "--ignore=E501" bandit: name: bandit From e1bbed82357a9c6d388d5d6f0630c476a2f1d20f Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 15:49:00 +0000 Subject: [PATCH 051/135] Attempt to make flake8 job ignore E501 --- .flake8 | 2 ++ .github/workflows/main.yaml | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..16520fc --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = E501 \ No newline at end of file diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5926011..888adcf 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -30,7 +30,6 @@ jobs: continue_on_error: false black: true flake8: true - flake8-options: "--ignore=E501" bandit: name: bandit From cbde259b16a41276c242e6a88555eba5ae594862 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 15:51:59 +0000 Subject: [PATCH 052/135] Add requests package to dependencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 7ceaa54..6aa4172 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ test = [ "isort==5.11.4", "pydocstyle==6.1.1", "flake8-cognitive-complexity==0.1.0", + "requests==2.26.0", ] [tool.setuptools.packages] From 101e1860fbd4de19bdbf54fda61c4e1680d8eaa3 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 15:54:18 +0000 Subject: [PATCH 053/135] Add oauthlib to the dependencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6aa4172..2d04c4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ test = [ "pydocstyle==6.1.1", "flake8-cognitive-complexity==0.1.0", "requests==2.26.0", + "oauthlib==3.1.1", ] [tool.setuptools.packages] From a7e17b5682d1662de0079ffd28cfdbcff1700d64 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 27 Jan 2025 15:56:28 +0000 Subject: [PATCH 054/135] Add requests-oauthlib to dependencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 2d04c4e..595de06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ test = [ "flake8-cognitive-complexity==0.1.0", "requests==2.26.0", "oauthlib==3.1.1", + "requests-oauthlib==1.3.0", ] [tool.setuptools.packages] From c625a544c4e503ba506a0f0d259df0d0ed54ec77 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 31 Jan 2025 11:49:31 +0000 Subject: [PATCH 055/135] Refactor of authentication client --- config/config.py | 111 ++++++++++++------ config/models/m2m_authentication_config.py | 24 ++++ config/models/url.py | 34 ++++++ datacosmos/client.py | 43 ++----- .../client/test_client_authentication.py | 22 ++-- .../client/test_client_delete_request.py | 19 +-- .../client/test_client_get_request.py | 19 +-- .../client/test_client_initialization.py | 25 ++-- .../client/test_client_post_request.py | 23 ++-- .../client/test_client_put_request.py | 19 +-- .../client/test_client_token_refreshing.py | 18 +-- 11 files changed, 222 insertions(+), 135 deletions(-) create mode 100644 config/models/m2m_authentication_config.py create mode 100644 config/models/url.py diff --git a/config/config.py b/config/config.py index de676bd..4022e13 100644 --- a/config/config.py +++ b/config/config.py @@ -1,52 +1,87 @@ -"""Module for managing configuration settings for the Datacosmos SDK. +"""Configuration module for the Datacosmos SDK. -Supports loading from YAML files and environment variables. +Handles configuration management using Pydantic and Pydantic Settings. +It loads default values, allows overrides via YAML configuration files, +and supports environment variable-based overrides. """ import os -from dataclasses import dataclass +from typing import Literal import yaml +from pydantic import model_validator +from pydantic_settings import BaseSettings, SettingsConfigDict +from config.models.m2m_authentication_config import M2MAuthenticationConfig +from config.models.url import URL -@dataclass -class Config: - """Configuration for the Datacosmos SDK. - Contains authentication details such as client ID, secret, token - URL, and audience. - """ +class Config(BaseSettings): + """Centralized configuration for the Datacosmos SDK.""" - client_id: str - client_secret: str - token_url: str - audience: str + model_config = SettingsConfigDict( + env_nested_delimiter="__", + nested_model_default_partial_update=True, + ) - @staticmethod - def from_yaml(file_path: str = "config/config.yaml") -> "Config": - """Load configuration from a YAML file. + # General configurations + environment: Literal["local", "test", "prod"] = "test" + log_format: Literal["json", "text"] = "text" + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO" - Defaults to 'config/config.yaml' unless otherwise specified. + # Authentication configuration + authentication: M2MAuthenticationConfig = M2MAuthenticationConfig( + type="m2m", + client_id="zCeZWJamwnb8ZIQEK35rhx0hSAjsZI4D", + token_url="https://login.open-cosmos.com/oauth/token", + audience="https://test.beeapp.open-cosmos.com", + client_secret="tAeaSgLds7g535ofGq79Zm2DSbWMCOsuRyY5lbyObJe9eAeSN_fxoy-5kaXnVSYa", + ) + + # STAC API configuration + stac: URL = URL( + protocol="https", + host="test.app.open-cosmos.com", + port=443, + path="/api/data/v0/stac", + ) + + @classmethod + def from_yaml(cls, file_path: str = "config/config.yaml") -> "Config": + """Load configuration from a YAML file and override defaults. + + Args: + file_path (str): The path to the YAML configuration file. + + Returns: + Config: An instance of the Config class with loaded settings. """ - with open(file_path, "r") as f: - data = yaml.safe_load(f) - auth = data.get("auth", {}) - return Config( - client_id=auth["client-id"], - client_secret=auth["client-secret"], - token_url=auth["token-url"], - audience=auth["audience"], - ) - - @staticmethod - def from_env() -> "Config": - """Load configuration from environment variables. - - Raises an exception if any required variable is missing. + config_data = {} + if os.path.exists(file_path): + with open(file_path, "r") as f: + yaml_data = yaml.safe_load(f) or {} + # Remove empty values from YAML to avoid overwriting with `None` + config_data = { + k: v for k, v in yaml_data.items() if v not in [None, ""] + } + return cls(**config_data) + + @model_validator(mode="before") + @classmethod + def merge_with_env(cls, values): + """Override settings with environment variables if set. + + This method checks if any environment variables corresponding to the + config fields are set and updates their values accordingly. + + Args: + values (dict): The configuration values before validation. + + Returns: + dict: The updated configuration values with environment variable overrides. """ - return Config( - client_id=os.getenv("OC_AUTH_CLIENT_ID"), - client_secret=os.getenv("OC_AUTH_CLIENT_SECRET"), - token_url=os.getenv("OC_AUTH_TOKEN_URL"), - audience=os.getenv("OC_AUTH_AUDIENCE"), - ) + for field in cls.model_fields: + env_value = os.getenv(f"OC_{field.upper()}") + if env_value: + values[field] = env_value + return values \ No newline at end of file diff --git a/config/models/m2m_authentication_config.py b/config/models/m2m_authentication_config.py new file mode 100644 index 0000000..028387a --- /dev/null +++ b/config/models/m2m_authentication_config.py @@ -0,0 +1,24 @@ +"""Module for configuring machine-to-machine (M2M) authentication. + +Used when running scripts in the cluster that require automated authentication +without user interaction. +""" + +from typing import Literal + +from pydantic import BaseModel + + +class M2MAuthenticationConfig(BaseModel): + """Configuration for machine-to-machine authentication. + + This is used when running scripts in the cluster that require authentication + with client credentials. + """ + + type: Literal["m2m"] + client_id: str + token_url: str + audience: str + # Some infrastructure deployments do not require a client secret. + client_secret: str = "" \ No newline at end of file diff --git a/config/models/url.py b/config/models/url.py new file mode 100644 index 0000000..537efd1 --- /dev/null +++ b/config/models/url.py @@ -0,0 +1,34 @@ +"""Module defining a structured URL configuration model. + +Ensures that URLs contain required components such as protocol, host, +port, and path. +""" + +from common.domain.url import URL as DomainURL +from pydantic import BaseModel + + +class URL(BaseModel): + """Generic configuration model for a URL. + + This class provides attributes to store URL components and a method + to convert them into a `DomainURL` instance. + """ + + protocol: str + host: str + port: int + path: str + + def as_domain_url(self) -> DomainURL: + """Convert the URL instance to a `DomainURL` object. + + Returns: + DomainURL: A domain-specific URL object. + """ + return DomainURL( + protocol=self.protocol, + host=self.host, + port=self.port, + base=self.path, + ) \ No newline at end of file diff --git a/datacosmos/client.py b/datacosmos/client.py index 4c55987..87fa2cf 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -5,7 +5,6 @@ """ import logging -import os from datetime import datetime, timedelta, timezone from typing import Any, Optional @@ -27,51 +26,35 @@ class DatacosmosClient: def __init__( self, config: Optional[Config] = None, - config_file: str = "config/config.yaml", ): """Initialize the DatacosmosClient. - If no configuration is provided, it will load from the specified - YAML file or fall back to environment variables. + Load configuration from the specified YAML file, environment variables, + or fallback to the default values provided in the `Config` class. """ self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.INFO) - self.config = config or self._load_config(config_file) + self.config = config or Config.from_yaml() self.token = None self.token_expiry = None self._http_client = self._authenticate_and_initialize_client() - def _load_config(self, config_file: str) -> Config: - """Load configuration from the YAML file. - - Fall back to environment variables if the file is missing. - """ - try: - if os.path.exists(config_file): - self.logger.info(f"Loading configuration from {config_file}") - return Config.from_yaml(config_file) - self.logger.info( - "Loading configuration from environment variables" - ) - return Config.from_env() - except Exception as e: - self.logger.error(f"Failed to load configuration: {e}") - raise - def _authenticate_and_initialize_client(self) -> requests.Session: """Authenticate and initialize the HTTP client with a valid token.""" try: self.logger.info("Authenticating with the token endpoint") - client = BackendApplicationClient(client_id=self.config.client_id) + client = BackendApplicationClient( + client_id=self.config.authentication.client_id + ) oauth_session = OAuth2Session(client=client) # Fetch the token using client credentials token_response = oauth_session.fetch_token( - token_url=self.config.token_url, - client_id=self.config.client_id, - client_secret=self.config.client_secret, - audience=self.config.audience, + token_url=self.config.authentication.token_url, + client_id=self.config.authentication.client_id, + client_secret=self.config.authentication.client_secret, + audience=self.config.authentication.audience, ) self.token = token_response["access_token"] @@ -82,9 +65,7 @@ def _authenticate_and_initialize_client(self) -> requests.Session: # Initialize the HTTP session with the Authorization header http_client = requests.Session() - http_client.headers.update( - {"Authorization": f"Bearer {self.token}"} - ) + http_client.headers.update({"Authorization": f"Bearer {self.token}"}) return http_client except RequestException as e: self.logger.error(f"Request failed during authentication: {e}") @@ -141,4 +122,4 @@ def put(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: def delete(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: """Send a DELETE request using the authenticated session.""" - return self.request("DELETE", url, *args, **kwargs) + return self.request("DELETE", url, *args, **kwargs) \ No newline at end of file diff --git a/tests/unit/datacosmos/client/test_client_authentication.py b/tests/unit/datacosmos/client/test_client_authentication.py index 2a1cfc5..0041016 100644 --- a/tests/unit/datacosmos/client/test_client_authentication.py +++ b/tests/unit/datacosmos/client/test_client_authentication.py @@ -1,6 +1,7 @@ from unittest.mock import patch from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient @@ -21,10 +22,10 @@ def test_client_authentication(mock_auth_client, mock_fetch_token): def mock_authenticate_and_initialize_client(self): # Call the real fetch_token (simulated by the mock) token_response = mock_fetch_token( - token_url=self.config.token_url, - client_id=self.config.client_id, - client_secret=self.config.client_secret, - audience=self.config.audience, + token_url=self.config.authentication.token_url, + client_id=self.config.authentication.client_id, + client_secret=self.config.authentication.client_secret, + audience=self.config.authentication.audience, ) self.token = token_response["access_token"] self.token_expiry = "mock-expiry" @@ -34,10 +35,13 @@ def mock_authenticate_and_initialize_client(self): # Create a mock configuration config = Config( - client_id="test-client-id", - client_secret="test-client-secret", - token_url="https://mock.token.url/oauth/token", - audience="https://mock.audience", + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) ) # Initialize the client @@ -52,4 +56,4 @@ def mock_authenticate_and_initialize_client(self): client_secret="test-client-secret", audience="https://mock.audience", ) - mock_auth_client.assert_called_once_with(client) + mock_auth_client.assert_called_once_with(client) \ No newline at end of file diff --git a/tests/unit/datacosmos/client/test_client_delete_request.py b/tests/unit/datacosmos/client/test_client_delete_request.py index 0495ed8..f4af82b 100644 --- a/tests/unit/datacosmos/client/test_client_delete_request.py +++ b/tests/unit/datacosmos/client/test_client_delete_request.py @@ -1,12 +1,11 @@ from unittest.mock import MagicMock, patch from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_delete_request(mock_auth_client): """Test that the client performs a DELETE request correctly.""" # Mock the HTTP client @@ -17,11 +16,15 @@ def test_delete_request(mock_auth_client): mock_auth_client.return_value = mock_http_client config = Config( - client_id="test-client-id", - client_secret="test-client-secret", - token_url="https://mock.token.url/oauth/token", - audience="https://mock.audience", + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) ) + client = DatacosmosClient(config=config) response = client.delete("https://mock.api/some-endpoint") @@ -30,4 +33,4 @@ def test_delete_request(mock_auth_client): mock_http_client.request.assert_called_once_with( "DELETE", "https://mock.api/some-endpoint" ) - mock_auth_client.call_count == 2 + mock_auth_client.call_count == 2 \ No newline at end of file diff --git a/tests/unit/datacosmos/client/test_client_get_request.py b/tests/unit/datacosmos/client/test_client_get_request.py index 9b05c12..7f6182b 100644 --- a/tests/unit/datacosmos/client/test_client_get_request.py +++ b/tests/unit/datacosmos/client/test_client_get_request.py @@ -1,12 +1,11 @@ from unittest.mock import MagicMock, patch from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_client_get_request(mock_auth_client): """Test that the client performs a GET request correctly.""" # Mock the HTTP client @@ -18,11 +17,15 @@ def test_client_get_request(mock_auth_client): mock_auth_client.return_value = mock_http_client config = Config( - client_id="test-client-id", - client_secret="test-client-secret", - token_url="https://mock.token.url/oauth/token", - audience="https://mock.audience", + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) ) + client = DatacosmosClient(config=config) response = client.get("https://mock.api/some-endpoint") @@ -32,4 +35,4 @@ def test_client_get_request(mock_auth_client): mock_http_client.request.assert_called_once_with( "GET", "https://mock.api/some-endpoint" ) - mock_auth_client.call_count == 2 + mock_auth_client.call_count == 2 \ No newline at end of file diff --git a/tests/unit/datacosmos/client/test_client_initialization.py b/tests/unit/datacosmos/client/test_client_initialization.py index fea667c..96ebb62 100644 --- a/tests/unit/datacosmos/client/test_client_initialization.py +++ b/tests/unit/datacosmos/client/test_client_initialization.py @@ -1,30 +1,27 @@ from unittest.mock import MagicMock, patch from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) -@patch("os.path.exists", return_value=False) -@patch("config.Config.from_env") -def test_client_initialization(mock_from_env, mock_exists, mock_auth_client): +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +def test_client_initialization(mock_auth_client): """Test that the client initializes correctly with environment variables and mocks the HTTP client.""" mock_config = Config( - client_id="test-client-id", - client_secret="test-client-secret", - token_url="https://mock.token.url/oauth/token", - audience="https://mock.audience", + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="zCeZWJamwnb8ZIQEK35rhx0hSAjsZI4D", + token_url="https://login.open-cosmos.com/oauth/token", + audience="https://test.beeapp.open-cosmos.com", + client_secret="tAeaSgLds7g535ofGq79Zm2DSbWMCOsuRyY5lbyObJe9eAeSN_fxoy-5kaXnVSYa", + ) ) - mock_from_env.return_value = mock_config mock_auth_client.return_value = MagicMock() # Mock the HTTP client client = DatacosmosClient() assert client.config == mock_config assert client._http_client is not None # Ensure the HTTP client is mocked - mock_exists.assert_called_once_with("config/config.yaml") - mock_from_env.assert_called_once() - mock_auth_client.assert_called_once() + mock_auth_client.assert_called_once() \ No newline at end of file diff --git a/tests/unit/datacosmos/client/test_client_post_request.py b/tests/unit/datacosmos/client/test_client_post_request.py index dab34db..99c9664 100644 --- a/tests/unit/datacosmos/client/test_client_post_request.py +++ b/tests/unit/datacosmos/client/test_client_post_request.py @@ -1,12 +1,11 @@ from unittest.mock import MagicMock, patch from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_post_request(mock_auth_client): """Test that the client performs a POST request correctly.""" # Mock the HTTP client @@ -18,15 +17,17 @@ def test_post_request(mock_auth_client): mock_auth_client.return_value = mock_http_client config = Config( - client_id="test-client-id", - client_secret="test-client-secret", - token_url="https://mock.token.url/oauth/token", - audience="https://mock.audience", + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) ) + client = DatacosmosClient(config=config) - response = client.post( - "https://mock.api/some-endpoint", json={"key": "value"} - ) + response = client.post("https://mock.api/some-endpoint", json={"key": "value"}) # Assertions assert response.status_code == 201 @@ -34,4 +35,4 @@ def test_post_request(mock_auth_client): mock_http_client.request.assert_called_once_with( "POST", "https://mock.api/some-endpoint", json={"key": "value"} ) - mock_auth_client.call_count == 2 + mock_auth_client.call_count == 2 \ No newline at end of file diff --git a/tests/unit/datacosmos/client/test_client_put_request.py b/tests/unit/datacosmos/client/test_client_put_request.py index 479dbaf..0e19f72 100644 --- a/tests/unit/datacosmos/client/test_client_put_request.py +++ b/tests/unit/datacosmos/client/test_client_put_request.py @@ -1,12 +1,11 @@ from unittest.mock import MagicMock, patch from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_put_request(mock_auth_client): """Test that the client performs a PUT request correctly.""" # Mock the HTTP client @@ -18,11 +17,15 @@ def test_put_request(mock_auth_client): mock_auth_client.return_value = mock_http_client config = Config( - client_id="test-client-id", - client_secret="test-client-secret", - token_url="https://mock.token.url/oauth/token", - audience="https://mock.audience", + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) ) + client = DatacosmosClient(config=config) response = client.put( "https://mock.api/some-endpoint", json={"key": "updated-value"} @@ -34,4 +37,4 @@ def test_put_request(mock_auth_client): mock_http_client.request.assert_called_once_with( "PUT", "https://mock.api/some-endpoint", json={"key": "updated-value"} ) - mock_auth_client.call_count == 2 + mock_auth_client.call_count == 2 \ No newline at end of file diff --git a/tests/unit/datacosmos/client/test_client_token_refreshing.py b/tests/unit/datacosmos/client/test_client_token_refreshing.py index a6abd05..d35db59 100644 --- a/tests/unit/datacosmos/client/test_client_token_refreshing.py +++ b/tests/unit/datacosmos/client/test_client_token_refreshing.py @@ -2,12 +2,11 @@ from unittest.mock import MagicMock, patch from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client" -) +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") def test_client_token_refreshing(mock_auth_client): """Test that the client refreshes the token when it expires.""" # Mock the HTTP client returned by _authenticate_and_initialize_client @@ -19,10 +18,13 @@ def test_client_token_refreshing(mock_auth_client): mock_auth_client.return_value = mock_http_client config = Config( - client_id="test-client-id", - client_secret="test-client-secret", - token_url="https://mock.token.url/oauth/token", - audience="https://mock.audience", + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) ) # Initialize the client (first call to _authenticate_and_initialize_client) @@ -46,4 +48,4 @@ def test_client_token_refreshing(mock_auth_client): # Verify the request was made correctly mock_http_client.request.assert_called_once_with( "GET", "https://mock.api/some-endpoint" - ) + ) \ No newline at end of file From 146448427d9d6d1d0f71ba91daa6591a3712cca5 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 31 Jan 2025 11:52:03 +0000 Subject: [PATCH 056/135] Add new line in end of files --- config/config.py | 2 +- config/models/m2m_authentication_config.py | 2 +- config/models/url.py | 2 +- datacosmos/client.py | 2 +- tests/unit/datacosmos/client/test_client_authentication.py | 2 +- tests/unit/datacosmos/client/test_client_delete_request.py | 2 +- tests/unit/datacosmos/client/test_client_get_request.py | 2 +- tests/unit/datacosmos/client/test_client_initialization.py | 2 +- tests/unit/datacosmos/client/test_client_post_request.py | 2 +- tests/unit/datacosmos/client/test_client_put_request.py | 2 +- tests/unit/datacosmos/client/test_client_token_refreshing.py | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/config/config.py b/config/config.py index 4022e13..8cd9226 100644 --- a/config/config.py +++ b/config/config.py @@ -84,4 +84,4 @@ def merge_with_env(cls, values): env_value = os.getenv(f"OC_{field.upper()}") if env_value: values[field] = env_value - return values \ No newline at end of file + return values diff --git a/config/models/m2m_authentication_config.py b/config/models/m2m_authentication_config.py index 028387a..0afca20 100644 --- a/config/models/m2m_authentication_config.py +++ b/config/models/m2m_authentication_config.py @@ -21,4 +21,4 @@ class M2MAuthenticationConfig(BaseModel): token_url: str audience: str # Some infrastructure deployments do not require a client secret. - client_secret: str = "" \ No newline at end of file + client_secret: str = "" diff --git a/config/models/url.py b/config/models/url.py index 537efd1..7df9c83 100644 --- a/config/models/url.py +++ b/config/models/url.py @@ -31,4 +31,4 @@ def as_domain_url(self) -> DomainURL: host=self.host, port=self.port, base=self.path, - ) \ No newline at end of file + ) diff --git a/datacosmos/client.py b/datacosmos/client.py index 87fa2cf..f2e570e 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -122,4 +122,4 @@ def put(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: def delete(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: """Send a DELETE request using the authenticated session.""" - return self.request("DELETE", url, *args, **kwargs) \ No newline at end of file + return self.request("DELETE", url, *args, **kwargs) diff --git a/tests/unit/datacosmos/client/test_client_authentication.py b/tests/unit/datacosmos/client/test_client_authentication.py index 0041016..2327c9e 100644 --- a/tests/unit/datacosmos/client/test_client_authentication.py +++ b/tests/unit/datacosmos/client/test_client_authentication.py @@ -56,4 +56,4 @@ def mock_authenticate_and_initialize_client(self): client_secret="test-client-secret", audience="https://mock.audience", ) - mock_auth_client.assert_called_once_with(client) \ No newline at end of file + mock_auth_client.assert_called_once_with(client) diff --git a/tests/unit/datacosmos/client/test_client_delete_request.py b/tests/unit/datacosmos/client/test_client_delete_request.py index f4af82b..6b61f0b 100644 --- a/tests/unit/datacosmos/client/test_client_delete_request.py +++ b/tests/unit/datacosmos/client/test_client_delete_request.py @@ -33,4 +33,4 @@ def test_delete_request(mock_auth_client): mock_http_client.request.assert_called_once_with( "DELETE", "https://mock.api/some-endpoint" ) - mock_auth_client.call_count == 2 \ No newline at end of file + mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_get_request.py b/tests/unit/datacosmos/client/test_client_get_request.py index 7f6182b..4963756 100644 --- a/tests/unit/datacosmos/client/test_client_get_request.py +++ b/tests/unit/datacosmos/client/test_client_get_request.py @@ -35,4 +35,4 @@ def test_client_get_request(mock_auth_client): mock_http_client.request.assert_called_once_with( "GET", "https://mock.api/some-endpoint" ) - mock_auth_client.call_count == 2 \ No newline at end of file + mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_initialization.py b/tests/unit/datacosmos/client/test_client_initialization.py index 96ebb62..9422fca 100644 --- a/tests/unit/datacosmos/client/test_client_initialization.py +++ b/tests/unit/datacosmos/client/test_client_initialization.py @@ -24,4 +24,4 @@ def test_client_initialization(mock_auth_client): assert client.config == mock_config assert client._http_client is not None # Ensure the HTTP client is mocked - mock_auth_client.assert_called_once() \ No newline at end of file + mock_auth_client.assert_called_once() diff --git a/tests/unit/datacosmos/client/test_client_post_request.py b/tests/unit/datacosmos/client/test_client_post_request.py index 99c9664..5023602 100644 --- a/tests/unit/datacosmos/client/test_client_post_request.py +++ b/tests/unit/datacosmos/client/test_client_post_request.py @@ -35,4 +35,4 @@ def test_post_request(mock_auth_client): mock_http_client.request.assert_called_once_with( "POST", "https://mock.api/some-endpoint", json={"key": "value"} ) - mock_auth_client.call_count == 2 \ No newline at end of file + mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_put_request.py b/tests/unit/datacosmos/client/test_client_put_request.py index 0e19f72..c894fc0 100644 --- a/tests/unit/datacosmos/client/test_client_put_request.py +++ b/tests/unit/datacosmos/client/test_client_put_request.py @@ -37,4 +37,4 @@ def test_put_request(mock_auth_client): mock_http_client.request.assert_called_once_with( "PUT", "https://mock.api/some-endpoint", json={"key": "updated-value"} ) - mock_auth_client.call_count == 2 \ No newline at end of file + mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/client/test_client_token_refreshing.py b/tests/unit/datacosmos/client/test_client_token_refreshing.py index d35db59..ad44784 100644 --- a/tests/unit/datacosmos/client/test_client_token_refreshing.py +++ b/tests/unit/datacosmos/client/test_client_token_refreshing.py @@ -48,4 +48,4 @@ def test_client_token_refreshing(mock_auth_client): # Verify the request was made correctly mock_http_client.request.assert_called_once_with( "GET", "https://mock.api/some-endpoint" - ) \ No newline at end of file + ) From 0170cd38c4976e0a35d977d780422b574a05d285 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 31 Jan 2025 11:55:18 +0000 Subject: [PATCH 057/135] Add pydantic and pydantic-settings to dependencies --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 595de06..8c40caa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,8 @@ test = [ "requests==2.26.0", "oauthlib==3.1.1", "requests-oauthlib==1.3.0", + "pydantic==2.10.6", + "pydantic-settings==2.7.1" ] [tool.setuptools.packages] From 2c1256e494b2dc3b4d6e1e7c422a7c902114afde Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 31 Jan 2025 11:58:25 +0000 Subject: [PATCH 058/135] Fix pipeline --- .github/workflows/main.yaml | 82 +++++++++++++++++++++++-------------- pyproject.toml | 33 +++++++-------- 2 files changed, 69 insertions(+), 46 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 888adcf..4374c3f 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -15,35 +15,38 @@ jobs: steps: - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: "3.10" + - name: set up poetry + run: curl -sSL https://install.python-poetry.org | python3 - + - name: configure gitlab auth + run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - name: install dependencies - run: pip install .[test] - - name: auto-fix with black and isort + run: poetry install --no-interaction + - name: run linters manually with poetry run: | - black . --check --quiet || black . - isort . --check-only --quiet || isort . - - name: lint - uses: wearerequired/lint-action@v2 - with: - continue_on_error: false - black: true - flake8: true - + poetry run black . --check + poetry run isort . --check-only + poetry run flake8 . + bandit: name: bandit runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: "3.10" + - name: set up poetry + run: curl -sSL https://install.python-poetry.org | python3 - + - name: configure gitlab auth + run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - name: install dependencies - run: pip install .[test] + run: poetry install --no-interaction - name: bandit - run: bandit -r -c pyproject.toml . --skip B105,B106,B101 + run: poetry run bandit -r -c pyproject.toml . --skip B105,B106,B101 cognitive: name: cognitive @@ -51,13 +54,17 @@ jobs: steps: - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: "3.10" + - name: set up poetry + run: curl -sSL https://install.python-poetry.org | python3 - + - name: configure gitlab auth + run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - name: install dependencies - run: pip install .[test] + run: poetry install --no-interaction - name: cognitive - run: flake8 . --max-cognitive-complexity=5 --ignore=E501 + run: poetry run flake8 . --max-cognitive-complexity=5 --ignore=E501 isort: name: isort @@ -65,11 +72,15 @@ jobs: steps: - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: "3.10" + - name: set up poetry + run: curl -sSL https://install.python-poetry.org | python3 - + - name: configure gitlab auth + run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - name: install dependencies - run: pip install .[test] + run: poetry install --no-interaction - name: isort uses: isort/isort-action@v1.1.0 @@ -79,31 +90,36 @@ jobs: steps: - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: "3.10" + - name: set up poetry + run: curl -sSL https://install.python-poetry.org | python3 - + - name: configure gitlab auth + run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - name: install dependencies - run: pip install .[test] + run: poetry install --no-interaction - name: pydocstyle - run: pydocstyle . + run: poetry run pydocstyle . test: name: test runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] needs: [bandit, cognitive, isort, lint, pydocstyle] steps: - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} + python-version: "3.10" + - name: set up poetry + run: curl -sSL https://install.python-poetry.org | python3 - + - name: configure gitlab auth + run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - name: install dependencies - run: pip install .[test] + run: poetry install --no-interaction - name: test - run: python -m pytest + run: poetry run pytest release: name: tag, changelog, release, publish @@ -112,6 +128,12 @@ jobs: if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 + - name: set up poetry + run: curl -sSL https://install.python-poetry.org | python3 - + - name: configure gitlab auth + run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} + - name: install dependencies + run: poetry install --no-interaction - name: version uses: paulhatch/semantic-version@v5.0.0 id: version @@ -145,4 +167,4 @@ jobs: ## Included Pull Requests - ${{ steps.changelog.outputs.changes }} + ${{ steps.changelog.outputs.changes }} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8c40caa..f07db41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,40 +3,41 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "datacosmos-sdk" +name = "datacosmos" version = "0.0.1" authors = [ { name="Open Cosmos", email="support@open-cosmos.com" }, ] description = "A library for interacting with DataCosmos from Python code" -requires-python = ">=3.8" +requires-python = ">=3.10" classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] -dependencies = [] - -[project.optional-dependencies] -test = [ - "black==22.12.0", - "flake8==6.0.0", +dependencies = [ + "python-common==0.13.1", + "black==22.3.0", + "flake8==4.0.1", "pytest==7.2.0", "bandit[toml]==1.7.4", "isort==5.11.4", "pydocstyle==6.1.1", "flake8-cognitive-complexity==0.1.0", - "requests==2.26.0", - "oauthlib==3.1.1", - "requests-oauthlib==1.3.0", + "requests==2.31.0", + "oauthlib==3.2.0", + "requests-oauthlib==1.3.1", "pydantic==2.10.6", - "pydantic-settings==2.7.1" + "pydantic-settings==2.7.1", + "pystac==1.12.1" ] -[tool.setuptools.packages] -find = {} - [tool.bandit] - [tool.pydocstyle] convention = "google" + +# Add GitLab private registry as a source +# useless comment +[[tool.poetry.source]] +name = "gitlab" +url = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" \ No newline at end of file From fcb80da3ddf28d63c338355c30ae68860c9faa2a Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Tue, 4 Feb 2025 15:47:54 +0000 Subject: [PATCH 059/135] Remove hardcoded credentials from config.py --- config/config.py | 62 ++++---- datacosmos/client.py | 10 +- .../client/test_client_authentication.py | 145 +++++++++++------- .../client/test_client_initialization.py | 27 ---- 4 files changed, 136 insertions(+), 108 deletions(-) delete mode 100644 tests/unit/datacosmos/client/test_client_initialization.py diff --git a/config/config.py b/config/config.py index 8cd9226..e4c6164 100644 --- a/config/config.py +++ b/config/config.py @@ -6,10 +6,10 @@ """ import os -from typing import Literal +from typing import Literal, Optional import yaml -from pydantic import model_validator +from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict from config.models.m2m_authentication_config import M2MAuthenticationConfig @@ -29,14 +29,8 @@ class Config(BaseSettings): log_format: Literal["json", "text"] = "text" log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO" - # Authentication configuration - authentication: M2MAuthenticationConfig = M2MAuthenticationConfig( - type="m2m", - client_id="zCeZWJamwnb8ZIQEK35rhx0hSAjsZI4D", - token_url="https://login.open-cosmos.com/oauth/token", - audience="https://test.beeapp.open-cosmos.com", - client_secret="tAeaSgLds7g535ofGq79Zm2DSbWMCOsuRyY5lbyObJe9eAeSN_fxoy-5kaXnVSYa", - ) + # Authentication configuration (must be explicitly provided) + authentication: Optional[M2MAuthenticationConfig] = None # STAC API configuration stac: URL = URL( @@ -64,24 +58,36 @@ def from_yaml(cls, file_path: str = "config/config.yaml") -> "Config": config_data = { k: v for k, v in yaml_data.items() if v not in [None, ""] } + return cls(**config_data) - @model_validator(mode="before") @classmethod - def merge_with_env(cls, values): - """Override settings with environment variables if set. - - This method checks if any environment variables corresponding to the - config fields are set and updates their values accordingly. - - Args: - values (dict): The configuration values before validation. - - Returns: - dict: The updated configuration values with environment variable overrides. - """ - for field in cls.model_fields: - env_value = os.getenv(f"OC_{field.upper()}") - if env_value: - values[field] = env_value - return values + def from_env(cls) -> "Config": + """Load configuration from environment variables.""" + env_auth = { + "type": "m2m", + "client_id": os.getenv("OC_AUTH_CLIENT_ID"), + "token_url": os.getenv("OC_AUTH_TOKEN_URL"), + "audience": os.getenv("OC_AUTH_AUDIENCE"), + "client_secret": os.getenv("OC_AUTH_CLIENT_SECRET"), + } + + if all(env_auth.values()): # Ensure all values exist + env_auth_config = M2MAuthenticationConfig(**env_auth) + else: + env_auth_config = None # If missing, let validation handle it + + return cls(authentication=env_auth_config) + + @field_validator("authentication", mode="before") + @classmethod + def validate_authentication(cls, v): + """Ensure authentication is provided through one of the allowed methods.""" + if v is None: + raise ValueError( + "M2M authentication is required. Please provide it via:" + "\n1. Explicit instantiation (Config(authentication=...))" + "\n2. A YAML config file (config.yaml)" + "\n3. Environment variables (OC_AUTH_CLIENT_ID, OC_AUTH_TOKEN_URL, etc.)" + ) + return v diff --git a/datacosmos/client.py b/datacosmos/client.py index f2e570e..984da88 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -35,7 +35,15 @@ def __init__( self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.INFO) - self.config = config or Config.from_yaml() + if config: + self.config = config + else: + try: + self.config = Config.from_yaml() + except ValueError: + self.logger.info("No valid YAML config found, falling back to env vars.") + self.config = Config.from_env() + self.token = None self.token_expiry = None self._http_client = self._authenticate_and_initialize_client() diff --git a/tests/unit/datacosmos/client/test_client_authentication.py b/tests/unit/datacosmos/client/test_client_authentication.py index 2327c9e..c352ffe 100644 --- a/tests/unit/datacosmos/client/test_client_authentication.py +++ b/tests/unit/datacosmos/client/test_client_authentication.py @@ -1,59 +1,100 @@ from unittest.mock import patch - +import os +import pytest +import yaml from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient -@patch("datacosmos.client.OAuth2Session.fetch_token") -@patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client", - autospec=True, -) -def test_client_authentication(mock_auth_client, mock_fetch_token): - """Test that the client correctly fetches a token during authentication.""" - # Mock the token response from OAuth2Session - mock_fetch_token.return_value = { - "access_token": "mock-access-token", - "expires_in": 3600, - } - - # Simulate _authenticate_and_initialize_client calling fetch_token - def mock_authenticate_and_initialize_client(self): - # Call the real fetch_token (simulated by the mock) - token_response = mock_fetch_token( - token_url=self.config.authentication.token_url, - client_id=self.config.authentication.client_id, - client_secret=self.config.authentication.client_secret, - audience=self.config.authentication.audience, - ) - self.token = token_response["access_token"] - self.token_expiry = "mock-expiry" - - # Attach the side effect to the mock - mock_auth_client.side_effect = mock_authenticate_and_initialize_client - - # Create a mock configuration - config = Config( - authentication=M2MAuthenticationConfig( - type="m2m", - client_id="test-client-id", - client_secret="test-client-secret", - token_url="https://mock.token.url/oauth/token", - audience="https://mock.audience", +@pytest.mark.usefixtures("mock_fetch_token", "mock_auth_client") +class TestClientAuthentication: + """Test suite for DatacosmosClient authentication.""" + + @pytest.fixture + def mock_fetch_token(self): + """Fixture to mock OAuth2 token fetch.""" + with patch("datacosmos.client.OAuth2Session.fetch_token") as mock: + mock.return_value = { + "access_token": "mock-access-token", + "expires_in": 3600, + } + yield mock + + @pytest.fixture + def mock_auth_client(self, mock_fetch_token): + """Fixture to mock the authentication client initialization.""" + with patch( + "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client", + autospec=True, + ) as mock: + + def mock_authenticate(self): + """Simulate authentication by setting token values.""" + token_response = mock_fetch_token.return_value + self.token = token_response["access_token"] + self.token_expiry = "mock-expiry" + + mock.side_effect = mock_authenticate + yield mock + + def test_authentication_with_explicit_config(self): + """Test authentication when explicitly providing Config.""" + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) ) - ) - - # Initialize the client - client = DatacosmosClient(config=config) - - # Assertions - assert client.token == "mock-access-token" - assert client.token_expiry == "mock-expiry" - mock_fetch_token.assert_called_once_with( - token_url="https://mock.token.url/oauth/token", - client_id="test-client-id", - client_secret="test-client-secret", - audience="https://mock.audience", - ) - mock_auth_client.assert_called_once_with(client) + + client = DatacosmosClient(config=config) + + assert client.token == "mock-access-token" + assert client.token_expiry == "mock-expiry" + + @patch("config.config.Config.from_yaml") + def test_authentication_from_yaml(self, mock_from_yaml, tmp_path): + """Test authentication when loading Config from YAML file.""" + config_path = tmp_path / "config.yaml" + yaml_data = { + "authentication": { + "type": "m2m", + "client_id": "test-client-id", + "client_secret": "test-client-secret", + "token_url": "https://mock.token.url/oauth/token", + "audience": "https://mock.audience", + } + } + + with open(config_path, "w") as f: + yaml.dump(yaml_data, f) + + mock_from_yaml.return_value = Config.from_yaml(str(config_path)) + + # Clear any previous calls before instantiating the client + mock_from_yaml.reset_mock() + + client = DatacosmosClient() + + assert client.token == "mock-access-token" + assert client.token_expiry == "mock-expiry" + + # Ensure it was called exactly once after reset + mock_from_yaml.assert_called_once() + + + @patch.dict(os.environ, { + "OC_AUTH_CLIENT_ID": "test-client-id", + "OC_AUTH_TOKEN_URL": "https://mock.token.url/oauth/token", + "OC_AUTH_AUDIENCE": "https://mock.audience", + "OC_AUTH_CLIENT_SECRET": "test-client-secret" + }) + def test_authentication_from_env(self): + """Test authentication when loading Config from environment variables.""" + client = DatacosmosClient() + + assert client.token == "mock-access-token" + assert client.token_expiry == "mock-expiry" diff --git a/tests/unit/datacosmos/client/test_client_initialization.py b/tests/unit/datacosmos/client/test_client_initialization.py deleted file mode 100644 index 9422fca..0000000 --- a/tests/unit/datacosmos/client/test_client_initialization.py +++ /dev/null @@ -1,27 +0,0 @@ -from unittest.mock import MagicMock, patch - -from config.config import Config -from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient - - -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") -def test_client_initialization(mock_auth_client): - """Test that the client initializes correctly with environment variables - and mocks the HTTP client.""" - mock_config = Config( - authentication=M2MAuthenticationConfig( - type="m2m", - client_id="zCeZWJamwnb8ZIQEK35rhx0hSAjsZI4D", - token_url="https://login.open-cosmos.com/oauth/token", - audience="https://test.beeapp.open-cosmos.com", - client_secret="tAeaSgLds7g535ofGq79Zm2DSbWMCOsuRyY5lbyObJe9eAeSN_fxoy-5kaXnVSYa", - ) - ) - mock_auth_client.return_value = MagicMock() # Mock the HTTP client - - client = DatacosmosClient() - - assert client.config == mock_config - assert client._http_client is not None # Ensure the HTTP client is mocked - mock_auth_client.assert_called_once() From 16558a25abfce99c0cdfbb0de8ae12d121be4c33 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Tue, 4 Feb 2025 18:01:52 +0000 Subject: [PATCH 060/135] Allow sdk user to say what log levels he/she wants to see when using the sdk. It defaults to all errors more critical than info --- datacosmos/client.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index 984da88..3aed0d9 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -1,12 +1,6 @@ -"""DatacosmosClient handles authenticated interactions with the Datacosmos API. - -Automatically manages token refreshing and provides HTTP convenience -methods. -""" - import logging from datetime import datetime, timedelta, timezone -from typing import Any, Optional +from typing import Any, Optional, Literal import requests from oauthlib.oauth2 import BackendApplicationClient @@ -26,15 +20,19 @@ class DatacosmosClient: def __init__( self, config: Optional[Config] = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", ): """Initialize the DatacosmosClient. - Load configuration from the specified YAML file, environment variables, - or fallback to the default values provided in the `Config` class. + Args: + config (Optional[Config]): Configuration object. + log_level (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]): The logging level. """ + # Initialize logger self.logger = logging.getLogger(__name__) - self.logger.setLevel(logging.INFO) + self.set_log_level(log_level) + # Load configuration from input, YAML, or environment variables if config: self.config = config else: @@ -48,10 +46,19 @@ def __init__( self.token_expiry = None self._http_client = self._authenticate_and_initialize_client() + def set_log_level(self, level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]): + """Set the logging level based on user input. + + Args: + level (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]): The logging level. + """ + log_level = getattr(logging, level.upper(), logging.INFO) + self.logger.setLevel(log_level) + def _authenticate_and_initialize_client(self) -> requests.Session: """Authenticate and initialize the HTTP client with a valid token.""" try: - self.logger.info("Authenticating with the token endpoint") + self.logger.debug("Authenticating with the token endpoint") client = BackendApplicationClient( client_id=self.config.authentication.client_id ) @@ -69,7 +76,7 @@ def _authenticate_and_initialize_client(self) -> requests.Session: self.token_expiry = datetime.now(timezone.utc) + timedelta( seconds=token_response.get("expires_in", 3600) ) - self.logger.info("Authentication successful, token obtained") + self.logger.debug("Authentication successful, token obtained") # Initialize the HTTP session with the Authorization header http_client = requests.Session() @@ -85,7 +92,7 @@ def _authenticate_and_initialize_client(self) -> requests.Session: def _refresh_token_if_needed(self): """Refresh the token if it has expired.""" if not self.token or self.token_expiry <= datetime.now(timezone.utc): - self.logger.info("Token expired or missing, refreshing token") + self.logger.debug("Token expired or missing, refreshing token") self._http_client = self._authenticate_and_initialize_client() def get_http_client(self) -> requests.Session: @@ -102,10 +109,10 @@ def request( """ self._refresh_token_if_needed() try: - self.logger.info(f"Making {method.upper()} request to {url}") + self.logger.debug(f"Making {method.upper()} request to {url}") response = self._http_client.request(method, url, *args, **kwargs) response.raise_for_status() - self.logger.info( + self.logger.debug( f"Request to {url} succeeded with status {response.status_code}" ) return response From 6b355f5f950b43fb0bf3b51fe53fd5c969cfc4c9 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Tue, 4 Feb 2025 18:03:25 +0000 Subject: [PATCH 061/135] remove useless comment from m2m_authentication_config model --- config/models/m2m_authentication_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/models/m2m_authentication_config.py b/config/models/m2m_authentication_config.py index 0afca20..45d92ca 100644 --- a/config/models/m2m_authentication_config.py +++ b/config/models/m2m_authentication_config.py @@ -20,5 +20,4 @@ class M2MAuthenticationConfig(BaseModel): client_id: str token_url: str audience: str - # Some infrastructure deployments do not require a client secret. - client_secret: str = "" + client_secret: str From 7a3c8b5a9947c6f4234538615a7224b958010866 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Tue, 4 Feb 2025 18:06:29 +0000 Subject: [PATCH 062/135] Fix CI linting problems --- datacosmos/client.py | 8 +++++++- .../unit/datacosmos/client/test_client_authentication.py | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index 3aed0d9..c30c4ae 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -1,6 +1,12 @@ +"""DatacosmosClient handles authenticated interactions with the Datacosmos API. + +Automatically manages token refreshing and provides HTTP convenience +methods. +""" + import logging from datetime import datetime, timedelta, timezone -from typing import Any, Optional, Literal +from typing import Any, Literal, Optional import requests from oauthlib.oauth2 import BackendApplicationClient diff --git a/tests/unit/datacosmos/client/test_client_authentication.py b/tests/unit/datacosmos/client/test_client_authentication.py index c352ffe..dc1a5d3 100644 --- a/tests/unit/datacosmos/client/test_client_authentication.py +++ b/tests/unit/datacosmos/client/test_client_authentication.py @@ -1,7 +1,9 @@ -from unittest.mock import patch import os +from unittest.mock import patch + import pytest import yaml + from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient @@ -85,7 +87,6 @@ def test_authentication_from_yaml(self, mock_from_yaml, tmp_path): # Ensure it was called exactly once after reset mock_from_yaml.assert_called_once() - @patch.dict(os.environ, { "OC_AUTH_CLIENT_ID": "test-client-id", "OC_AUTH_TOKEN_URL": "https://mock.token.url/oauth/token", From 7aafba39a37f5a20ab518b64af8a0c6a2c40e316 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Tue, 4 Feb 2025 18:08:11 +0000 Subject: [PATCH 063/135] Apply black --- datacosmos/client.py | 8 ++++++-- .../client/test_client_authentication.py | 15 +++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index c30c4ae..a6b6177 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -45,14 +45,18 @@ def __init__( try: self.config = Config.from_yaml() except ValueError: - self.logger.info("No valid YAML config found, falling back to env vars.") + self.logger.info( + "No valid YAML config found, falling back to env vars." + ) self.config = Config.from_env() self.token = None self.token_expiry = None self._http_client = self._authenticate_and_initialize_client() - def set_log_level(self, level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]): + def set_log_level( + self, level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + ): """Set the logging level based on user input. Args: diff --git a/tests/unit/datacosmos/client/test_client_authentication.py b/tests/unit/datacosmos/client/test_client_authentication.py index dc1a5d3..1e536f1 100644 --- a/tests/unit/datacosmos/client/test_client_authentication.py +++ b/tests/unit/datacosmos/client/test_client_authentication.py @@ -87,12 +87,15 @@ def test_authentication_from_yaml(self, mock_from_yaml, tmp_path): # Ensure it was called exactly once after reset mock_from_yaml.assert_called_once() - @patch.dict(os.environ, { - "OC_AUTH_CLIENT_ID": "test-client-id", - "OC_AUTH_TOKEN_URL": "https://mock.token.url/oauth/token", - "OC_AUTH_AUDIENCE": "https://mock.audience", - "OC_AUTH_CLIENT_SECRET": "test-client-secret" - }) + @patch.dict( + os.environ, + { + "OC_AUTH_CLIENT_ID": "test-client-id", + "OC_AUTH_TOKEN_URL": "https://mock.token.url/oauth/token", + "OC_AUTH_AUDIENCE": "https://mock.audience", + "OC_AUTH_CLIENT_SECRET": "test-client-secret", + }, + ) def test_authentication_from_env(self): """Test authentication when loading Config from environment variables.""" client = DatacosmosClient() From 28082005c45aaa0618a1d7f63a8f8200117b1b05 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 11:14:02 +0000 Subject: [PATCH 064/135] Address comments in MR --- config/config.py | 83 ++++++++++++----- datacosmos/client.py | 88 ++++++------------- datacosmos/exceptions/__init__.py | 1 + datacosmos/exceptions/datacosmos_exception.py | 27 ++++++ 4 files changed, 115 insertions(+), 84 deletions(-) create mode 100644 datacosmos/exceptions/__init__.py create mode 100644 datacosmos/exceptions/datacosmos_exception.py diff --git a/config/config.py b/config/config.py index e4c6164..3ab4b86 100644 --- a/config/config.py +++ b/config/config.py @@ -24,21 +24,12 @@ class Config(BaseSettings): nested_model_default_partial_update=True, ) - # General configurations environment: Literal["local", "test", "prod"] = "test" log_format: Literal["json", "text"] = "text" log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO" - # Authentication configuration (must be explicitly provided) authentication: Optional[M2MAuthenticationConfig] = None - - # STAC API configuration - stac: URL = URL( - protocol="https", - host="test.app.open-cosmos.com", - port=443, - path="/api/data/v0/stac", - ) + stac: Optional[URL] = None @classmethod def from_yaml(cls, file_path: str = "config/config.yaml") -> "Config": @@ -50,21 +41,27 @@ def from_yaml(cls, file_path: str = "config/config.yaml") -> "Config": Returns: Config: An instance of the Config class with loaded settings. """ - config_data = {} + config_data: dict = {} if os.path.exists(file_path): with open(file_path, "r") as f: yaml_data = yaml.safe_load(f) or {} # Remove empty values from YAML to avoid overwriting with `None` config_data = { - k: v for k, v in yaml_data.items() if v not in [None, ""] + key: value + for key, value in yaml_data.items() + if value not in [None, ""] } return cls(**config_data) @classmethod def from_env(cls) -> "Config": - """Load configuration from environment variables.""" - env_auth = { + """Load configuration from environment variables. + + Returns: + Config: An instance of the Config class with settings loaded from environment variables. + """ + auth_env_vars: dict[str, Optional[str]] = { "type": "m2m", "client_id": os.getenv("OC_AUTH_CLIENT_ID"), "token_url": os.getenv("OC_AUTH_TOKEN_URL"), @@ -72,22 +69,62 @@ def from_env(cls) -> "Config": "client_secret": os.getenv("OC_AUTH_CLIENT_SECRET"), } - if all(env_auth.values()): # Ensure all values exist - env_auth_config = M2MAuthenticationConfig(**env_auth) - else: - env_auth_config = None # If missing, let validation handle it + authentication_config: Optional[M2MAuthenticationConfig] = ( + M2MAuthenticationConfig(**auth_env_vars) + if all(auth_env_vars.values()) + else None + ) - return cls(authentication=env_auth_config) + stac_config: URL = URL( + protocol=os.getenv("OC_STAC_PROTOCOL", "https"), + host=os.getenv("OC_STAC_HOST", "test.app.open-cosmos.com"), + port=int(os.getenv("OC_STAC_PORT", "443")), + path=os.getenv("OC_STAC_PATH", "/api/data/v0/stac"), + ) + + return cls(authentication=authentication_config, stac=stac_config) @field_validator("authentication", mode="before") @classmethod - def validate_authentication(cls, v): - """Ensure authentication is provided through one of the allowed methods.""" - if v is None: + def validate_authentication( + cls, auth_config: Optional[M2MAuthenticationConfig] + ) -> M2MAuthenticationConfig: + """Ensure authentication is provided through one of the allowed methods. + + Args: + auth_config (Optional[M2MAuthenticationConfig]): The authentication config to validate. + + Returns: + M2MAuthenticationConfig: The validated authentication configuration. + + Raises: + ValueError: If authentication is missing. + """ + if auth_config is None: raise ValueError( "M2M authentication is required. Please provide it via:" "\n1. Explicit instantiation (Config(authentication=...))" "\n2. A YAML config file (config.yaml)" "\n3. Environment variables (OC_AUTH_CLIENT_ID, OC_AUTH_TOKEN_URL, etc.)" ) - return v + return auth_config + + @field_validator("stac", mode="before") + @classmethod + def validate_stac(cls, stac_config: Optional[URL]) -> URL: + """Ensure STAC configuration has a default if not explicitly set. + + Args: + stac_config (Optional[URL]): The STAC config to validate. + + Returns: + URL: The validated STAC configuration. + """ + if stac_config is None: + return URL( + protocol="https", + host="test.app.open-cosmos.com", + port=443, + path="/api/data/v0/stac", + ) + return stac_config diff --git a/datacosmos/client.py b/datacosmos/client.py index a6b6177..d631b14 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -4,77 +4,47 @@ methods. """ -import logging from datetime import datetime, timedelta, timezone -from typing import Any, Literal, Optional +from typing import Any, Optional import requests from oauthlib.oauth2 import BackendApplicationClient -from requests.exceptions import RequestException +from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout from requests_oauthlib import OAuth2Session from config.config import Config +from datacosmos.exceptions.datacosmos_exception import DatacosmosException class DatacosmosClient: - """DatacosmosClient handles authenticated interactions with the Datacosmos API. + """Client to interact with the Datacosmos API with authentication and request handling.""" - Automatically manages token refreshing and provides HTTP convenience - methods. - """ - - def __init__( - self, - config: Optional[Config] = None, - log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", - ): + def __init__(self, config: Optional[Config] = None): """Initialize the DatacosmosClient. Args: config (Optional[Config]): Configuration object. - log_level (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]): The logging level. """ - # Initialize logger - self.logger = logging.getLogger(__name__) - self.set_log_level(log_level) - - # Load configuration from input, YAML, or environment variables if config: self.config = config else: try: self.config = Config.from_yaml() except ValueError: - self.logger.info( - "No valid YAML config found, falling back to env vars." - ) self.config = Config.from_env() self.token = None self.token_expiry = None self._http_client = self._authenticate_and_initialize_client() - def set_log_level( - self, level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] - ): - """Set the logging level based on user input. - - Args: - level (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]): The logging level. - """ - log_level = getattr(logging, level.upper(), logging.INFO) - self.logger.setLevel(log_level) - def _authenticate_and_initialize_client(self) -> requests.Session: """Authenticate and initialize the HTTP client with a valid token.""" try: - self.logger.debug("Authenticating with the token endpoint") client = BackendApplicationClient( client_id=self.config.authentication.client_id ) oauth_session = OAuth2Session(client=client) - # Fetch the token using client credentials token_response = oauth_session.fetch_token( token_url=self.config.authentication.token_url, client_id=self.config.authentication.client_id, @@ -86,52 +56,48 @@ def _authenticate_and_initialize_client(self) -> requests.Session: self.token_expiry = datetime.now(timezone.utc) + timedelta( seconds=token_response.get("expires_in", 3600) ) - self.logger.debug("Authentication successful, token obtained") - # Initialize the HTTP session with the Authorization header http_client = requests.Session() http_client.headers.update({"Authorization": f"Bearer {self.token}"}) return http_client + except (HTTPError, ConnectionError, Timeout) as e: + raise DatacosmosException(f"Authentication failed: {str(e)}") from e except RequestException as e: - self.logger.error(f"Request failed during authentication: {e}") - raise - except Exception as e: - self.logger.error(f"Unexpected error during authentication: {e}") - raise + raise DatacosmosException( + f"Unexpected request failure during authentication: {str(e)}" + ) from e def _refresh_token_if_needed(self): """Refresh the token if it has expired.""" if not self.token or self.token_expiry <= datetime.now(timezone.utc): - self.logger.debug("Token expired or missing, refreshing token") self._http_client = self._authenticate_and_initialize_client() - def get_http_client(self) -> requests.Session: - """Return the authenticated HTTP client, refreshing the token if necessary.""" - self._refresh_token_if_needed() - return self._http_client - def request( self, method: str, url: str, *args: Any, **kwargs: Any ) -> requests.Response: - """Send an HTTP request using the authenticated session. - - Logs request and response details. - """ + """Send an HTTP request using the authenticated session.""" self._refresh_token_if_needed() try: - self.logger.debug(f"Making {method.upper()} request to {url}") response = self._http_client.request(method, url, *args, **kwargs) response.raise_for_status() - self.logger.debug( - f"Request to {url} succeeded with status {response.status_code}" - ) return response + except HTTPError as e: + raise DatacosmosException( + f"HTTP error during {method.upper()} request to {url}", + response=e.response, + ) from e + except ConnectionError as e: + raise DatacosmosException( + f"Connection error during {method.upper()} request to {url}: {str(e)}" + ) from e + except Timeout as e: + raise DatacosmosException( + f"Request timeout during {method.upper()} request to {url}: {str(e)}" + ) from e except RequestException as e: - self.logger.error(f"HTTP request failed: {e}") - raise - except Exception as e: - self.logger.error(f"Unexpected error during HTTP request: {e}") - raise + raise DatacosmosException( + f"Unexpected request failure during {method.upper()} request to {url}: {str(e)}" + ) from e def get(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: """Send a GET request using the authenticated session.""" diff --git a/datacosmos/exceptions/__init__.py b/datacosmos/exceptions/__init__.py new file mode 100644 index 0000000..17476aa --- /dev/null +++ b/datacosmos/exceptions/__init__.py @@ -0,0 +1 @@ +"""Exceptions for the datacosmos package.""" diff --git a/datacosmos/exceptions/datacosmos_exception.py b/datacosmos/exceptions/datacosmos_exception.py new file mode 100644 index 0000000..5639aca --- /dev/null +++ b/datacosmos/exceptions/datacosmos_exception.py @@ -0,0 +1,27 @@ +"""Base exception class for all Datacosmos SDK exceptions.""" + +from typing import Optional + +from requests import Response +from requests.exceptions import RequestException + + +class DatacosmosException(RequestException): + """Base exception class for all Datacosmos SDK exceptions.""" + + def __init__(self, message: str, response: Optional[Response] = None): + """Initialize DatacosmosException. + + Args: + message (str): The error message. + response (Optional[Response]): The HTTP response object, if available. + """ + self.response = response + self.status_code = response.status_code if response else None + self.details = response.text if response else None + full_message = ( + f"{message} (Status: {self.status_code}, Details: {self.details})" + if response + else message + ) + super().__init__(full_message) From cc803306bf7d732a5d3156469e2e90afaebf6a7c Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 11:15:51 +0000 Subject: [PATCH 065/135] Apply isort --- datacosmos/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index d631b14..93220ed 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -9,7 +9,8 @@ import requests from oauthlib.oauth2 import BackendApplicationClient -from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout +from requests.exceptions import (ConnectionError, HTTPError, RequestException, + Timeout) from requests_oauthlib import OAuth2Session from config.config import Config From 84ea9da84c9aec66fcd6ea1647a815fa8dc13ba4 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 11:31:18 +0000 Subject: [PATCH 066/135] Apply changes in the pipeline --- .github/workflows/main.yaml | 28 +++++----------------------- datacosmos/client.py | 3 +-- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 4374c3f..d2983ed 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -24,10 +24,10 @@ jobs: run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - name: install dependencies run: poetry install --no-interaction - - name: run linters manually with poetry + - name: run isort before black run: | - poetry run black . --check - poetry run isort . --check-only + poetry run isort . --check-only # Run isort first + poetry run black . --check # Then black poetry run flake8 . bandit: @@ -66,24 +66,6 @@ jobs: - name: cognitive run: poetry run flake8 . --max-cognitive-complexity=5 --ignore=E501 - isort: - name: isort - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: set up python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: set up poetry - run: curl -sSL https://install.python-poetry.org | python3 - - - name: configure gitlab auth - run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - - name: install dependencies - run: poetry install --no-interaction - - name: isort - uses: isort/isort-action@v1.1.0 - pydocstyle: name: pydocstyle runs-on: ubuntu-latest @@ -105,7 +87,7 @@ jobs: test: name: test runs-on: ubuntu-latest - needs: [bandit, cognitive, isort, lint, pydocstyle] + needs: [bandit, cognitive, lint, pydocstyle] steps: - uses: actions/checkout@v3 - name: set up python @@ -167,4 +149,4 @@ jobs: ## Included Pull Requests - ${{ steps.changelog.outputs.changes }} \ No newline at end of file + ${{ steps.changelog.outputs.changes }} diff --git a/datacosmos/client.py b/datacosmos/client.py index 93220ed..d631b14 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -9,8 +9,7 @@ import requests from oauthlib.oauth2 import BackendApplicationClient -from requests.exceptions import (ConnectionError, HTTPError, RequestException, - Timeout) +from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout from requests_oauthlib import OAuth2Session from config.config import Config From 982197db77c45923db53a5a8de2419b23ee1e8ef Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 11:39:29 +0000 Subject: [PATCH 067/135] Add changes in pyproject.toml to make isort behave similarly to black --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index f07db41..bdb8c33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,14 @@ dependencies = [ [tool.pydocstyle] convention = "google" +[tool.isort] +profile = "black" +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +line_length = 88 + # Add GitLab private registry as a source # useless comment [[tool.poetry.source]] From d26e181a8ba2c1181d5e4ddcf8f7f4a0ea3cefa9 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 12:04:07 +0000 Subject: [PATCH 068/135] Allow default values for type, token_url and audience --- config/config.py | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/config/config.py b/config/config.py index 3ab4b86..e14d731 100644 --- a/config/config.py +++ b/config/config.py @@ -6,7 +6,7 @@ """ import os -from typing import Literal, Optional +from typing import ClassVar, Literal, Optional import yaml from pydantic import field_validator @@ -31,6 +31,10 @@ class Config(BaseSettings): authentication: Optional[M2MAuthenticationConfig] = None stac: Optional[URL] = None + DEFAULT_AUTH_TYPE: ClassVar[str] = "m2m" + DEFAULT_AUTH_TOKEN_URL: ClassVar[str] = "https://login.open-cosmos.com/oauth/token" + DEFAULT_AUTH_AUDIENCE: ClassVar[str] = "https://test.beeapp.open-cosmos.com" + @classmethod def from_yaml(cls, file_path: str = "config/config.yaml") -> "Config": """Load configuration from a YAML file and override defaults. @@ -61,21 +65,15 @@ def from_env(cls) -> "Config": Returns: Config: An instance of the Config class with settings loaded from environment variables. """ - auth_env_vars: dict[str, Optional[str]] = { - "type": "m2m", - "client_id": os.getenv("OC_AUTH_CLIENT_ID"), - "token_url": os.getenv("OC_AUTH_TOKEN_URL"), - "audience": os.getenv("OC_AUTH_AUDIENCE"), - "client_secret": os.getenv("OC_AUTH_CLIENT_SECRET"), - } - - authentication_config: Optional[M2MAuthenticationConfig] = ( - M2MAuthenticationConfig(**auth_env_vars) - if all(auth_env_vars.values()) - else None + authentication_config = M2MAuthenticationConfig( + type=os.getenv("OC_AUTH_TYPE", cls.DEFAULT_AUTH_TYPE), + client_id=os.getenv("OC_AUTH_CLIENT_ID"), + client_secret=os.getenv("OC_AUTH_CLIENT_SECRET"), + token_url=os.getenv("OC_AUTH_TOKEN_URL", cls.DEFAULT_AUTH_TOKEN_URL), + audience=os.getenv("OC_AUTH_AUDIENCE", cls.DEFAULT_AUTH_AUDIENCE), ) - stac_config: URL = URL( + stac_config = URL( protocol=os.getenv("OC_STAC_PROTOCOL", "https"), host=os.getenv("OC_STAC_HOST", "test.app.open-cosmos.com"), port=int(os.getenv("OC_STAC_PORT", "443")), @@ -89,7 +87,7 @@ def from_env(cls) -> "Config": def validate_authentication( cls, auth_config: Optional[M2MAuthenticationConfig] ) -> M2MAuthenticationConfig: - """Ensure authentication is provided through one of the allowed methods. + """Ensure authentication is provided and defaults are applied where necessary. Args: auth_config (Optional[M2MAuthenticationConfig]): The authentication config to validate. @@ -98,15 +96,30 @@ def validate_authentication( M2MAuthenticationConfig: The validated authentication configuration. Raises: - ValueError: If authentication is missing. + ValueError: If client_id or client_secret is missing. """ if auth_config is None: raise ValueError( "M2M authentication is required. Please provide it via:" "\n1. Explicit instantiation (Config(authentication=...))" "\n2. A YAML config file (config.yaml)" - "\n3. Environment variables (OC_AUTH_CLIENT_ID, OC_AUTH_TOKEN_URL, etc.)" + "\n3. Environment variables (OC_AUTH_CLIENT_ID, OC_AUTH_CLIENT_SECRET, etc.)" ) + + if isinstance(auth_config, dict): + auth_config = M2MAuthenticationConfig(**auth_config) + + # Apply defaults for missing values + auth_config.type = auth_config.type or cls.DEFAULT_AUTH_TYPE + auth_config.token_url = auth_config.token_url or cls.DEFAULT_AUTH_TOKEN_URL + auth_config.audience = auth_config.audience or cls.DEFAULT_AUTH_AUDIENCE + + # Ensure critical values are provided + if not auth_config.client_id or not auth_config.client_secret: + raise ValueError( + "client_id and client_secret are required for authentication." + ) + return auth_config @field_validator("stac", mode="before") From 84e077f0ac0cad6c79847112b21602156c36a092 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 12:07:48 +0000 Subject: [PATCH 069/135] decrease cognitive load --- config/config.py | 51 +++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/config/config.py b/config/config.py index e14d731..7a5ca6c 100644 --- a/config/config.py +++ b/config/config.py @@ -85,12 +85,12 @@ def from_env(cls) -> "Config": @field_validator("authentication", mode="before") @classmethod def validate_authentication( - cls, auth_config: Optional[M2MAuthenticationConfig] + cls, auth_data: Optional[dict] ) -> M2MAuthenticationConfig: - """Ensure authentication is provided and defaults are applied where necessary. + """Ensure authentication is provided and apply defaults. Args: - auth_config (Optional[M2MAuthenticationConfig]): The authentication config to validate. + auth_data (Optional[dict]): The authentication config as a dictionary. Returns: M2MAuthenticationConfig: The validated authentication configuration. @@ -98,29 +98,36 @@ def validate_authentication( Raises: ValueError: If client_id or client_secret is missing. """ - if auth_config is None: + if not auth_data: raise ValueError( - "M2M authentication is required. Please provide it via:" - "\n1. Explicit instantiation (Config(authentication=...))" - "\n2. A YAML config file (config.yaml)" - "\n3. Environment variables (OC_AUTH_CLIENT_ID, OC_AUTH_CLIENT_SECRET, etc.)" + "M2M authentication is required. Provide it via:\n" + "1. Explicit instantiation (Config(authentication=...))\n" + "2. A YAML config file (config.yaml)\n" + "3. Environment variables (OC_AUTH_CLIENT_ID, OC_AUTH_CLIENT_SECRET, etc.)" ) - if isinstance(auth_config, dict): - auth_config = M2MAuthenticationConfig(**auth_config) - - # Apply defaults for missing values - auth_config.type = auth_config.type or cls.DEFAULT_AUTH_TYPE - auth_config.token_url = auth_config.token_url or cls.DEFAULT_AUTH_TOKEN_URL - auth_config.audience = auth_config.audience or cls.DEFAULT_AUTH_AUDIENCE - - # Ensure critical values are provided - if not auth_config.client_id or not auth_config.client_secret: - raise ValueError( - "client_id and client_secret are required for authentication." - ) + # Convert dict to M2MAuthenticationConfig + auth = ( + M2MAuthenticationConfig(**auth_data) + if isinstance(auth_data, dict) + else auth_data + ) - return auth_config + # Apply defaults where necessary + auth.type = auth.type or cls.DEFAULT_AUTH_TYPE + auth.token_url = auth.token_url or cls.DEFAULT_AUTH_TOKEN_URL + auth.audience = auth.audience or cls.DEFAULT_AUTH_AUDIENCE + + # Validate required fields + missing_fields = [ + field + for field in ("client_id", "client_secret") + if not getattr(auth, field) + ] + if missing_fields: + raise ValueError(f"Missing required fields: {', '.join(missing_fields)}") + + return auth @field_validator("stac", mode="before") @classmethod From 5e2135cfc0b77f9f556d09595b3fbea170f46d8f Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 12:11:47 +0000 Subject: [PATCH 070/135] Attempt to decrease cognitive complexity even more --- config/config.py | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/config/config.py b/config/config.py index 7a5ca6c..cb8845f 100644 --- a/config/config.py +++ b/config/config.py @@ -96,38 +96,58 @@ def validate_authentication( M2MAuthenticationConfig: The validated authentication configuration. Raises: - ValueError: If client_id or client_secret is missing. + ValueError: If authentication is missing or required fields are not set. """ if not auth_data: - raise ValueError( - "M2M authentication is required. Provide it via:\n" - "1. Explicit instantiation (Config(authentication=...))\n" - "2. A YAML config file (config.yaml)\n" - "3. Environment variables (OC_AUTH_CLIENT_ID, OC_AUTH_CLIENT_SECRET, etc.)" - ) + cls.raise_missing_auth_error() + + auth = cls.parse_auth_config(auth_data) + auth = cls.apply_auth_defaults(auth) + + cls.check_required_auth_fields(auth) + return auth + + @staticmethod + def raise_missing_auth_error(): + """Raise an error when authentication is missing.""" + raise ValueError( + "M2M authentication is required. Provide it via:\n" + "1. Explicit instantiation (Config(authentication=...))\n" + "2. A YAML config file (config.yaml)\n" + "3. Environment variables (OC_AUTH_CLIENT_ID, OC_AUTH_CLIENT_SECRET, etc.)" + ) - # Convert dict to M2MAuthenticationConfig - auth = ( + @staticmethod + def parse_auth_config(auth_data: dict) -> M2MAuthenticationConfig: + """Convert dictionary input to M2MAuthenticationConfig object.""" + return ( M2MAuthenticationConfig(**auth_data) if isinstance(auth_data, dict) else auth_data ) - # Apply defaults where necessary + @classmethod + def apply_auth_defaults( + cls, auth: M2MAuthenticationConfig + ) -> M2MAuthenticationConfig: + """Apply default authentication values if they are missing.""" auth.type = auth.type or cls.DEFAULT_AUTH_TYPE auth.token_url = auth.token_url or cls.DEFAULT_AUTH_TOKEN_URL auth.audience = auth.audience or cls.DEFAULT_AUTH_AUDIENCE + return auth - # Validate required fields + @staticmethod + def check_required_auth_fields(auth: M2MAuthenticationConfig): + """Ensure required fields (client_id, client_secret) are provided.""" missing_fields = [ field for field in ("client_id", "client_secret") if not getattr(auth, field) ] if missing_fields: - raise ValueError(f"Missing required fields: {', '.join(missing_fields)}") - - return auth + raise ValueError( + f"Missing required authentication fields: {', '.join(missing_fields)}" + ) @field_validator("stac", mode="before") @classmethod From 9e3d7d19c41ca6fe58643f500745b73445e100ff Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Thu, 6 Feb 2025 11:30:58 +0000 Subject: [PATCH 071/135] use prod values as default values; remove logging config --- config/config.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/config/config.py b/config/config.py index cb8845f..35f7c81 100644 --- a/config/config.py +++ b/config/config.py @@ -6,7 +6,7 @@ """ import os -from typing import ClassVar, Literal, Optional +from typing import ClassVar, Optional import yaml from pydantic import field_validator @@ -24,16 +24,12 @@ class Config(BaseSettings): nested_model_default_partial_update=True, ) - environment: Literal["local", "test", "prod"] = "test" - log_format: Literal["json", "text"] = "text" - log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO" - authentication: Optional[M2MAuthenticationConfig] = None stac: Optional[URL] = None DEFAULT_AUTH_TYPE: ClassVar[str] = "m2m" DEFAULT_AUTH_TOKEN_URL: ClassVar[str] = "https://login.open-cosmos.com/oauth/token" - DEFAULT_AUTH_AUDIENCE: ClassVar[str] = "https://test.beeapp.open-cosmos.com" + DEFAULT_AUTH_AUDIENCE: ClassVar[str] = "https://beeapp.open-cosmos.com" @classmethod def from_yaml(cls, file_path: str = "config/config.yaml") -> "Config": @@ -75,7 +71,7 @@ def from_env(cls) -> "Config": stac_config = URL( protocol=os.getenv("OC_STAC_PROTOCOL", "https"), - host=os.getenv("OC_STAC_HOST", "test.app.open-cosmos.com"), + host=os.getenv("OC_STAC_HOST", "app.open-cosmos.com"), port=int(os.getenv("OC_STAC_PORT", "443")), path=os.getenv("OC_STAC_PATH", "/api/data/v0/stac"), ) @@ -163,7 +159,7 @@ def validate_stac(cls, stac_config: Optional[URL]) -> URL: if stac_config is None: return URL( protocol="https", - host="test.app.open-cosmos.com", + host="app.open-cosmos.com", port=443, path="/api/data/v0/stac", ) From 05bd6b45a3f7339d61267fbcfd1bea96fee59889 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 29 Jan 2025 12:15:15 +0000 Subject: [PATCH 072/135] Add first version of stac catalog query functionalities to datacosmos sdk --- datacosmos/stac/__init__.py | 0 datacosmos/stac/models/search_parameters.py | 53 +++++++++ datacosmos/stac/stac_client.py | 120 ++++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 datacosmos/stac/__init__.py create mode 100644 datacosmos/stac/models/search_parameters.py create mode 100644 datacosmos/stac/stac_client.py diff --git a/datacosmos/stac/__init__.py b/datacosmos/stac/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/datacosmos/stac/models/search_parameters.py b/datacosmos/stac/models/search_parameters.py new file mode 100644 index 0000000..189cda9 --- /dev/null +++ b/datacosmos/stac/models/search_parameters.py @@ -0,0 +1,53 @@ +from pydantic import BaseModel, Field, model_validator +from typing import Optional, Union + + +class SearchParameters(BaseModel): + """ + Encapsulates the parameters for the STAC search API with validation. + """ + + bbox: Optional[list[float]] = Field( + None, + description="Bounding box filter [minX, minY, maxX, maxY]. Optional six values for 3D bounding box.", + example=[-180.0, -90.0, 180.0, 90.0], + ) + datetime_range: Optional[str] = Field( + None, + alias="datetime", + description=( + "Temporal filter, either a single RFC 3339 datetime or an interval. " + 'Example: "2025-01-01T00:00:00Z/.."' + ), + ) + intersects: Optional[dict] = Field( + None, description="GeoJSON geometry filter, e.g., a Polygon or Point." + ) + ids: Optional[list[str]] = Field( + None, + description="Array of item IDs to filter by.", + example=["item1", "item2"], + ) + collections: Optional[list[str]] = Field( + None, + description="Array of collection IDs to filter by.", + example=["collection1", "collection2"], + ) + limit: Optional[int] = Field( + None, + ge=1, + le=10000, + description="Maximum number of items per page. Default: 10, Max: 10000.", + example=10, + ) + query: Optional[dict[str, dict[str, Union[str, int, float]]]] = Field( + None, + description="Additional property filters, e.g., { 'cloud_coverage': { 'lt': 10 } }.", + ) + + @model_validator(mode="before") + def validate_bbox(cls, values): + bbox = values.get("bbox") + if bbox and len(bbox) not in {4, 6}: + raise ValueError("bbox must contain 4 or 6 values.") + return values diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py new file mode 100644 index 0000000..ef0d343 --- /dev/null +++ b/datacosmos/stac/stac_client.py @@ -0,0 +1,120 @@ +import pystac +from typing import Generator +from datacosmos.client import DatacosmosClient +from datacosmos.stac.models.search_parameters import SearchParameters + + +class STACClient: + """ + A client for interacting with the STAC API. + """ + + def __init__(self, client: DatacosmosClient): + """ + Initialize the STACClient with a DatacosmosClient. + + Args: + client (DatacosmosClient): The authenticated Datacosmos client instance. + """ + self.client = client + self.base_url = "https://test.app.open-cosmos.com/api/data/v0/stac" + + def search_items( + self, parameters: SearchParameters + ) -> Generator[pystac.Item, None, None]: + """ + Query the STAC catalog using the POST endpoint with flexible filters and pagination. + + Args: + parameters (SearchParameters): The search parameters. + + Yields: + pystac.Item: Parsed STAC item. + """ + url = f"{self.base_url}/search" + body = parameters.model_dump(by_alias=True, exclude_none=True) + return self._paginate_items(url, body) + + def fetch_item(self, item_id: str, collection_id: str) -> pystac.Item: + """ + Fetch a single STAC item by ID. + + Args: + item_id (str): The ID of the item to fetch. + collection_id (str): The ID of the collection containing the item. + + Returns: + pystac.Item: The fetched STAC item. + """ + url = f"{self.client.config.base_url}/collections/{collection_id}/items/{item_id}" + response = self.client.get(url) + response.raise_for_status() + return pystac.Item.from_dict(response.json()) + + def fetch_collection_items( + self, collection_id: str, parameters: SearchParameters = None + ) -> Generator[pystac.Item, None, None]: + """ + Fetch all items in a collection with pagination. + + Args: + collection_id (str): The ID of the collection. + parameters (SearchParameters): Optional additional parameters for the query. + + Yields: + pystac.Item: Parsed STAC item. + """ + if not parameters: + parameters = SearchParameters(collections=[collection_id]) + else: + parameters.collections = [collection_id] + + return self.search_items(parameters) + + def _paginate_items(self, url: str, body: dict) -> Generator[pystac.Item, None, None]: + """ + Handles pagination for the STAC search POST endpoint. + Fetches items one page at a time using the 'next' link. + + Args: + url (str): The base URL for the search endpoint. + body (dict): The request body containing search parameters. + + Yields: + pystac.Item: Parsed STAC item. + """ + params = {"limit": body.get("limit", 10)} # Default limit to 10 if not provided + + while True: + # Make the POST request + response = self.client.post(url, json=body, params=params) + response.raise_for_status() + data = response.json() + + # Process features (STAC items) + for feature in data.get("features", []): + yield pystac.Item.from_dict(feature) + + # Handle pagination via the 'next' link + next_link = next( + (link for link in data.get("links", []) if link.get("rel") == "next"), + None, + ) + if next_link: + next_href = next_link.get("href", "") + + # Validate the href + if not next_href: + self.client.logger.warning("Next link href is empty. Stopping pagination.") + break + + # Extract token from the href + try: + token = next_href.split("?")[1].split("=")[-1] + params["cursor"] = token + except IndexError: + self.client.logger.error(f"Failed to parse pagination token from {next_href}") + break + else: + break + From 7c939fa554628128092d70803057323ac122aa86 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 29 Jan 2025 17:35:20 +0000 Subject: [PATCH 073/135] Some refactoring to be able to read config variables easily --- datacosmos/stac/stac_client.py | 7 +++---- pyproject.toml | 2 +- requirements.txt | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 requirements.txt diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index ef0d343..e8e5cad 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -3,7 +3,6 @@ from datacosmos.client import DatacosmosClient from datacosmos.stac.models.search_parameters import SearchParameters - class STACClient: """ A client for interacting with the STAC API. @@ -17,7 +16,7 @@ def __init__(self, client: DatacosmosClient): client (DatacosmosClient): The authenticated Datacosmos client instance. """ self.client = client - self.base_url = "https://test.app.open-cosmos.com/api/data/v0/stac" + self.base_url = client.config.stac.as_domain_url() def search_items( self, parameters: SearchParameters @@ -31,7 +30,7 @@ def search_items( Yields: pystac.Item: Parsed STAC item. """ - url = f"{self.base_url}/search" + url = self.base_url.with_suffix("/search") body = parameters.model_dump(by_alias=True, exclude_none=True) return self._paginate_items(url, body) @@ -46,7 +45,7 @@ def fetch_item(self, item_id: str, collection_id: str) -> pystac.Item: Returns: pystac.Item: The fetched STAC item. """ - url = f"{self.client.config.base_url}/collections/{collection_id}/items/{item_id}" + url = self.base_url.with_suffix(f"/collections/{collection_id}/items/{item_id}") response = self.client.get(url) response.raise_for_status() return pystac.Item.from_dict(response.json()) diff --git a/pyproject.toml b/pyproject.toml index bdb8c33..906afbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] -dependencies = [ +dependencies = ["pydantic_settings>=2.7.0" "python-common==0.13.1", "black==22.3.0", "flake8==4.0.1", diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6a177e6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile pyproject.toml +# +annotated-types==0.7.0 + # via pydantic +pydantic==2.10.6 + # via pydantic-settings +pydantic-core==2.27.2 + # via pydantic +pydantic-settings==2.7.1 + # via datacosmos-sdk (pyproject.toml) +python-dotenv==1.0.1 + # via pydantic-settings +typing-extensions==4.12.2 + # via + # pydantic + # pydantic-core From 316f77342229088dfabcde3d1f875ff01b48a1b6 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Thu, 30 Jan 2025 10:12:16 +0000 Subject: [PATCH 074/135] Refactor fetch_collection_items method to just receive collection_id as input parameter --- datacosmos/stac/stac_client.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index e8e5cad..f0e1239 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -50,24 +50,17 @@ def fetch_item(self, item_id: str, collection_id: str) -> pystac.Item: response.raise_for_status() return pystac.Item.from_dict(response.json()) - def fetch_collection_items( - self, collection_id: str, parameters: SearchParameters = None - ) -> Generator[pystac.Item, None, None]: + def fetch_collection_items(self, collection_id: str) -> Generator[pystac.Item, None, None]: """ Fetch all items in a collection with pagination. Args: collection_id (str): The ID of the collection. - parameters (SearchParameters): Optional additional parameters for the query. Yields: pystac.Item: Parsed STAC item. """ - if not parameters: - parameters = SearchParameters(collections=[collection_id]) - else: - parameters.collections = [collection_id] - + parameters = SearchParameters(collections=[collection_id]) return self.search_items(parameters) def _paginate_items(self, url: str, body: dict) -> Generator[pystac.Item, None, None]: From 3e44baae3af4f5918d722ffe32818ac8317a3e59 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Thu, 30 Jan 2025 15:11:31 +0000 Subject: [PATCH 075/135] Add unit tests to stac client --- .../test_fetch_collection_items.py | 22 +++++++++++ .../stac/stac_client/test_fetch_item.py | 31 +++++++++++++++ .../stac/stac_client/test_search_items.py | 38 +++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py create mode 100644 tests/unit/datacosmos/stac/stac_client/test_fetch_item.py create mode 100644 tests/unit/datacosmos/stac/stac_client/test_search_items.py diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py new file mode 100644 index 0000000..9aa1413 --- /dev/null +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py @@ -0,0 +1,22 @@ +from unittest.mock import patch, MagicMock +from datacosmos.stac.stac_client import STACClient +from datacosmos.client import DatacosmosClient + + +@patch.object(STACClient, "search_items") +def test_fetch_collection_items(mock_search_items): + """Test fetching all items in a collection.""" + mock_search_items.return_value = iter([ + MagicMock(id="item-1"), + MagicMock(id="item-2"), + ]) + + client = DatacosmosClient() + stac_client = STACClient(client) + + results = list(stac_client.fetch_collection_items("test-collection")) + + assert len(results) == 2 + assert results[0].id == "item-1" + assert results[1].id == "item-2" + mock_search_items.assert_called_once() diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py new file mode 100644 index 0000000..51aeb68 --- /dev/null +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py @@ -0,0 +1,31 @@ +from unittest.mock import patch, MagicMock +from datacosmos.stac.stac_client import STACClient +from datacosmos.client import DatacosmosClient + + +@patch.object(DatacosmosClient, "get") +def test_fetch_item(mock_get): + """Test fetching a single STAC item by ID.""" + mock_response = MagicMock() + mock_response.json.return_value = { + "id": "item-1", + "collection": "test-collection", + "type": "Feature", + "stac_version": "1.0.0", + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "properties": { + "datetime": "2023-12-01T12:00:00Z" + }, + "assets": {}, + "links": [] + } + mock_get.return_value = mock_response + + client = DatacosmosClient() + stac_client = STACClient(client) + + item = stac_client.fetch_item("item-1", "test-collection") + + assert item.id == "item-1" + assert item.properties["datetime"] == "2023-12-01T12:00:00Z" + mock_get.assert_called_once() diff --git a/tests/unit/datacosmos/stac/stac_client/test_search_items.py b/tests/unit/datacosmos/stac/stac_client/test_search_items.py new file mode 100644 index 0000000..b8bd0c0 --- /dev/null +++ b/tests/unit/datacosmos/stac/stac_client/test_search_items.py @@ -0,0 +1,38 @@ +from unittest.mock import patch, MagicMock +from datacosmos.stac.stac_client import STACClient +from datacosmos.stac.models.search_parameters import SearchParameters +from datacosmos.client import DatacosmosClient + + +@patch.object(DatacosmosClient, "post") +def test_search_items(mock_post): + """Test searching STAC items with filters and pagination.""" + mock_response = MagicMock() + mock_response.json.return_value = { + "features": [ + { + "id": "item-1", + "collection": "test-collection", + "type": "Feature", + "stac_version": "1.0.0", + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "properties": { + "datetime": "2023-12-01T12:00:00Z" + }, + "assets": {}, + "links": [] + } + ], + "links": [] + } + mock_post.return_value = mock_response + + client = DatacosmosClient() + stac_client = STACClient(client) + parameters = SearchParameters(collections=["test-collection"]) + + results = list(stac_client.search_items(parameters)) + + assert len(results) == 1 + assert results[0].id == "item-1" + mock_post.assert_called_once() From 1e4fa8e436b1abcc8343da98ec78fce4613aacf6 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Thu, 30 Jan 2025 15:14:53 +0000 Subject: [PATCH 076/135] Make paginate_items private --- datacosmos/stac/stac_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index f0e1239..3ac5cd9 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -32,7 +32,7 @@ def search_items( """ url = self.base_url.with_suffix("/search") body = parameters.model_dump(by_alias=True, exclude_none=True) - return self._paginate_items(url, body) + return self.__paginate_items(url, body) def fetch_item(self, item_id: str, collection_id: str) -> pystac.Item: """ @@ -63,7 +63,7 @@ def fetch_collection_items(self, collection_id: str) -> Generator[pystac.Item, N parameters = SearchParameters(collections=[collection_id]) return self.search_items(parameters) - def _paginate_items(self, url: str, body: dict) -> Generator[pystac.Item, None, None]: + def __paginate_items(self, url: str, body: dict) -> Generator[pystac.Item, None, None]: """ Handles pagination for the STAC search POST endpoint. Fetches items one page at a time using the 'next' link. From 054e431b77ec0ed0021d34d51d495c2b4c9d2e19 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Thu, 30 Jan 2025 15:36:46 +0000 Subject: [PATCH 077/135] Fix pipeline issues --- datacosmos/stac/__init__.py | 7 ++ datacosmos/stac/models/search_parameters.py | 16 ++- datacosmos/stac/stac_client.py | 102 ++++++++++-------- .../test_fetch_collection_items.py | 5 +- .../stac/stac_client/test_fetch_item.py | 5 +- .../stac/stac_client/test_search_items.py | 7 +- 6 files changed, 88 insertions(+), 54 deletions(-) diff --git a/datacosmos/stac/__init__.py b/datacosmos/stac/__init__.py index e69de29..aa016c4 100644 --- a/datacosmos/stac/__init__.py +++ b/datacosmos/stac/__init__.py @@ -0,0 +1,7 @@ +""" +STAC package for interacting with the STAC API. + +This package provides functionalities to query, fetch, and interact +with STAC (SpatioTemporal Asset Catalog) services using an authenticated +Datacosmos client. +""" diff --git a/datacosmos/stac/models/search_parameters.py b/datacosmos/stac/models/search_parameters.py index 189cda9..af3868b 100644 --- a/datacosmos/stac/models/search_parameters.py +++ b/datacosmos/stac/models/search_parameters.py @@ -1,11 +1,18 @@ -from pydantic import BaseModel, Field, model_validator +""" +Module defining the SearchParameters model for STAC API queries. + +This module contains the `SearchParameters` model, which encapsulates +filtering criteria for searching STAC items, such as spatial, temporal, +and property-based filters. +""" + from typing import Optional, Union +from pydantic import BaseModel, Field, model_validator + class SearchParameters(BaseModel): - """ - Encapsulates the parameters for the STAC search API with validation. - """ + """Encapsulates the parameters for the STAC search API with validation.""" bbox: Optional[list[float]] = Field( None, @@ -47,6 +54,7 @@ class SearchParameters(BaseModel): @model_validator(mode="before") def validate_bbox(cls, values): + """Validate that the `bbox` field contains either 4 or 6 values.""" bbox = values.get("bbox") if bbox and len(bbox) not in {4, 6}: raise ValueError("bbox must contain 4 or 6 values.") diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index 3ac5cd9..4bd1ad1 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -1,16 +1,22 @@ +""" +STAC Client module. + +This module provides a client to interact with a STAC (SpatioTemporal Asset Catalog) API. +""" + +from typing import Generator, Optional + import pystac -from typing import Generator + from datacosmos.client import DatacosmosClient from datacosmos.stac.models.search_parameters import SearchParameters + class STACClient: - """ - A client for interacting with the STAC API. - """ + """Client for interacting with the STAC API.""" def __init__(self, client: DatacosmosClient): - """ - Initialize the STACClient with a DatacosmosClient. + """Initialize the STACClient with a DatacosmosClient. Args: client (DatacosmosClient): The authenticated Datacosmos client instance. @@ -21,8 +27,7 @@ def __init__(self, client: DatacosmosClient): def search_items( self, parameters: SearchParameters ) -> Generator[pystac.Item, None, None]: - """ - Query the STAC catalog using the POST endpoint with flexible filters and pagination. + """Query the STAC catalog using the POST endpoint with filtering and pagination. Args: parameters (SearchParameters): The search parameters. @@ -32,11 +37,10 @@ def search_items( """ url = self.base_url.with_suffix("/search") body = parameters.model_dump(by_alias=True, exclude_none=True) - return self.__paginate_items(url, body) + return self._paginate_items(url, body) def fetch_item(self, item_id: str, collection_id: str) -> pystac.Item: - """ - Fetch a single STAC item by ID. + """Fetch a single STAC item by ID. Args: item_id (str): The ID of the item to fetch. @@ -50,9 +54,10 @@ def fetch_item(self, item_id: str, collection_id: str) -> pystac.Item: response.raise_for_status() return pystac.Item.from_dict(response.json()) - def fetch_collection_items(self, collection_id: str) -> Generator[pystac.Item, None, None]: - """ - Fetch all items in a collection with pagination. + def fetch_collection_items( + self, collection_id: str + ) -> Generator[pystac.Item, None, None]: + """Fetch all items in a collection with pagination. Args: collection_id (str): The ID of the collection. @@ -63,9 +68,9 @@ def fetch_collection_items(self, collection_id: str) -> Generator[pystac.Item, N parameters = SearchParameters(collections=[collection_id]) return self.search_items(parameters) - def __paginate_items(self, url: str, body: dict) -> Generator[pystac.Item, None, None]: - """ - Handles pagination for the STAC search POST endpoint. + def _paginate_items(self, url: str, body: dict) -> Generator[pystac.Item, None, None]: + """Handle pagination for the STAC search POST endpoint. + Fetches items one page at a time using the 'next' link. Args: @@ -78,35 +83,46 @@ def __paginate_items(self, url: str, body: dict) -> Generator[pystac.Item, None, params = {"limit": body.get("limit", 10)} # Default limit to 10 if not provided while True: - # Make the POST request response = self.client.post(url, json=body, params=params) response.raise_for_status() data = response.json() - # Process features (STAC items) - for feature in data.get("features", []): - yield pystac.Item.from_dict(feature) - - # Handle pagination via the 'next' link - next_link = next( - (link for link in data.get("links", []) if link.get("rel") == "next"), - None, - ) - if next_link: - next_href = next_link.get("href", "") - - # Validate the href - if not next_href: - self.client.logger.warning("Next link href is empty. Stopping pagination.") - break - - # Extract token from the href - try: - token = next_href.split("?")[1].split("=")[-1] - params["cursor"] = token - except IndexError: - self.client.logger.error(f"Failed to parse pagination token from {next_href}") - break - else: + yield from (pystac.Item.from_dict(feature) for feature in data.get("features", [])) + + # Get next pagination link + next_href = self._get_next_link(data) + if not next_href: break + # Extract token for next page + token = self._extract_pagination_token(next_href) + if not token: + break + params["cursor"] = token + + def _get_next_link(self, data: dict) -> Optional[str]: + """Extract the next page link from the response. + + Args: + data (dict): The response JSON from the STAC API. + + Returns: + Optional[str]: The URL for the next page, or None if no next page exists. + """ + next_link = next((link for link in data.get("links", []) if link.get("rel") == "next"), None) + return next_link.get("href", "") if next_link else None + + def _extract_pagination_token(self, next_href: str) -> Optional[str]: + """Extract the pagination token from the next link URL. + + Args: + next_href (str): The next page URL. + + Returns: + Optional[str]: The extracted token, or None if parsing fails. + """ + try: + return next_href.split("?")[1].split("=")[-1] + except (IndexError, AttributeError): + self.client.logger.error(f"Failed to parse pagination token from {next_href}") + return None diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py index 9aa1413..54a1aa5 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py @@ -1,6 +1,7 @@ -from unittest.mock import patch, MagicMock -from datacosmos.stac.stac_client import STACClient +from unittest.mock import MagicMock, patch + from datacosmos.client import DatacosmosClient +from datacosmos.stac.stac_client import STACClient @patch.object(STACClient, "search_items") diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py index 51aeb68..f076507 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py @@ -1,6 +1,7 @@ -from unittest.mock import patch, MagicMock -from datacosmos.stac.stac_client import STACClient +from unittest.mock import MagicMock, patch + from datacosmos.client import DatacosmosClient +from datacosmos.stac.stac_client import STACClient @patch.object(DatacosmosClient, "get") diff --git a/tests/unit/datacosmos/stac/stac_client/test_search_items.py b/tests/unit/datacosmos/stac/stac_client/test_search_items.py index b8bd0c0..968acb0 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_search_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_search_items.py @@ -1,7 +1,8 @@ -from unittest.mock import patch, MagicMock -from datacosmos.stac.stac_client import STACClient -from datacosmos.stac.models.search_parameters import SearchParameters +from unittest.mock import MagicMock, patch + from datacosmos.client import DatacosmosClient +from datacosmos.stac.models.search_parameters import SearchParameters +from datacosmos.stac.stac_client import STACClient @patch.object(DatacosmosClient, "post") From bb3f9b52b3cf7dff6a5795513bd39d6baacf507c Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Thu, 30 Jan 2025 15:55:13 +0000 Subject: [PATCH 078/135] Fix more pipeline issues --- datacosmos/stac/__init__.py | 8 +++----- datacosmos/stac/models/search_parameters.py | 7 ++----- datacosmos/stac/stac_client.py | 5 ++--- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/datacosmos/stac/__init__.py b/datacosmos/stac/__init__.py index aa016c4..e87992f 100644 --- a/datacosmos/stac/__init__.py +++ b/datacosmos/stac/__init__.py @@ -1,7 +1,5 @@ -""" -STAC package for interacting with the STAC API. +"""STAC package for interacting with the STAC API, providing query and fetch functionalities. -This package provides functionalities to query, fetch, and interact -with STAC (SpatioTemporal Asset Catalog) services using an authenticated -Datacosmos client. +It enables interaction with STAC (SpatioTemporal Asset Catalog) services +using an authenticated Datacosmos client. """ diff --git a/datacosmos/stac/models/search_parameters.py b/datacosmos/stac/models/search_parameters.py index af3868b..8dd74d3 100644 --- a/datacosmos/stac/models/search_parameters.py +++ b/datacosmos/stac/models/search_parameters.py @@ -1,9 +1,6 @@ -""" -Module defining the SearchParameters model for STAC API queries. +"""Module defining the SearchParameters model for STAC API queries, encapsulating filtering criteria. -This module contains the `SearchParameters` model, which encapsulates -filtering criteria for searching STAC items, such as spatial, temporal, -and property-based filters. +It includes spatial, temporal, and property-based filters for querying STAC items efficiently. """ from typing import Optional, Union diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index 4bd1ad1..2799178 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -1,7 +1,6 @@ -""" -STAC Client module. +"""STAC Client module for interacting with a STAC (SpatioTemporal Asset Catalog) API. -This module provides a client to interact with a STAC (SpatioTemporal Asset Catalog) API. +Provides methods for querying, fetching, and paginating STAC items. """ from typing import Generator, Optional From a51e9773c8d53490c39f2817bd833f20f52ab70c Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Thu, 30 Jan 2025 16:32:58 +0000 Subject: [PATCH 079/135] Try to add python-common in a different way --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 6a177e6..eef3064 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,4 @@ typing-extensions==4.12.2 # via # pydantic # pydantic-core + From 37a73bb9d0dca7a33d5701fb1974958a7802843f Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Thu, 30 Jan 2025 17:19:40 +0000 Subject: [PATCH 080/135] Try to add python-common in a different way --- .github/workflows/main.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d2983ed..0d6f4c6 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,5 +1,8 @@ name: main +env: + GITLAB_PYPI_TOKEN: ${{ secrets.GITLAB_PYPI_TOKEN }} + on: push: branches: From ee997211732cd0b13618713890cfb488065f116f Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Thu, 30 Jan 2025 17:30:00 +0000 Subject: [PATCH 081/135] Install dependencies with poetry and use secret for private repo usage --- .github/workflows/main.yaml | 3 --- pyproject.toml | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 0d6f4c6..d2983ed 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,8 +1,5 @@ name: main -env: - GITLAB_PYPI_TOKEN: ${{ secrets.GITLAB_PYPI_TOKEN }} - on: push: branches: diff --git a/pyproject.toml b/pyproject.toml index 906afbe..0113711 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" [project] name = "datacosmos" From f85bbe0e97379c5ea409851a66c16d5f26c43850 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 31 Jan 2025 06:29:14 +0000 Subject: [PATCH 082/135] More changes to pyproject.toml --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0113711..906afbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" [project] name = "datacosmos" From 0e8e2a86bdb6063725ca3578f872fb3d84dda592 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 31 Jan 2025 10:46:20 +0000 Subject: [PATCH 083/135] Apply black --- datacosmos/stac/stac_client.py | 16 ++++++++++++---- .../stac_client/test_fetch_collection_items.py | 10 ++++++---- .../stac/stac_client/test_fetch_item.py | 6 ++---- .../stac/stac_client/test_search_items.py | 8 +++----- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index 2799178..3eac88e 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -67,7 +67,9 @@ def fetch_collection_items( parameters = SearchParameters(collections=[collection_id]) return self.search_items(parameters) - def _paginate_items(self, url: str, body: dict) -> Generator[pystac.Item, None, None]: + def _paginate_items( + self, url: str, body: dict + ) -> Generator[pystac.Item, None, None]: """Handle pagination for the STAC search POST endpoint. Fetches items one page at a time using the 'next' link. @@ -86,7 +88,9 @@ def _paginate_items(self, url: str, body: dict) -> Generator[pystac.Item, None, response.raise_for_status() data = response.json() - yield from (pystac.Item.from_dict(feature) for feature in data.get("features", [])) + yield from ( + pystac.Item.from_dict(feature) for feature in data.get("features", []) + ) # Get next pagination link next_href = self._get_next_link(data) @@ -108,7 +112,9 @@ def _get_next_link(self, data: dict) -> Optional[str]: Returns: Optional[str]: The URL for the next page, or None if no next page exists. """ - next_link = next((link for link in data.get("links", []) if link.get("rel") == "next"), None) + next_link = next( + (link for link in data.get("links", []) if link.get("rel") == "next"), None + ) return next_link.get("href", "") if next_link else None def _extract_pagination_token(self, next_href: str) -> Optional[str]: @@ -123,5 +129,7 @@ def _extract_pagination_token(self, next_href: str) -> Optional[str]: try: return next_href.split("?")[1].split("=")[-1] except (IndexError, AttributeError): - self.client.logger.error(f"Failed to parse pagination token from {next_href}") + self.client.logger.error( + f"Failed to parse pagination token from {next_href}" + ) return None diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py index 54a1aa5..e894f66 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py @@ -7,10 +7,12 @@ @patch.object(STACClient, "search_items") def test_fetch_collection_items(mock_search_items): """Test fetching all items in a collection.""" - mock_search_items.return_value = iter([ - MagicMock(id="item-1"), - MagicMock(id="item-2"), - ]) + mock_search_items.return_value = iter( + [ + MagicMock(id="item-1"), + MagicMock(id="item-2"), + ] + ) client = DatacosmosClient() stac_client = STACClient(client) diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py index f076507..b80318c 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py @@ -14,11 +14,9 @@ def test_fetch_item(mock_get): "type": "Feature", "stac_version": "1.0.0", "geometry": {"type": "Point", "coordinates": [0, 0]}, - "properties": { - "datetime": "2023-12-01T12:00:00Z" - }, + "properties": {"datetime": "2023-12-01T12:00:00Z"}, "assets": {}, - "links": [] + "links": [], } mock_get.return_value = mock_response diff --git a/tests/unit/datacosmos/stac/stac_client/test_search_items.py b/tests/unit/datacosmos/stac/stac_client/test_search_items.py index 968acb0..8edd98c 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_search_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_search_items.py @@ -17,14 +17,12 @@ def test_search_items(mock_post): "type": "Feature", "stac_version": "1.0.0", "geometry": {"type": "Point", "coordinates": [0, 0]}, - "properties": { - "datetime": "2023-12-01T12:00:00Z" - }, + "properties": {"datetime": "2023-12-01T12:00:00Z"}, "assets": {}, - "links": [] + "links": [], } ], - "links": [] + "links": [], } mock_post.return_value = mock_response From c0c76356c5b888af27d14f15ff76a7dfd8df5ffc Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 31 Jan 2025 12:29:04 +0000 Subject: [PATCH 084/135] Do rebase --- datacosmos/client.py | 2 +- datacosmos/stac/stac_client.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index d631b14..f7025d8 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -113,4 +113,4 @@ def put(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: def delete(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: """Send a DELETE request using the authenticated session.""" - return self.request("DELETE", url, *args, **kwargs) + return self.request("DELETE", url, *args, **kwargs) \ No newline at end of file diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index 3eac88e..7ee3108 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -132,4 +132,4 @@ def _extract_pagination_token(self, next_href: str) -> Optional[str]: self.client.logger.error( f"Failed to parse pagination token from {next_href}" ) - return None + return None \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 906afbe..bdb8c33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] -dependencies = ["pydantic_settings>=2.7.0" +dependencies = [ "python-common==0.13.1", "black==22.3.0", "flake8==4.0.1", From a995f1100282acf479452095d270c6f6b3e58e15 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 31 Jan 2025 12:31:16 +0000 Subject: [PATCH 085/135] Add new line at end of files --- datacosmos/client.py | 2 +- datacosmos/stac/stac_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index f7025d8..d631b14 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -113,4 +113,4 @@ def put(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: def delete(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: """Send a DELETE request using the authenticated session.""" - return self.request("DELETE", url, *args, **kwargs) \ No newline at end of file + return self.request("DELETE", url, *args, **kwargs) diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index 7ee3108..3eac88e 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -132,4 +132,4 @@ def _extract_pagination_token(self, next_href: str) -> Optional[str]: self.client.logger.error( f"Failed to parse pagination token from {next_href}" ) - return None \ No newline at end of file + return None From 43010d62dfb9bd29ff9aeb3833de2a0b0ee57f56 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 31 Jan 2025 15:12:35 +0000 Subject: [PATCH 086/135] remove requirements.txt --- requirements.txt | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index eef3064..0000000 --- a/requirements.txt +++ /dev/null @@ -1,21 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile pyproject.toml -# -annotated-types==0.7.0 - # via pydantic -pydantic==2.10.6 - # via pydantic-settings -pydantic-core==2.27.2 - # via pydantic -pydantic-settings==2.7.1 - # via datacosmos-sdk (pyproject.toml) -python-dotenv==1.0.1 - # via pydantic-settings -typing-extensions==4.12.2 - # via - # pydantic - # pydantic-core - From c8feca439a64f11e5c234cde98cbc29a0b4a26bb Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Tue, 4 Feb 2025 18:45:12 +0000 Subject: [PATCH 087/135] fix stac client unit tests --- .../test_fetch_collection_items.py | 25 +++++++++++++------ .../stac/stac_client/test_fetch_item.py | 20 ++++++++++++--- .../stac/stac_client/test_search_items.py | 20 ++++++++++++--- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py index e894f66..b9ca5fe 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py @@ -1,20 +1,31 @@ from unittest.mock import MagicMock, patch - from datacosmos.client import DatacosmosClient from datacosmos.stac.stac_client import STACClient +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig +@patch("requests_oauthlib.OAuth2Session.fetch_token") @patch.object(STACClient, "search_items") -def test_fetch_collection_items(mock_search_items): +def test_fetch_collection_items(mock_search_items, mock_fetch_token): """Test fetching all items in a collection.""" + mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} + mock_search_items.return_value = iter( - [ - MagicMock(id="item-1"), - MagicMock(id="item-2"), - ] + [MagicMock(id="item-1"), MagicMock(id="item-2")] + ) + + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) ) - client = DatacosmosClient() + client = DatacosmosClient(config=config) stac_client = STACClient(client) results = list(stac_client.fetch_collection_items("test-collection")) diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py index b80318c..849d08e 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py @@ -1,12 +1,16 @@ from unittest.mock import MagicMock, patch - from datacosmos.client import DatacosmosClient from datacosmos.stac.stac_client import STACClient +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig +@patch("requests_oauthlib.OAuth2Session.fetch_token") @patch.object(DatacosmosClient, "get") -def test_fetch_item(mock_get): +def test_fetch_item(mock_get, mock_fetch_token): """Test fetching a single STAC item by ID.""" + mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} + mock_response = MagicMock() mock_response.json.return_value = { "id": "item-1", @@ -20,7 +24,17 @@ def test_fetch_item(mock_get): } mock_get.return_value = mock_response - client = DatacosmosClient() + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + ) + + client = DatacosmosClient(config=config) stac_client = STACClient(client) item = stac_client.fetch_item("item-1", "test-collection") diff --git a/tests/unit/datacosmos/stac/stac_client/test_search_items.py b/tests/unit/datacosmos/stac/stac_client/test_search_items.py index 8edd98c..35a56b7 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_search_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_search_items.py @@ -1,13 +1,17 @@ from unittest.mock import MagicMock, patch - from datacosmos.client import DatacosmosClient from datacosmos.stac.models.search_parameters import SearchParameters from datacosmos.stac.stac_client import STACClient +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig +@patch("requests_oauthlib.OAuth2Session.fetch_token") @patch.object(DatacosmosClient, "post") -def test_search_items(mock_post): +def test_search_items(mock_post, mock_fetch_token): """Test searching STAC items with filters and pagination.""" + mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} + mock_response = MagicMock() mock_response.json.return_value = { "features": [ @@ -26,7 +30,17 @@ def test_search_items(mock_post): } mock_post.return_value = mock_response - client = DatacosmosClient() + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + ) + + client = DatacosmosClient(config=config) stac_client = STACClient(client) parameters = SearchParameters(collections=["test-collection"]) From 49d57802f461631e71ed7afd673faa7f9c484f79 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Tue, 4 Feb 2025 18:47:39 +0000 Subject: [PATCH 088/135] fix linting issues --- .../stac/stac_client/test_fetch_collection_items.py | 7 ++++--- tests/unit/datacosmos/stac/stac_client/test_fetch_item.py | 7 ++++--- .../unit/datacosmos/stac/stac_client/test_search_items.py | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py index b9ca5fe..50de2a5 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py @@ -1,8 +1,9 @@ from unittest.mock import MagicMock, patch -from datacosmos.client import DatacosmosClient -from datacosmos.stac.stac_client import STACClient + from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig +from datacosmos.client import DatacosmosClient +from datacosmos.stac.stac_client import STACClient @patch("requests_oauthlib.OAuth2Session.fetch_token") @@ -10,7 +11,7 @@ def test_fetch_collection_items(mock_search_items, mock_fetch_token): """Test fetching all items in a collection.""" mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} - + mock_search_items.return_value = iter( [MagicMock(id="item-1"), MagicMock(id="item-2")] ) diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py index 849d08e..e066022 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py @@ -1,8 +1,9 @@ from unittest.mock import MagicMock, patch -from datacosmos.client import DatacosmosClient -from datacosmos.stac.stac_client import STACClient + from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig +from datacosmos.client import DatacosmosClient +from datacosmos.stac.stac_client import STACClient @patch("requests_oauthlib.OAuth2Session.fetch_token") @@ -10,7 +11,7 @@ def test_fetch_item(mock_get, mock_fetch_token): """Test fetching a single STAC item by ID.""" mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} - + mock_response = MagicMock() mock_response.json.return_value = { "id": "item-1", diff --git a/tests/unit/datacosmos/stac/stac_client/test_search_items.py b/tests/unit/datacosmos/stac/stac_client/test_search_items.py index 35a56b7..08ebd49 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_search_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_search_items.py @@ -1,9 +1,10 @@ from unittest.mock import MagicMock, patch + +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient from datacosmos.stac.models.search_parameters import SearchParameters from datacosmos.stac.stac_client import STACClient -from config.config import Config -from config.models.m2m_authentication_config import M2MAuthenticationConfig @patch("requests_oauthlib.OAuth2Session.fetch_token") From f04d32d5f1728ba632ec5166d224d3c3b39d6878 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 29 Jan 2025 17:35:20 +0000 Subject: [PATCH 089/135] Some refactoring to be able to read config variables easily --- datacosmos/stac/stac_client.py | 1 - pyproject.toml | 2 +- requirements.txt | 20 ++++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 requirements.txt diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index 3eac88e..d401bd1 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -10,7 +10,6 @@ from datacosmos.client import DatacosmosClient from datacosmos.stac.models.search_parameters import SearchParameters - class STACClient: """Client for interacting with the STAC API.""" diff --git a/pyproject.toml b/pyproject.toml index bdb8c33..906afbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] -dependencies = [ +dependencies = ["pydantic_settings>=2.7.0" "python-common==0.13.1", "black==22.3.0", "flake8==4.0.1", diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6a177e6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile pyproject.toml +# +annotated-types==0.7.0 + # via pydantic +pydantic==2.10.6 + # via pydantic-settings +pydantic-core==2.27.2 + # via pydantic +pydantic-settings==2.7.1 + # via datacosmos-sdk (pyproject.toml) +python-dotenv==1.0.1 + # via pydantic-settings +typing-extensions==4.12.2 + # via + # pydantic + # pydantic-core From f5c81fcee583908d51bd8b346f3d1eec821df153 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Tue, 4 Feb 2025 11:57:19 +0000 Subject: [PATCH 090/135] Add new methods for stac item management --- datacosmos/stac/stac_client.py | 141 ++++++++++++++++++++------------- 1 file changed, 86 insertions(+), 55 deletions(-) diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index d401bd1..e949026 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -1,14 +1,15 @@ -"""STAC Client module for interacting with a STAC (SpatioTemporal Asset Catalog) API. +""" +STAC Client module for interacting with a STAC (SpatioTemporal Asset Catalog) API. -Provides methods for querying, fetching, and paginating STAC items. +Provides methods for querying, fetching, creating, updating, and deleting STAC items. """ from typing import Generator, Optional -import pystac - +from pystac import Item from datacosmos.client import DatacosmosClient from datacosmos.stac.models.search_parameters import SearchParameters +from common.sdk.http_response import check_api_response class STACClient: """Client for interacting with the STAC API.""" @@ -22,53 +23,105 @@ def __init__(self, client: DatacosmosClient): self.client = client self.base_url = client.config.stac.as_domain_url() + def fetch_item(self, item_id: str, collection_id: str) -> Item: + """Fetch a single STAC item by ID. + + Args: + item_id (str): The ID of the item to fetch. + collection_id (str): The ID of the collection containing the item. + + Returns: + Item: The fetched STAC item. + """ + url = self.base_url.with_suffix(f"/collections/{collection_id}/items/{item_id}") + response = self.client.get(url) + check_api_response(response) + return Item.from_dict(response.json()) + + def fetch_collection_items( + self, collection_id: str, parameters: Optional[SearchParameters] = None + ) -> Generator[Item, None, None]: + """Fetch all items in a collection with optional filtering. + + Args: + collection_id (str): The ID of the collection. + parameters (Optional[SearchParameters]): Filtering parameters (spatial, temporal, etc.). + + Yields: + Item: Parsed STAC item. + """ + if parameters is None: + parameters = SearchParameters(collections=[collection_id]) + + return self.search_items(parameters) + def search_items( self, parameters: SearchParameters - ) -> Generator[pystac.Item, None, None]: + ) -> Generator[Item, None, None]: """Query the STAC catalog using the POST endpoint with filtering and pagination. Args: parameters (SearchParameters): The search parameters. Yields: - pystac.Item: Parsed STAC item. + Item: Parsed STAC item. """ url = self.base_url.with_suffix("/search") body = parameters.model_dump(by_alias=True, exclude_none=True) return self._paginate_items(url, body) - def fetch_item(self, item_id: str, collection_id: str) -> pystac.Item: - """Fetch a single STAC item by ID. + def create_item(self, collection_id: str, item: Item) -> Item: + """Create a new STAC item in a specified collection. Args: - item_id (str): The ID of the item to fetch. + collection_id (str): The ID of the collection where the item will be created. + item (Item): The STAC Item to be created. + + Returns: + Item: The created STAC Item. + + Raises: + RequestError: If the API returns an error response. + """ + url = self.base_url.with_suffix(f"/collections/{collection_id}/items") + item_json: dict = item.to_dict() # Convert the STAC Item to JSON format + + response = self.client.post(url, json=item_json) + check_api_response(response) + + return Item.from_dict(response.json()) + + def update_item(self, item_id: str, collection_id: str, update_data: dict) -> Item: + """Update an existing STAC item. + + Args: + item_id (str): The ID of the item to update. collection_id (str): The ID of the collection containing the item. + update_data (dict): The update data (partial or full). Returns: - pystac.Item: The fetched STAC item. + Item: The updated STAC item. """ url = self.base_url.with_suffix(f"/collections/{collection_id}/items/{item_id}") - response = self.client.get(url) - response.raise_for_status() - return pystac.Item.from_dict(response.json()) + response = self.client.patch(url, json=update_data) + check_api_response(response) + return Item.from_dict(response.json()) - def fetch_collection_items( - self, collection_id: str - ) -> Generator[pystac.Item, None, None]: - """Fetch all items in a collection with pagination. + def delete_item(self, item_id: str, collection_id: str) -> None: + """Delete a STAC item by its ID. Args: - collection_id (str): The ID of the collection. + item_id (str): The ID of the item to delete. + collection_id (str): The ID of the collection containing the item. - Yields: - pystac.Item: Parsed STAC item. + Raises: + OCError: If the item is not found or deletion is forbidden. """ - parameters = SearchParameters(collections=[collection_id]) - return self.search_items(parameters) + url = self.base_url.with_suffix(f"/collections/{collection_id}/items/{item_id}") + response = self.client.delete(url) + check_api_response(response) - def _paginate_items( - self, url: str, body: dict - ) -> Generator[pystac.Item, None, None]: + def _paginate_items(self, url: str, body: dict) -> Generator[Item, None, None]: """Handle pagination for the STAC search POST endpoint. Fetches items one page at a time using the 'next' link. @@ -78,57 +131,35 @@ def _paginate_items( body (dict): The request body containing search parameters. Yields: - pystac.Item: Parsed STAC item. + Item: Parsed STAC item. """ - params = {"limit": body.get("limit", 10)} # Default limit to 10 if not provided + params = {"limit": body.get("limit", 10)} while True: response = self.client.post(url, json=body, params=params) - response.raise_for_status() + check_api_response(response) data = response.json() - yield from ( - pystac.Item.from_dict(feature) for feature in data.get("features", []) - ) + yield from (Item.from_dict(feature) for feature in data.get("features", [])) - # Get next pagination link next_href = self._get_next_link(data) if not next_href: break - # Extract token for next page token = self._extract_pagination_token(next_href) if not token: break params["cursor"] = token def _get_next_link(self, data: dict) -> Optional[str]: - """Extract the next page link from the response. - - Args: - data (dict): The response JSON from the STAC API. - - Returns: - Optional[str]: The URL for the next page, or None if no next page exists. - """ - next_link = next( - (link for link in data.get("links", []) if link.get("rel") == "next"), None - ) + """Extract the next page link from the response.""" + next_link = next((link for link in data.get("links", []) if link.get("rel") == "next"), None) return next_link.get("href", "") if next_link else None def _extract_pagination_token(self, next_href: str) -> Optional[str]: - """Extract the pagination token from the next link URL. - - Args: - next_href (str): The next page URL. - - Returns: - Optional[str]: The extracted token, or None if parsing fails. - """ + """Extract the pagination token from the next link URL.""" try: return next_href.split("?")[1].split("=")[-1] except (IndexError, AttributeError): - self.client.logger.error( - f"Failed to parse pagination token from {next_href}" - ) + self.client.logger.error(f"Failed to parse pagination token from {next_href}") return None From 95861d71ecc228f353566929e14c7c30ddb97292 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 14:34:37 +0000 Subject: [PATCH 091/135] Adapt unit tests to method check_api_response added to stac_client --- pyproject.toml | 2 +- .../stac_client/test_fetch_collection_items.py | 14 ++++++++++---- .../datacosmos/stac/stac_client/test_fetch_item.py | 7 ++++++- .../stac/stac_client/test_search_items.py | 7 ++++++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 906afbe..77212cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] -dependencies = ["pydantic_settings>=2.7.0" +dependencies = ["pydantic_settings>=2.7.0", "python-common==0.13.1", "black==22.3.0", "flake8==4.0.1", diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py index 50de2a5..7b821f2 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py @@ -7,14 +7,18 @@ @patch("requests_oauthlib.OAuth2Session.fetch_token") +@patch("datacosmos.stac.stac_client.check_api_response") @patch.object(STACClient, "search_items") -def test_fetch_collection_items(mock_search_items, mock_fetch_token): +def test_fetch_collection_items(mock_search_items, mock_check_api_response, mock_fetch_token): """Test fetching all items in a collection.""" mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} - mock_search_items.return_value = iter( - [MagicMock(id="item-1"), MagicMock(id="item-2")] - ) + mock_response_1 = MagicMock(id="item-1") + mock_response_2 = MagicMock(id="item-2") + + mock_search_items.return_value = iter([mock_response_1, mock_response_2]) + + mock_check_api_response.return_value = None config = Config( authentication=M2MAuthenticationConfig( @@ -34,4 +38,6 @@ def test_fetch_collection_items(mock_search_items, mock_fetch_token): assert len(results) == 2 assert results[0].id == "item-1" assert results[1].id == "item-2" + mock_search_items.assert_called_once() + mock_check_api_response.assert_not_called() diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py index e066022..59957d4 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py @@ -7,12 +7,14 @@ @patch("requests_oauthlib.OAuth2Session.fetch_token") +@patch("datacosmos.stac.stac_client.check_api_response") @patch.object(DatacosmosClient, "get") -def test_fetch_item(mock_get, mock_fetch_token): +def test_fetch_item(mock_get, mock_check_api_response, mock_fetch_token): """Test fetching a single STAC item by ID.""" mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} mock_response = MagicMock() + mock_response.status_code = 200 mock_response.json.return_value = { "id": "item-1", "collection": "test-collection", @@ -25,6 +27,8 @@ def test_fetch_item(mock_get, mock_fetch_token): } mock_get.return_value = mock_response + mock_check_api_response.return_value = None + config = Config( authentication=M2MAuthenticationConfig( type="m2m", @@ -43,3 +47,4 @@ def test_fetch_item(mock_get, mock_fetch_token): assert item.id == "item-1" assert item.properties["datetime"] == "2023-12-01T12:00:00Z" mock_get.assert_called_once() + mock_check_api_response.assert_called_once_with(mock_response) diff --git a/tests/unit/datacosmos/stac/stac_client/test_search_items.py b/tests/unit/datacosmos/stac/stac_client/test_search_items.py index 08ebd49..1f28116 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_search_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_search_items.py @@ -8,12 +8,14 @@ @patch("requests_oauthlib.OAuth2Session.fetch_token") +@patch("datacosmos.stac.stac_client.check_api_response") @patch.object(DatacosmosClient, "post") -def test_search_items(mock_post, mock_fetch_token): +def test_search_items(mock_post, mock_check_api_response, mock_fetch_token): """Test searching STAC items with filters and pagination.""" mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} mock_response = MagicMock() + mock_response.status_code = 200 # Ensure the mock behaves like a real response mock_response.json.return_value = { "features": [ { @@ -31,6 +33,8 @@ def test_search_items(mock_post, mock_fetch_token): } mock_post.return_value = mock_response + mock_check_api_response.return_value = None + config = Config( authentication=M2MAuthenticationConfig( type="m2m", @@ -50,3 +54,4 @@ def test_search_items(mock_post, mock_fetch_token): assert len(results) == 1 assert results[0].id == "item-1" mock_post.assert_called_once() + mock_check_api_response.assert_called_once_with(mock_response) # Ensure the API check was called From c444cdfd852d5e3d110d4c71077879e1f571014a Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 15:56:11 +0000 Subject: [PATCH 092/135] Create itemupdate model; create unittest for new methods in stac item --- datacosmos/client.py | 4 ++ datacosmos/stac/models/item_update.py | 22 ++++++++ datacosmos/stac/stac_client.py | 41 +++++++++++---- .../client/test_client_patch_request.py | 40 +++++++++++++++ .../stac/stac_client/test_create_item.py | 49 ++++++++++++++++++ .../stac/stac_client/test_delete_item.py | 36 +++++++++++++ .../stac/stac_client/test_update_item.py | 50 +++++++++++++++++++ 7 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 datacosmos/stac/models/item_update.py create mode 100644 tests/unit/datacosmos/client/test_client_patch_request.py create mode 100644 tests/unit/datacosmos/stac/stac_client/test_create_item.py create mode 100644 tests/unit/datacosmos/stac/stac_client/test_delete_item.py create mode 100644 tests/unit/datacosmos/stac/stac_client/test_update_item.py diff --git a/datacosmos/client.py b/datacosmos/client.py index d631b14..ca4dc82 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -110,6 +110,10 @@ def post(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: def put(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: """Send a PUT request using the authenticated session.""" return self.request("PUT", url, *args, **kwargs) + + def patch(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: + """Send a PATCH request using the authenticated session.""" + return self.request("PATCH", url, *args, **kwargs) def delete(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: """Send a DELETE request using the authenticated session.""" diff --git a/datacosmos/stac/models/item_update.py b/datacosmos/stac/models/item_update.py new file mode 100644 index 0000000..89400e8 --- /dev/null +++ b/datacosmos/stac/models/item_update.py @@ -0,0 +1,22 @@ +from typing import Optional, Any +from pydantic import BaseModel, Field +from pystac import Asset, Link +from shapely.geometry import mapping + + +class ItemUpdate(BaseModel): + """Model representing a partial update for a STAC item.""" + model_config = { + "arbitrary_types_allowed": True + } + + stac_extensions: Optional[list[str]] = None + geometry: Optional[dict[str, Any]] = None + bbox: Optional[list[float]] = Field(None, min_items=4, max_items=4) # Must be [minX, minY, maxX, maxY] + properties: Optional[dict[str, Any]] = None + assets: Optional[dict[str, Asset]] = None + links: Optional[list[Link]] = None + + def set_geometry(self, geom) -> None: + """Convert a shapely geometry to GeoJSON format.""" + self.geometry = mapping(geom) \ No newline at end of file diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index e949026..ac9ca15 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -9,7 +9,8 @@ from pystac import Item from datacosmos.client import DatacosmosClient from datacosmos.stac.models.search_parameters import SearchParameters -from common.sdk.http_response import check_api_response +from datacosmos.stac.models.item_update import ItemUpdate +from common.sdk.http_response import check_api_response, InvalidRequest class STACClient: """Client for interacting with the STAC API.""" @@ -84,28 +85,39 @@ def create_item(self, collection_id: str, item: Item) -> Item: RequestError: If the API returns an error response. """ url = self.base_url.with_suffix(f"/collections/{collection_id}/items") - item_json: dict = item.to_dict() # Convert the STAC Item to JSON format + item_json: dict = item.to_dict() response = self.client.post(url, json=item_json) check_api_response(response) return Item.from_dict(response.json()) - def update_item(self, item_id: str, collection_id: str, update_data: dict) -> Item: - """Update an existing STAC item. + def update_item(self, item_id: str, collection_id: str, update_data: ItemUpdate) -> Item: + """Partially update an existing STAC item. Args: item_id (str): The ID of the item to update. collection_id (str): The ID of the collection containing the item. - update_data (dict): The update data (partial or full). + update_data (ItemUpdate): The structured update payload. Returns: Item: The updated STAC item. """ url = self.base_url.with_suffix(f"/collections/{collection_id}/items/{item_id}") - response = self.client.patch(url, json=update_data) + + update_payload = update_data.model_dump(by_alias=True, exclude_none=True) + + if "assets" in update_payload: + update_payload["assets"] = { + key: asset.to_dict() for key, asset in update_payload["assets"].items() + } + if "links" in update_payload: + update_payload["links"] = [link.to_dict() for link in update_payload["links"]] + + response = self.client.patch(url, json=update_payload) check_api_response(response) - return Item.from_dict(response.json()) + + return Item.from_dict(response.json()) def delete_item(self, item_id: str, collection_id: str) -> None: """Delete a STAC item by its ID. @@ -157,9 +169,18 @@ def _get_next_link(self, data: dict) -> Optional[str]: return next_link.get("href", "") if next_link else None def _extract_pagination_token(self, next_href: str) -> Optional[str]: - """Extract the pagination token from the next link URL.""" + """Extract the pagination token from the next link URL. + + Args: + next_href (str): The next page URL. + + Returns: + Optional[str]: The extracted token, or None if parsing fails. + + Raises: + InvalidRequest: If pagination token extraction fails. + """ try: return next_href.split("?")[1].split("=")[-1] except (IndexError, AttributeError): - self.client.logger.error(f"Failed to parse pagination token from {next_href}") - return None + raise InvalidRequest(f"Failed to parse pagination token from {next_href}") diff --git a/tests/unit/datacosmos/client/test_client_patch_request.py b/tests/unit/datacosmos/client/test_client_patch_request.py new file mode 100644 index 0000000..fb92c52 --- /dev/null +++ b/tests/unit/datacosmos/client/test_client_patch_request.py @@ -0,0 +1,40 @@ +from unittest.mock import MagicMock, patch + +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig +from datacosmos.client import DatacosmosClient + + +@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +def test_patch_request(mock_auth_client): + """Test that the client performs a PATCH request correctly.""" + # Mock the HTTP client + mock_http_client = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"message": "updated"} + mock_http_client.request.return_value = mock_response + mock_auth_client.return_value = mock_http_client + + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + ) + + client = DatacosmosClient(config=config) + response = client.patch( + "https://mock.api/some-endpoint", json={"key": "updated-value"} + ) + + # Assertions + assert response.status_code == 200 + assert response.json() == {"message": "updated"} + mock_http_client.request.assert_called_once_with( + "PATCH", "https://mock.api/some-endpoint", json={"key": "updated-value"} + ) + mock_auth_client.call_count == 2 diff --git a/tests/unit/datacosmos/stac/stac_client/test_create_item.py b/tests/unit/datacosmos/stac/stac_client/test_create_item.py new file mode 100644 index 0000000..cf34357 --- /dev/null +++ b/tests/unit/datacosmos/stac/stac_client/test_create_item.py @@ -0,0 +1,49 @@ +from unittest.mock import MagicMock, patch +from pystac import Item + +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig +from datacosmos.client import DatacosmosClient +from datacosmos.stac.stac_client import STACClient + + +@patch("requests_oauthlib.OAuth2Session.fetch_token") +@patch.object(DatacosmosClient, "post") +@patch("datacosmos.stac.stac_client.check_api_response") +def test_create_item(mock_check_api_response, mock_post, mock_fetch_token): + """Test creating a new STAC item.""" + mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} + + mock_response = MagicMock() + mock_response.json.return_value = { + "id": "item-1", + "collection": "test-collection", + "type": "Feature", + "stac_version": "1.0.0", + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "properties": {"datetime": "2023-12-01T12:00:00Z"}, + "assets": {}, + "links": [], + } + mock_post.return_value = mock_response + + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + ) + + client = DatacosmosClient(config=config) + stac_client = STACClient(client) + + item = Item.from_dict(mock_response.json()) + created_item = stac_client.create_item("test-collection", item) + + assert created_item.id == "item-1" + assert created_item.properties["datetime"] == "2023-12-01T12:00:00Z" + mock_post.assert_called_once() + mock_check_api_response.assert_called_once() diff --git a/tests/unit/datacosmos/stac/stac_client/test_delete_item.py b/tests/unit/datacosmos/stac/stac_client/test_delete_item.py new file mode 100644 index 0000000..d014220 --- /dev/null +++ b/tests/unit/datacosmos/stac/stac_client/test_delete_item.py @@ -0,0 +1,36 @@ +from unittest.mock import MagicMock, patch + +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig +from datacosmos.client import DatacosmosClient +from datacosmos.stac.stac_client import STACClient + + +@patch("requests_oauthlib.OAuth2Session.fetch_token") +@patch.object(DatacosmosClient, "delete") +@patch("datacosmos.stac.stac_client.check_api_response") +def test_delete_item(mock_check_api_response, mock_delete, mock_fetch_token): + """Test deleting a STAC item.""" + mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} + + mock_response = MagicMock() + mock_response.status_code = 204 # Successful deletion + mock_delete.return_value = mock_response + + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + ) + + client = DatacosmosClient(config=config) + stac_client = STACClient(client) + + stac_client.delete_item("item-1", "test-collection") + + mock_delete.assert_called_once() + mock_check_api_response.assert_called_once() diff --git a/tests/unit/datacosmos/stac/stac_client/test_update_item.py b/tests/unit/datacosmos/stac/stac_client/test_update_item.py new file mode 100644 index 0000000..0e2c4bf --- /dev/null +++ b/tests/unit/datacosmos/stac/stac_client/test_update_item.py @@ -0,0 +1,50 @@ +from unittest.mock import MagicMock, patch +from pystac import Item +from datacosmos.stac.models.item_update import ItemUpdate + +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig +from datacosmos.client import DatacosmosClient +from datacosmos.stac.stac_client import STACClient + + +@patch("requests_oauthlib.OAuth2Session.fetch_token") +@patch.object(DatacosmosClient, "patch") +@patch("datacosmos.stac.stac_client.check_api_response") +def test_update_item(mock_check_api_response, mock_patch, mock_fetch_token): + """Test updating an existing STAC item.""" + mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} + + mock_response = MagicMock() + mock_response.json.return_value = { + "id": "item-1", + "collection": "test-collection", + "type": "Feature", + "stac_version": "1.0.0", + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "properties": {"datetime": "2023-12-01T12:00:00Z", "new_property": "value"}, + "assets": {}, + "links": [], + } + mock_patch.return_value = mock_response + + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + ) + + client = DatacosmosClient(config=config) + stac_client = STACClient(client) + + update_data = ItemUpdate(properties={"new_property": "value"}) + updated_item = stac_client.update_item("item-1", "test-collection", update_data) + + assert updated_item.id == "item-1" + assert updated_item.properties["new_property"] == "value" + mock_patch.assert_called_once() + mock_check_api_response.assert_called_once() From 52b9c84b476ab863a6474b653b8c6829c3be92a0 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 15:56:42 +0000 Subject: [PATCH 093/135] Apply isort and black --- datacosmos/client.py | 2 +- datacosmos/stac/models/item_update.py | 14 ++++++----- datacosmos/stac/stac_client.py | 24 ++++++++++++------- .../stac/stac_client/test_create_item.py | 1 + .../test_fetch_collection_items.py | 8 ++++--- .../stac/stac_client/test_fetch_item.py | 2 +- .../stac/stac_client/test_search_items.py | 6 +++-- .../stac/stac_client/test_update_item.py | 3 ++- 8 files changed, 37 insertions(+), 23 deletions(-) diff --git a/datacosmos/client.py b/datacosmos/client.py index ca4dc82..d4082a8 100644 --- a/datacosmos/client.py +++ b/datacosmos/client.py @@ -110,7 +110,7 @@ def post(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: def put(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: """Send a PUT request using the authenticated session.""" return self.request("PUT", url, *args, **kwargs) - + def patch(self, url: str, *args: Any, **kwargs: Any) -> requests.Response: """Send a PATCH request using the authenticated session.""" return self.request("PATCH", url, *args, **kwargs) diff --git a/datacosmos/stac/models/item_update.py b/datacosmos/stac/models/item_update.py index 89400e8..7d146fe 100644 --- a/datacosmos/stac/models/item_update.py +++ b/datacosmos/stac/models/item_update.py @@ -1,4 +1,5 @@ -from typing import Optional, Any +from typing import Any, Optional + from pydantic import BaseModel, Field from pystac import Asset, Link from shapely.geometry import mapping @@ -6,17 +7,18 @@ class ItemUpdate(BaseModel): """Model representing a partial update for a STAC item.""" - model_config = { - "arbitrary_types_allowed": True - } + + model_config = {"arbitrary_types_allowed": True} stac_extensions: Optional[list[str]] = None geometry: Optional[dict[str, Any]] = None - bbox: Optional[list[float]] = Field(None, min_items=4, max_items=4) # Must be [minX, minY, maxX, maxY] + bbox: Optional[list[float]] = Field( + None, min_items=4, max_items=4 + ) # Must be [minX, minY, maxX, maxY] properties: Optional[dict[str, Any]] = None assets: Optional[dict[str, Asset]] = None links: Optional[list[Link]] = None def set_geometry(self, geom) -> None: """Convert a shapely geometry to GeoJSON format.""" - self.geometry = mapping(geom) \ No newline at end of file + self.geometry = mapping(geom) diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index ac9ca15..a559f6d 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -6,11 +6,13 @@ from typing import Generator, Optional +from common.sdk.http_response import InvalidRequest, check_api_response from pystac import Item + from datacosmos.client import DatacosmosClient -from datacosmos.stac.models.search_parameters import SearchParameters from datacosmos.stac.models.item_update import ItemUpdate -from common.sdk.http_response import check_api_response, InvalidRequest +from datacosmos.stac.models.search_parameters import SearchParameters + class STACClient: """Client for interacting with the STAC API.""" @@ -56,9 +58,7 @@ def fetch_collection_items( return self.search_items(parameters) - def search_items( - self, parameters: SearchParameters - ) -> Generator[Item, None, None]: + def search_items(self, parameters: SearchParameters) -> Generator[Item, None, None]: """Query the STAC catalog using the POST endpoint with filtering and pagination. Args: @@ -92,7 +92,9 @@ def create_item(self, collection_id: str, item: Item) -> Item: return Item.from_dict(response.json()) - def update_item(self, item_id: str, collection_id: str, update_data: ItemUpdate) -> Item: + def update_item( + self, item_id: str, collection_id: str, update_data: ItemUpdate + ) -> Item: """Partially update an existing STAC item. Args: @@ -112,12 +114,14 @@ def update_item(self, item_id: str, collection_id: str, update_data: ItemUpdate) key: asset.to_dict() for key, asset in update_payload["assets"].items() } if "links" in update_payload: - update_payload["links"] = [link.to_dict() for link in update_payload["links"]] + update_payload["links"] = [ + link.to_dict() for link in update_payload["links"] + ] response = self.client.patch(url, json=update_payload) check_api_response(response) - return Item.from_dict(response.json()) + return Item.from_dict(response.json()) def delete_item(self, item_id: str, collection_id: str) -> None: """Delete a STAC item by its ID. @@ -165,7 +169,9 @@ def _paginate_items(self, url: str, body: dict) -> Generator[Item, None, None]: def _get_next_link(self, data: dict) -> Optional[str]: """Extract the next page link from the response.""" - next_link = next((link for link in data.get("links", []) if link.get("rel") == "next"), None) + next_link = next( + (link for link in data.get("links", []) if link.get("rel") == "next"), None + ) return next_link.get("href", "") if next_link else None def _extract_pagination_token(self, next_href: str) -> Optional[str]: diff --git a/tests/unit/datacosmos/stac/stac_client/test_create_item.py b/tests/unit/datacosmos/stac/stac_client/test_create_item.py index cf34357..1d37cb0 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_create_item.py +++ b/tests/unit/datacosmos/stac/stac_client/test_create_item.py @@ -1,4 +1,5 @@ from unittest.mock import MagicMock, patch + from pystac import Item from config.config import Config diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py index 7b821f2..11b8bc3 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py @@ -9,7 +9,9 @@ @patch("requests_oauthlib.OAuth2Session.fetch_token") @patch("datacosmos.stac.stac_client.check_api_response") @patch.object(STACClient, "search_items") -def test_fetch_collection_items(mock_search_items, mock_check_api_response, mock_fetch_token): +def test_fetch_collection_items( + mock_search_items, mock_check_api_response, mock_fetch_token +): """Test fetching all items in a collection.""" mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} @@ -18,7 +20,7 @@ def test_fetch_collection_items(mock_search_items, mock_check_api_response, mock mock_search_items.return_value = iter([mock_response_1, mock_response_2]) - mock_check_api_response.return_value = None + mock_check_api_response.return_value = None config = Config( authentication=M2MAuthenticationConfig( @@ -38,6 +40,6 @@ def test_fetch_collection_items(mock_search_items, mock_check_api_response, mock assert len(results) == 2 assert results[0].id == "item-1" assert results[1].id == "item-2" - + mock_search_items.assert_called_once() mock_check_api_response.assert_not_called() diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py index 59957d4..35c2cbb 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py +++ b/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py @@ -27,7 +27,7 @@ def test_fetch_item(mock_get, mock_check_api_response, mock_fetch_token): } mock_get.return_value = mock_response - mock_check_api_response.return_value = None + mock_check_api_response.return_value = None config = Config( authentication=M2MAuthenticationConfig( diff --git a/tests/unit/datacosmos/stac/stac_client/test_search_items.py b/tests/unit/datacosmos/stac/stac_client/test_search_items.py index 1f28116..572a2cf 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_search_items.py +++ b/tests/unit/datacosmos/stac/stac_client/test_search_items.py @@ -33,7 +33,7 @@ def test_search_items(mock_post, mock_check_api_response, mock_fetch_token): } mock_post.return_value = mock_response - mock_check_api_response.return_value = None + mock_check_api_response.return_value = None config = Config( authentication=M2MAuthenticationConfig( @@ -54,4 +54,6 @@ def test_search_items(mock_post, mock_check_api_response, mock_fetch_token): assert len(results) == 1 assert results[0].id == "item-1" mock_post.assert_called_once() - mock_check_api_response.assert_called_once_with(mock_response) # Ensure the API check was called + mock_check_api_response.assert_called_once_with( + mock_response + ) # Ensure the API check was called diff --git a/tests/unit/datacosmos/stac/stac_client/test_update_item.py b/tests/unit/datacosmos/stac/stac_client/test_update_item.py index 0e2c4bf..0aeb25d 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_update_item.py +++ b/tests/unit/datacosmos/stac/stac_client/test_update_item.py @@ -1,10 +1,11 @@ from unittest.mock import MagicMock, patch + from pystac import Item -from datacosmos.stac.models.item_update import ItemUpdate from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient +from datacosmos.stac.models.item_update import ItemUpdate from datacosmos.stac.stac_client import STACClient From b48c7695bc3f3b3c9b77095478bc91d30c750060 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 15:58:51 +0000 Subject: [PATCH 094/135] Delete unnecessary requirements.txt file --- requirements.txt | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6a177e6..0000000 --- a/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile pyproject.toml -# -annotated-types==0.7.0 - # via pydantic -pydantic==2.10.6 - # via pydantic-settings -pydantic-core==2.27.2 - # via pydantic -pydantic-settings==2.7.1 - # via datacosmos-sdk (pyproject.toml) -python-dotenv==1.0.1 - # via pydantic-settings -typing-extensions==4.12.2 - # via - # pydantic - # pydantic-core From d30bed11c01a8514a2428642fa8dd7c5d7f6faef Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 16:01:00 +0000 Subject: [PATCH 095/135] fix pydocstyle issues --- datacosmos/stac/models/item_update.py | 2 ++ datacosmos/stac/stac_client.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/datacosmos/stac/models/item_update.py b/datacosmos/stac/models/item_update.py index 7d146fe..d2dfde3 100644 --- a/datacosmos/stac/models/item_update.py +++ b/datacosmos/stac/models/item_update.py @@ -1,3 +1,5 @@ +"""Model representing a partial update for a STAC item.""" + from typing import Any, Optional from pydantic import BaseModel, Field diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index a559f6d..92372ac 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -1,5 +1,4 @@ -""" -STAC Client module for interacting with a STAC (SpatioTemporal Asset Catalog) API. +"""STAC Client module for interacting with a STAC (SpatioTemporal Asset Catalog) API. Provides methods for querying, fetching, creating, updating, and deleting STAC items. """ From a4b197801ffe87c14e7dc90279984f419dba2761 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 16:03:20 +0000 Subject: [PATCH 096/135] Delete unused import --- tests/unit/datacosmos/stac/stac_client/test_update_item.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/datacosmos/stac/stac_client/test_update_item.py b/tests/unit/datacosmos/stac/stac_client/test_update_item.py index 0aeb25d..fa21c40 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_update_item.py +++ b/tests/unit/datacosmos/stac/stac_client/test_update_item.py @@ -1,7 +1,5 @@ from unittest.mock import MagicMock, patch -from pystac import Item - from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig from datacosmos.client import DatacosmosClient From 8e6e821126e459512647b4e37ef8fc140addbb99 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 16:06:13 +0000 Subject: [PATCH 097/135] Add shapely as dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 77212cc..9fa6b9ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,8 @@ dependencies = ["pydantic_settings>=2.7.0", "requests-oauthlib==1.3.1", "pydantic==2.10.6", "pydantic-settings==2.7.1", - "pystac==1.12.1" + "pystac==1.12.1", + "shapely==1.8.0", ] [tool.bandit] From 731599ffb4cd14a6c3867a44f3c6cc8abafbb7a5 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 16:15:37 +0000 Subject: [PATCH 098/135] Change python-common version to the latest one --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9fa6b9ab..1a30511 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = ["pydantic_settings>=2.7.0", - "python-common==0.13.1", + "python-common==1.7.4", "black==22.3.0", "flake8==4.0.1", "pytest==7.2.0", From bdc97305ed5849507d265af34da76565d4123dcc Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 5 Feb 2025 16:25:58 +0000 Subject: [PATCH 099/135] Check if python-common 1.4.0 works --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1a30511..76a6bc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = ["pydantic_settings>=2.7.0", - "python-common==1.7.4", + "python-common==1.4.0", "black==22.3.0", "flake8==4.0.1", "pytest==7.2.0", From 038dff94d696d99e463c875dad9d4b05fc23e528 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sat, 8 Feb 2025 16:30:09 +0000 Subject: [PATCH 100/135] Fix issues in create_item, update_item and delete_item methods; Adapt unit tests to changes in stac item methods; create unit test for ItemUpdate model --- datacosmos/stac/models/item_update.py | 20 +++++++-- datacosmos/stac/stac_client.py | 14 +------ .../stac/models/test_item_update.py | 42 +++++++++++++++++++ .../stac/stac_client/test_create_item.py | 12 ++++-- .../stac/stac_client/test_update_item.py | 16 ++++--- 5 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 tests/unit/datacosmos/stac/models/test_item_update.py diff --git a/datacosmos/stac/models/item_update.py b/datacosmos/stac/models/item_update.py index d2dfde3..1af4ac4 100644 --- a/datacosmos/stac/models/item_update.py +++ b/datacosmos/stac/models/item_update.py @@ -1,8 +1,6 @@ -"""Model representing a partial update for a STAC item.""" - from typing import Any, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator from pystac import Asset, Link from shapely.geometry import mapping @@ -24,3 +22,19 @@ class ItemUpdate(BaseModel): def set_geometry(self, geom) -> None: """Convert a shapely geometry to GeoJSON format.""" self.geometry = mapping(geom) + + @model_validator(mode="before") + def validate_datetime_fields(cls, values): + """Ensure at least one of 'datetime' or 'start_datetime'/'end_datetime' exists.""" + properties = values.get("properties", {}) + has_datetime = "datetime" in properties and properties["datetime"] is not None + has_start_end = ( + "start_datetime" in properties and properties["start_datetime"] is not None + ) and ("end_datetime" in properties and properties["end_datetime"] is not None) + + if not has_datetime and not has_start_end: + raise ValueError( + "Either 'datetime' or both 'start_datetime' and 'end_datetime' must be provided." + ) + + return values diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index 92372ac..2d41ad9 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -70,16 +70,13 @@ def search_items(self, parameters: SearchParameters) -> Generator[Item, None, No body = parameters.model_dump(by_alias=True, exclude_none=True) return self._paginate_items(url, body) - def create_item(self, collection_id: str, item: Item) -> Item: + def create_item(self, collection_id: str, item: Item) -> None: """Create a new STAC item in a specified collection. Args: collection_id (str): The ID of the collection where the item will be created. item (Item): The STAC Item to be created. - Returns: - Item: The created STAC Item. - Raises: RequestError: If the API returns an error response. """ @@ -89,20 +86,15 @@ def create_item(self, collection_id: str, item: Item) -> Item: response = self.client.post(url, json=item_json) check_api_response(response) - return Item.from_dict(response.json()) - def update_item( self, item_id: str, collection_id: str, update_data: ItemUpdate - ) -> Item: + ) -> None: """Partially update an existing STAC item. Args: item_id (str): The ID of the item to update. collection_id (str): The ID of the collection containing the item. update_data (ItemUpdate): The structured update payload. - - Returns: - Item: The updated STAC item. """ url = self.base_url.with_suffix(f"/collections/{collection_id}/items/{item_id}") @@ -120,8 +112,6 @@ def update_item( response = self.client.patch(url, json=update_payload) check_api_response(response) - return Item.from_dict(response.json()) - def delete_item(self, item_id: str, collection_id: str) -> None: """Delete a STAC item by its ID. diff --git a/tests/unit/datacosmos/stac/models/test_item_update.py b/tests/unit/datacosmos/stac/models/test_item_update.py new file mode 100644 index 0000000..d1ff225 --- /dev/null +++ b/tests/unit/datacosmos/stac/models/test_item_update.py @@ -0,0 +1,42 @@ +import pytest + +from datacosmos.stac.models.item_update import ItemUpdate + + +class TestItemUpdate: + """Unit tests for the ItemUpdate model validation.""" + + def test_valid_item_update_with_datetime(self): + """Test that ItemUpdate passes validation with a single datetime field.""" + update_data = ItemUpdate( + properties={"new_property": "value", "datetime": "2023-12-01T12:00:00Z"} + ) + assert update_data.properties["datetime"] == "2023-12-01T12:00:00Z" + + def test_valid_item_update_with_start_and_end_datetime(self): + """Test that ItemUpdate passes validation with start_datetime and end_datetime.""" + update_data = ItemUpdate( + properties={ + "new_property": "value", + "start_datetime": "2023-12-01T12:00:00Z", + "end_datetime": "2023-12-01T12:30:00Z", + } + ) + assert update_data.properties["start_datetime"] == "2023-12-01T12:00:00Z" + assert update_data.properties["end_datetime"] == "2023-12-01T12:30:00Z" + + def test_invalid_item_update_missing_datetime(self): + """Test that ItemUpdate fails validation when datetime is missing.""" + with pytest.raises( + ValueError, + match="Either 'datetime' or both 'start_datetime' and 'end_datetime' must be provided.", + ): + ItemUpdate(properties={"new_property": "value"}) + + def test_invalid_item_update_empty_properties(self): + """Test that ItemUpdate fails validation when properties are empty.""" + with pytest.raises( + ValueError, + match="Either 'datetime' or both 'start_datetime' and 'end_datetime' must be provided.", + ): + ItemUpdate(properties={}) diff --git a/tests/unit/datacosmos/stac/stac_client/test_create_item.py b/tests/unit/datacosmos/stac/stac_client/test_create_item.py index 1d37cb0..16ac088 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_create_item.py +++ b/tests/unit/datacosmos/stac/stac_client/test_create_item.py @@ -37,14 +37,18 @@ def test_create_item(mock_check_api_response, mock_post, mock_fetch_token): audience="https://mock.audience", ) ) - client = DatacosmosClient(config=config) stac_client = STACClient(client) item = Item.from_dict(mock_response.json()) - created_item = stac_client.create_item("test-collection", item) - assert created_item.id == "item-1" - assert created_item.properties["datetime"] == "2023-12-01T12:00:00Z" + stac_client.create_item("test-collection", item) + mock_post.assert_called_once() + mock_check_api_response.assert_called_once() + + mock_post.assert_called_with( + stac_client.base_url.with_suffix("/collections/test-collection/items"), + json=item.to_dict(), + ) diff --git a/tests/unit/datacosmos/stac/stac_client/test_update_item.py b/tests/unit/datacosmos/stac/stac_client/test_update_item.py index fa21c40..de466c1 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_update_item.py +++ b/tests/unit/datacosmos/stac/stac_client/test_update_item.py @@ -36,14 +36,20 @@ def test_update_item(mock_check_api_response, mock_patch, mock_fetch_token): audience="https://mock.audience", ) ) - client = DatacosmosClient(config=config) stac_client = STACClient(client) - update_data = ItemUpdate(properties={"new_property": "value"}) - updated_item = stac_client.update_item("item-1", "test-collection", update_data) + update_data = ItemUpdate( + properties={"new_property": "value", "datetime": "2023-12-01T12:00:00Z"} + ) + + stac_client.update_item("item-1", "test-collection", update_data) - assert updated_item.id == "item-1" - assert updated_item.properties["new_property"] == "value" mock_patch.assert_called_once() + mock_check_api_response.assert_called_once() + + mock_patch.assert_called_with( + stac_client.base_url.with_suffix("/collections/test-collection/items/item-1"), + json=update_data.model_dump(by_alias=True, exclude_none=True), + ) From fdc1523a14c98bd4e203e1d690e863e05feb7ce2 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sat, 8 Feb 2025 16:35:33 +0000 Subject: [PATCH 101/135] Refactor item_update.py to reduce cognitive complexity --- datacosmos/stac/models/item_update.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/datacosmos/stac/models/item_update.py b/datacosmos/stac/models/item_update.py index 1af4ac4..338df78 100644 --- a/datacosmos/stac/models/item_update.py +++ b/datacosmos/stac/models/item_update.py @@ -1,3 +1,5 @@ +"""Model representing a partial update for a STAC item.""" + from typing import Any, Optional from pydantic import BaseModel, Field, model_validator @@ -23,16 +25,27 @@ def set_geometry(self, geom) -> None: """Convert a shapely geometry to GeoJSON format.""" self.geometry = mapping(geom) + @staticmethod + def has_valid_datetime(properties: dict[str, Any]) -> bool: + """Check if 'datetime' is present and not None.""" + return properties.get("datetime") is not None + + @staticmethod + def has_valid_datetime_range(properties: dict[str, Any]) -> bool: + """Check if both 'start_datetime' and 'end_datetime' are present and not None.""" + return all( + properties.get(key) is not None + for key in ["start_datetime", "end_datetime"] + ) + @model_validator(mode="before") def validate_datetime_fields(cls, values): """Ensure at least one of 'datetime' or 'start_datetime'/'end_datetime' exists.""" properties = values.get("properties", {}) - has_datetime = "datetime" in properties and properties["datetime"] is not None - has_start_end = ( - "start_datetime" in properties and properties["start_datetime"] is not None - ) and ("end_datetime" in properties and properties["end_datetime"] is not None) - if not has_datetime and not has_start_end: + if not cls.has_valid_datetime(properties) and not cls.has_valid_datetime_range( + properties + ): raise ValueError( "Either 'datetime' or both 'start_datetime' and 'end_datetime' must be provided." ) From a25023e3c7d685418f59c25af17b2712d41120b7 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 16:39:47 +0000 Subject: [PATCH 102/135] Replace poetry by uv --- .github/workflows/main.yaml | 112 ++++++++++++++++++------------------ pyproject.toml | 13 +++-- 2 files changed, 63 insertions(+), 62 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d2983ed..799c4a2 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -14,75 +14,75 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: set up python + - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.10" - - name: set up poetry - run: curl -sSL https://install.python-poetry.org | python3 - - - name: configure gitlab auth - run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - - name: install dependencies - run: poetry install --no-interaction - - name: run isort before black + - name: Install uv + run: pip install uv + - name: Set up uv environment run: | - poetry run isort . --check-only # Run isort first - poetry run black . --check # Then black - poetry run flake8 . + uv venv + uv pip install -r pyproject.toml + - name: Run isort before black + run: | + uv venv exec isort . --check-only + uv venv exec black . --check + uv venv exec flake8 . bandit: name: bandit runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: set up python + - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.10" - - name: set up poetry - run: curl -sSL https://install.python-poetry.org | python3 - - - name: configure gitlab auth - run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - - name: install dependencies - run: poetry install --no-interaction - - name: bandit - run: poetry run bandit -r -c pyproject.toml . --skip B105,B106,B101 + - name: Install uv + run: pip install uv + - name: Set up uv environment + run: | + uv venv + uv pip install -r pyproject.toml + - name: Run Bandit security checks + run: uv venv exec bandit -r -c pyproject.toml . --skip B105,B106,B101 cognitive: name: cognitive runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: set up python + - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.10" - - name: set up poetry - run: curl -sSL https://install.python-poetry.org | python3 - - - name: configure gitlab auth - run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - - name: install dependencies - run: poetry install --no-interaction - - name: cognitive - run: poetry run flake8 . --max-cognitive-complexity=5 --ignore=E501 + - name: Install uv + run: pip install uv + - name: Set up uv environment + run: | + uv venv + uv pip install -r pyproject.toml + - name: Run cognitive complexity check + run: uv venv exec flake8 . --max-cognitive-complexity=5 --ignore=E501 pydocstyle: name: pydocstyle runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: set up python + - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.10" - - name: set up poetry - run: curl -sSL https://install.python-poetry.org | python3 - - - name: configure gitlab auth - run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - - name: install dependencies - run: poetry install --no-interaction - - name: pydocstyle - run: poetry run pydocstyle . + - name: Install uv + run: pip install uv + - name: Set up uv environment + run: | + uv venv + uv pip install -r pyproject.toml + - name: Run pydocstyle + run: uv venv exec pydocstyle . test: name: test @@ -90,18 +90,18 @@ jobs: needs: [bandit, cognitive, lint, pydocstyle] steps: - uses: actions/checkout@v3 - - name: set up python + - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.10" - - name: set up poetry - run: curl -sSL https://install.python-poetry.org | python3 - - - name: configure gitlab auth - run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - - name: install dependencies - run: poetry install --no-interaction - - name: test - run: poetry run pytest + - name: Install uv + run: pip install uv + - name: Set up uv environment + run: | + uv venv + uv pip install -r pyproject.toml + - name: Run tests + run: uv venv exec pytest release: name: tag, changelog, release, publish @@ -110,19 +110,19 @@ jobs: if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - - name: set up poetry - run: curl -sSL https://install.python-poetry.org | python3 - - - name: configure gitlab auth - run: poetry config http-basic.gitlab __token__ ${{ secrets.GITLAB_PYPI_TOKEN }} - - name: install dependencies - run: poetry install --no-interaction - - name: version + - name: Install uv + run: pip install uv + - name: Set up uv environment + run: | + uv venv + uv pip install -r pyproject.toml + - name: Determine new version uses: paulhatch/semantic-version@v5.0.0 id: version with: major_pattern: "(feat!)" minor_pattern: "(feat)" - - name: create changelog text + - name: Create changelog text id: changelog uses: loopwerk/tag-changelog@v1 with: @@ -136,7 +136,7 @@ jobs: tag_name: ${{ steps.version.outputs.version_tag }} release_name: Release ${{ steps.version.outputs.version_tag }} body: ${{ steps.changelog.outputs.changes }} - - name: create changelog pull request + - name: Create changelog pull request uses: peter-evans/create-pull-request@v2 with: commit-message: "Release ${{ steps.version.outputs.version_tag }} [skip ci]" diff --git a/pyproject.toml b/pyproject.toml index 76a6bc2..2df5355 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "datacosmos" -version = "0.0.1" +version = "0.0.1" authors = [ { name="Open Cosmos", email="support@open-cosmos.com" }, ] @@ -14,7 +14,8 @@ classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] -dependencies = ["pydantic_settings>=2.7.0", +dependencies = [ + "pydantic_settings>=2.7.0", "python-common==1.4.0", "black==22.3.0", "flake8==4.0.1", @@ -27,7 +28,6 @@ dependencies = ["pydantic_settings>=2.7.0", "oauthlib==3.2.0", "requests-oauthlib==1.3.1", "pydantic==2.10.6", - "pydantic-settings==2.7.1", "pystac==1.12.1", "shapely==1.8.0", ] @@ -45,8 +45,9 @@ force_grid_wrap = 0 use_parentheses = true line_length = 88 -# Add GitLab private registry as a source -# useless comment -[[tool.poetry.source]] +[tool.uv.sources] +python-common = { index = "gitlab" } + +[[tool.uv.index]] name = "gitlab" url = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" \ No newline at end of file From 0014f83bb53ca42295b3ae7bb0b8f2d4ad4e5ef9 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 16:43:00 +0000 Subject: [PATCH 103/135] Add authentication token to the pipeline --- .github/workflows/main.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 799c4a2..346e07d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -20,6 +20,8 @@ jobs: python-version: "3.10" - name: Install uv run: pip install uv + - name: Configure GitLab authentication for uv + run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment run: | uv venv @@ -41,6 +43,8 @@ jobs: python-version: "3.10" - name: Install uv run: pip install uv + - name: Configure GitLab authentication for uv + run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment run: | uv venv @@ -59,6 +63,8 @@ jobs: python-version: "3.10" - name: Install uv run: pip install uv + - name: Configure GitLab authentication for uv + run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment run: | uv venv @@ -77,6 +83,8 @@ jobs: python-version: "3.10" - name: Install uv run: pip install uv + - name: Configure GitLab authentication for uv + run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment run: | uv venv @@ -96,6 +104,8 @@ jobs: python-version: "3.10" - name: Install uv run: pip install uv + - name: Configure GitLab authentication for uv + run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment run: | uv venv @@ -112,6 +122,8 @@ jobs: - uses: actions/checkout@v3 - name: Install uv run: pip install uv + - name: Configure GitLab authentication for uv + run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment run: | uv venv From 77eecb261c71c7b8f2d90a68cfc1c1ded9acdf2b Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 16:45:59 +0000 Subject: [PATCH 104/135] Add authentication token to the pipeline through index-url --- .github/workflows/main.yaml | 52 ++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 346e07d..802f51f 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -3,7 +3,7 @@ name: main on: push: branches: - - "**" + - "**" pull_request: branches: - "**" @@ -20,12 +20,11 @@ jobs: python-version: "3.10" - name: Install uv run: pip install uv - - name: Configure GitLab authentication for uv - run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment + run: uv venv + - name: Install dependencies with GitLab authentication run: | - uv venv - uv pip install -r pyproject.toml + uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Run isort before black run: | uv venv exec isort . --check-only @@ -43,13 +42,12 @@ jobs: python-version: "3.10" - name: Install uv run: pip install uv - - name: Configure GitLab authentication for uv - run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment + run: uv venv + - name: Install dependencies with GitLab authentication run: | - uv venv - uv pip install -r pyproject.toml - - name: Run Bandit security checks + uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml + - name: Run bandit run: uv venv exec bandit -r -c pyproject.toml . --skip B105,B106,B101 cognitive: @@ -63,13 +61,12 @@ jobs: python-version: "3.10" - name: Install uv run: pip install uv - - name: Configure GitLab authentication for uv - run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment + run: uv venv + - name: Install dependencies with GitLab authentication run: | - uv venv - uv pip install -r pyproject.toml - - name: Run cognitive complexity check + uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml + - name: Run cognitive complexity checks run: uv venv exec flake8 . --max-cognitive-complexity=5 --ignore=E501 pydocstyle: @@ -83,12 +80,11 @@ jobs: python-version: "3.10" - name: Install uv run: pip install uv - - name: Configure GitLab authentication for uv - run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment + run: uv venv + - name: Install dependencies with GitLab authentication run: | - uv venv - uv pip install -r pyproject.toml + uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Run pydocstyle run: uv venv exec pydocstyle . @@ -104,12 +100,11 @@ jobs: python-version: "3.10" - name: Install uv run: pip install uv - - name: Configure GitLab authentication for uv - run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment + run: uv venv + - name: Install dependencies with GitLab authentication run: | - uv venv - uv pip install -r pyproject.toml + uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Run tests run: uv venv exec pytest @@ -122,13 +117,12 @@ jobs: - uses: actions/checkout@v3 - name: Install uv run: pip install uv - - name: Configure GitLab authentication for uv - run: uv pip config set global.index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Set up uv environment + run: uv venv + - name: Install dependencies with GitLab authentication run: | - uv venv - uv pip install -r pyproject.toml - - name: Determine new version + uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml + - name: Determine version uses: paulhatch/semantic-version@v5.0.0 id: version with: @@ -140,7 +134,7 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} exclude_types: other,doc,chore - - name: Create release + - name: Create GitHub Release uses: actions/create-release@latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From c9ca0ec31befd0e81d091fd8cf11d15b25efcea5 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 16:55:40 +0000 Subject: [PATCH 105/135] Attempt to fix the way linters run --- .github/workflows/main.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 802f51f..33028de 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -25,11 +25,11 @@ jobs: - name: Install dependencies with GitLab authentication run: | uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - - name: Run isort before black + - name: Run linters run: | - uv venv exec isort . --check-only - uv venv exec black . --check - uv venv exec flake8 . + isort . --check-only + black . --check + flake8 . bandit: name: bandit @@ -48,7 +48,7 @@ jobs: run: | uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Run bandit - run: uv venv exec bandit -r -c pyproject.toml . --skip B105,B106,B101 + run: bandit -r -c pyproject.toml . --skip B105,B106,B101 cognitive: name: cognitive @@ -66,8 +66,8 @@ jobs: - name: Install dependencies with GitLab authentication run: | uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - - name: Run cognitive complexity checks - run: uv venv exec flake8 . --max-cognitive-complexity=5 --ignore=E501 + - name: Run cognitive complexity check + run: flake8 . --max-cognitive-complexity=5 --ignore=E501 pydocstyle: name: pydocstyle @@ -86,7 +86,7 @@ jobs: run: | uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Run pydocstyle - run: uv venv exec pydocstyle . + run: pydocstyle . test: name: test @@ -106,7 +106,7 @@ jobs: run: | uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Run tests - run: uv venv exec pytest + run: pytest release: name: tag, changelog, release, publish From c424d87718c25b7497bbd0c84fe8c3dea69547ca Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 16:59:31 +0000 Subject: [PATCH 106/135] Activave virtual env before running any of the linters --- .github/workflows/main.yaml | 93 +++++++++++++------------------------ 1 file changed, 33 insertions(+), 60 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 33028de..d44b206 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -3,7 +3,7 @@ name: main on: push: branches: - - "**" + - "**" pull_request: branches: - "**" @@ -14,19 +14,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Install uv - run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab authentication - run: | - uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - - name: Run linters + - name: Install dependencies + run: uv pip install -r pyproject.toml + - name: Activate virtual environment & run linters run: | + source .venv/bin/activate isort . --check-only black . --check flake8 . @@ -36,57 +30,42 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Install uv - run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab authentication + - name: Install dependencies + run: uv pip install -r pyproject.toml + - name: Activate virtual environment & run bandit run: | - uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - - name: Run bandit - run: bandit -r -c pyproject.toml . --skip B105,B106,B101 + source .venv/bin/activate + bandit -r -c pyproject.toml . --skip B105,B106,B101 cognitive: name: cognitive runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Install uv - run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab authentication + - name: Install dependencies + run: uv pip install -r pyproject.toml + - name: Activate virtual environment & run flake8 run: | - uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - - name: Run cognitive complexity check - run: flake8 . --max-cognitive-complexity=5 --ignore=E501 + source .venv/bin/activate + flake8 . --max-cognitive-complexity=5 --ignore=E501 pydocstyle: name: pydocstyle runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Install uv - run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab authentication + - name: Install dependencies + run: uv pip install -r pyproject.toml + - name: Activate virtual environment & run pydocstyle run: | - uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - - name: Run pydocstyle - run: pydocstyle . + source .venv/bin/activate + pydocstyle . test: name: test @@ -94,19 +73,14 @@ jobs: needs: [bandit, cognitive, lint, pydocstyle] steps: - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Install uv - run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab authentication + - name: Install dependencies + run: uv pip install -r pyproject.toml + - name: Activate virtual environment & run tests run: | - uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - - name: Run tests - run: pytest + source .venv/bin/activate + pytest release: name: tag, changelog, release, publish @@ -115,26 +89,25 @@ jobs: if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - - name: Install uv - run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab authentication - run: | - uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - - name: Determine version + - name: Install dependencies + run: uv pip install -r pyproject.toml + - name: Activate virtual environment + run: source .venv/bin/activate + - name: version uses: paulhatch/semantic-version@v5.0.0 id: version with: major_pattern: "(feat!)" minor_pattern: "(feat)" - - name: Create changelog text + - name: create changelog text id: changelog uses: loopwerk/tag-changelog@v1 with: token: ${{ secrets.GITHUB_TOKEN }} exclude_types: other,doc,chore - - name: Create GitHub Release + - name: Create release uses: actions/create-release@latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -142,7 +115,7 @@ jobs: tag_name: ${{ steps.version.outputs.version_tag }} release_name: Release ${{ steps.version.outputs.version_tag }} body: ${{ steps.changelog.outputs.changes }} - - name: Create changelog pull request + - name: create changelog pull request uses: peter-evans/create-pull-request@v2 with: commit-message: "Release ${{ steps.version.outputs.version_tag }} [skip ci]" From 1c5bba2822380dc009c8e2d9076b8b4dedf0a125 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:02:37 +0000 Subject: [PATCH 107/135] Installing uv before anything else --- .github/workflows/main.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d44b206..6f91f32 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -14,6 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - name: Install dependencies @@ -30,6 +32,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - name: Install dependencies @@ -44,6 +48,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - name: Install dependencies @@ -58,6 +64,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - name: Install dependencies @@ -73,6 +81,8 @@ jobs: needs: [bandit, cognitive, lint, pydocstyle] steps: - uses: actions/checkout@v3 + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - name: Install dependencies @@ -89,6 +99,8 @@ jobs: if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - name: Install dependencies From 4006e9d99127d2bf108e9f3e53d2d925cd6481e9 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:06:08 +0000 Subject: [PATCH 108/135] Adding back gitlab token authentication --- .github/workflows/main.yaml | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 6f91f32..2dd84df 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -18,8 +18,8 @@ jobs: run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - - name: Install dependencies - run: uv pip install -r pyproject.toml + - name: Install dependencies with GitLab token + run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Activate virtual environment & run linters run: | source .venv/bin/activate @@ -36,8 +36,8 @@ jobs: run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - - name: Install dependencies - run: uv pip install -r pyproject.toml + - name: Install dependencies with GitLab token + run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Activate virtual environment & run bandit run: | source .venv/bin/activate @@ -52,8 +52,8 @@ jobs: run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - - name: Install dependencies - run: uv pip install -r pyproject.toml + - name: Install dependencies with GitLab token + run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Activate virtual environment & run flake8 run: | source .venv/bin/activate @@ -68,8 +68,8 @@ jobs: run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - - name: Install dependencies - run: uv pip install -r pyproject.toml + - name: Install dependencies with GitLab token + run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Activate virtual environment & run pydocstyle run: | source .venv/bin/activate @@ -85,8 +85,8 @@ jobs: run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - - name: Install dependencies - run: uv pip install -r pyproject.toml + - name: Install dependencies with GitLab token + run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" - name: Activate virtual environment & run tests run: | source .venv/bin/activate @@ -103,17 +103,15 @@ jobs: run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Set up uv environment run: uv venv - - name: Install dependencies - run: uv pip install -r pyproject.toml - - name: Activate virtual environment - run: source .venv/bin/activate - - name: version + - name: Install dependencies with GitLab token + run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" + - name: Version uses: paulhatch/semantic-version@v5.0.0 id: version with: major_pattern: "(feat!)" minor_pattern: "(feat)" - - name: create changelog text + - name: Create changelog text id: changelog uses: loopwerk/tag-changelog@v1 with: @@ -127,7 +125,7 @@ jobs: tag_name: ${{ steps.version.outputs.version_tag }} release_name: Release ${{ steps.version.outputs.version_tag }} body: ${{ steps.changelog.outputs.changes }} - - name: create changelog pull request + - name: Create changelog pull request uses: peter-evans/create-pull-request@v2 with: commit-message: "Release ${{ steps.version.outputs.version_tag }} [skip ci]" From f86bde3d183414deb1480d454c66a4bb721d5451 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:09:58 +0000 Subject: [PATCH 109/135] Attempt to fix pipeline --- .github/workflows/main.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 2dd84df..560a3d4 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -15,11 +15,11 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh + run: pip install uv - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Activate virtual environment & run linters run: | source .venv/bin/activate @@ -33,11 +33,11 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh + run: pip install uv - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Activate virtual environment & run bandit run: | source .venv/bin/activate @@ -49,11 +49,11 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh + run: pip install uv - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Activate virtual environment & run flake8 run: | source .venv/bin/activate @@ -65,11 +65,11 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh + run: pip install uv - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Activate virtual environment & run pydocstyle run: | source .venv/bin/activate @@ -82,11 +82,11 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh + run: pip install uv - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Activate virtual environment & run tests run: | source .venv/bin/activate @@ -100,11 +100,11 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh + run: pip install uv - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install -r pyproject.toml --index-url "https://${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Version uses: paulhatch/semantic-version@v5.0.0 id: version From 0ffc6cbcbd5ef2bd2f9cc7baee6ca2ab11b85e78 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:13:52 +0000 Subject: [PATCH 110/135] Attempt to fix libgeos-dev issue --- .github/workflows/main.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 560a3d4..834e17b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -14,6 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -32,6 +34,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -48,6 +52,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -64,6 +70,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -81,6 +89,8 @@ jobs: needs: [bandit, cognitive, lint, pydocstyle] steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -99,6 +109,8 @@ jobs: if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -138,4 +150,4 @@ jobs: ## Included Pull Requests - ${{ steps.changelog.outputs.changes }} + ${{ steps.changelog.outputs.changes }} \ No newline at end of file From 68f6b6a7a02e145292cc32781140ef03109004f6 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:22:24 +0000 Subject: [PATCH 111/135] Attempt to fix flake8 issue --- .github/workflows/main.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 834e17b..640eecb 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -27,7 +27,6 @@ jobs: source .venv/bin/activate isort . --check-only black . --check - flake8 . bandit: name: bandit @@ -60,6 +59,10 @@ jobs: run: uv venv - name: Install dependencies with GitLab token run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml + - name: Upgrade flake8 and importlib-metadata + run: | + source .venv/bin/activate + pip install --upgrade flake8 importlib-metadata - name: Activate virtual environment & run flake8 run: | source .venv/bin/activate From 941b6de8a8faa8cf326d4ffa09ca7e3b6b92143a Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:25:19 +0000 Subject: [PATCH 112/135] Update setuptools --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 640eecb..c303e6c 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -62,7 +62,7 @@ jobs: - name: Upgrade flake8 and importlib-metadata run: | source .venv/bin/activate - pip install --upgrade flake8 importlib-metadata + pip install --upgrade flake8 importlib-metadata setuptools - name: Activate virtual environment & run flake8 run: | source .venv/bin/activate From f3f7dfa0817b4c6a1c81be104c4b6ce05f7f52e8 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:28:22 +0000 Subject: [PATCH 113/135] Downgrade importlib-metadata version --- .github/workflows/main.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index c303e6c..25eb3c2 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -63,6 +63,10 @@ jobs: run: | source .venv/bin/activate pip install --upgrade flake8 importlib-metadata setuptools + - name: Downgrade importlib-metadata to compatible version + run: | + source .venv/bin/activate + pip install "importlib-metadata<5.0.0" - name: Activate virtual environment & run flake8 run: | source .venv/bin/activate From e320657665eccd5029e1b530f554dd04ef413998 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:31:09 +0000 Subject: [PATCH 114/135] Another attempt --- .github/workflows/main.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 25eb3c2..2effd21 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -59,14 +59,10 @@ jobs: run: uv venv - name: Install dependencies with GitLab token run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - - name: Upgrade flake8 and importlib-metadata - run: | - source .venv/bin/activate - pip install --upgrade flake8 importlib-metadata setuptools - name: Downgrade importlib-metadata to compatible version run: | source .venv/bin/activate - pip install "importlib-metadata<5.0.0" + pip install importlib-metadata==4.8.3 - name: Activate virtual environment & run flake8 run: | source .venv/bin/activate From 2b4edab600895ce9bba493fbfb00fcb420122c72 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:34:22 +0000 Subject: [PATCH 115/135] Another attempt to solve cognitive issue --- .github/workflows/main.yaml | 4 ---- pyproject.toml | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 2effd21..b45ba24 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -59,10 +59,6 @@ jobs: run: uv venv - name: Install dependencies with GitLab token run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - - name: Downgrade importlib-metadata to compatible version - run: | - source .venv/bin/activate - pip install importlib-metadata==4.8.3 - name: Activate virtual environment & run flake8 run: | source .venv/bin/activate diff --git a/pyproject.toml b/pyproject.toml index 2df5355..7ec5a87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "pydantic==2.10.6", "pystac==1.12.1", "shapely==1.8.0", + "importlib-metadata==4.8.3" ] [tool.bandit] From d36b9ac6c8d35b4faabe9174f31b828af1f347bd Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:40:14 +0000 Subject: [PATCH 116/135] Install importlib-metadata version 4.13.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7ec5a87..adbcbe0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "pydantic==2.10.6", "pystac==1.12.1", "shapely==1.8.0", - "importlib-metadata==4.8.3" + "importlib-metadata==4.13.0" ] [tool.bandit] From a8927d3e72418881d0eaa2470dd8ac7b134c83d2 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:46:26 +0000 Subject: [PATCH 117/135] Try to explicitly install importlib-metadata version 4.13.0 in main.yaml --- .github/workflows/main.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index b45ba24..22a8b5e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -59,6 +59,11 @@ jobs: run: uv venv - name: Install dependencies with GitLab token run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml + - name: Ensure correct importlib-metadata version + run: | + source .venv/bin/activate + pip uninstall -y importlib-metadata + pip install "importlib-metadata==4.13.0" - name: Activate virtual environment & run flake8 run: | source .venv/bin/activate From 89e8a60acdcfea216b86ef0f3493430ec3574144 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:48:53 +0000 Subject: [PATCH 118/135] force instalation of version 4.13.0 --- .github/workflows/main.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 22a8b5e..720777b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -62,8 +62,8 @@ jobs: - name: Ensure correct importlib-metadata version run: | source .venv/bin/activate - pip uninstall -y importlib-metadata - pip install "importlib-metadata==4.13.0" + pip install --force-reinstall "importlib-metadata==4.13.0" + pip freeze | grep importlib-metadata # Verify installed version - name: Activate virtual environment & run flake8 run: | source .venv/bin/activate From fbd42e067002610a8ed10065f1e72cd76db0c9e3 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Sun, 9 Feb 2025 17:50:27 +0000 Subject: [PATCH 119/135] Remove source activate from instalation --- .github/workflows/main.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 720777b..eb65e58 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -61,7 +61,6 @@ jobs: run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - name: Ensure correct importlib-metadata version run: | - source .venv/bin/activate pip install --force-reinstall "importlib-metadata==4.13.0" pip freeze | grep importlib-metadata # Verify installed version - name: Activate virtual environment & run flake8 From 1574c4cb7a0470c9d9dc1553b4cd5f4480fe51c5 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 10 Feb 2025 09:44:55 +0000 Subject: [PATCH 120/135] Replace flake8 with ruff; make git ignore uv.lock --- .github/workflows/main.yaml | 10 +++------- .gitignore | 2 ++ pyproject.toml | 6 ++---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index eb65e58..45f2df5 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -59,14 +59,10 @@ jobs: run: uv venv - name: Install dependencies with GitLab token run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml - - name: Ensure correct importlib-metadata version - run: | - pip install --force-reinstall "importlib-metadata==4.13.0" - pip freeze | grep importlib-metadata # Verify installed version - - name: Activate virtual environment & run flake8 + - name: Activate virtual environment & run ruff run: | source .venv/bin/activate - flake8 . --max-cognitive-complexity=5 --ignore=E501 + ruff check . --select C901 pydocstyle: name: pydocstyle @@ -153,4 +149,4 @@ jobs: ## Included Pull Requests - ${{ steps.changelog.outputs.changes }} \ No newline at end of file + ${{ steps.changelog.outputs.changes }} diff --git a/.gitignore b/.gitignore index 534e783..a91ebd1 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,5 @@ config/config.yaml # Ignore .vscode .vscode/ + +uv.lock diff --git a/pyproject.toml b/pyproject.toml index adbcbe0..2c73ee4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,19 +18,17 @@ dependencies = [ "pydantic_settings>=2.7.0", "python-common==1.4.0", "black==22.3.0", - "flake8==4.0.1", + "ruff==0.9.5", "pytest==7.2.0", "bandit[toml]==1.7.4", "isort==5.11.4", "pydocstyle==6.1.1", - "flake8-cognitive-complexity==0.1.0", "requests==2.31.0", "oauthlib==3.2.0", "requests-oauthlib==1.3.1", "pydantic==2.10.6", "pystac==1.12.1", - "shapely==1.8.0", - "importlib-metadata==4.13.0" + "shapely==1.8.0" ] [tool.bandit] From 71c7f3a4ac19727ec44072055dfb313571fd2176 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 10 Feb 2025 09:52:03 +0000 Subject: [PATCH 121/135] Exclude .venv dir from bandit --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 2c73ee4..fa937b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ ] [tool.bandit] +exclude_dirs = [".venv"] [tool.pydocstyle] convention = "google" From 4fa2706604d7775ba0cc1b12b627d0339eecdd66 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 10 Feb 2025 11:29:10 +0000 Subject: [PATCH 122/135] Update dependencies in pyproject.toml to only consider those dependencies needed for run time; Putting the other dependencies as optional in a dev section --- .github/workflows/main.yaml | 12 ++++++------ pyproject.toml | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 45f2df5..9680b67 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -21,7 +21,7 @@ jobs: - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" .[dev] - name: Activate virtual environment & run linters run: | source .venv/bin/activate @@ -40,7 +40,7 @@ jobs: - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" .[dev] - name: Activate virtual environment & run bandit run: | source .venv/bin/activate @@ -58,7 +58,7 @@ jobs: - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" .[dev] - name: Activate virtual environment & run ruff run: | source .venv/bin/activate @@ -76,7 +76,7 @@ jobs: - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" .[dev] - name: Activate virtual environment & run pydocstyle run: | source .venv/bin/activate @@ -95,7 +95,7 @@ jobs: - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" .[dev] - name: Activate virtual environment & run tests run: | source .venv/bin/activate @@ -115,7 +115,7 @@ jobs: - name: Set up uv environment run: uv venv - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" -r pyproject.toml + run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" . - name: Version uses: paulhatch/semantic-version@v5.0.0 id: version diff --git a/pyproject.toml b/pyproject.toml index fa937b8..e053441 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,12 +17,6 @@ classifiers = [ dependencies = [ "pydantic_settings>=2.7.0", "python-common==1.4.0", - "black==22.3.0", - "ruff==0.9.5", - "pytest==7.2.0", - "bandit[toml]==1.7.4", - "isort==5.11.4", - "pydocstyle==6.1.1", "requests==2.31.0", "oauthlib==3.2.0", "requests-oauthlib==1.3.1", @@ -31,6 +25,19 @@ dependencies = [ "shapely==1.8.0" ] +[project.optional-dependencies] +dev = [ + "black==22.3.0", + "ruff==0.9.5", + "pytest==7.2.0", + "bandit[toml]==1.7.4", + "isort==5.11.4", + "pydocstyle==6.1.1" +] + +[tool.setuptools] +packages = ["datacosmos"] + [tool.bandit] exclude_dirs = [".venv"] From 161445179c3727541637a23a4d6db66b1434bdf1 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 10 Feb 2025 14:00:03 +0000 Subject: [PATCH 123/135] Add uv.lock --- .gitignore | 1 - uv.lock | 974 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 974 insertions(+), 1 deletion(-) create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index a91ebd1..411066e 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,3 @@ config/config.yaml # Ignore .vscode .vscode/ -uv.lock diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..c1b6715 --- /dev/null +++ b/uv.lock @@ -0,0 +1,974 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "attrs" +version = "25.1.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, +] + +[[package]] +name = "bandit" +version = "1.7.4" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "gitpython" }, + { name = "pyyaml" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/36/a37a2f6f8d0ed8c3bc616616ed5019e1df2680bd8b7df49ceae80fd457de/bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2", size = 495104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/eb/ff828f4ec32c85e10d9c344e6b7f11bcacfb5d70f2fd16bea6fc1ae6df06/bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a", size = 118274 }, +] + +[package.optional-dependencies] +toml = [ + { name = "toml" }, +] + +[[package]] +name = "black" +version = "22.3.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/1f/b29c7371958ab41a800f8718f5d285bf4333b8d0b5a5a8650234463ee644/black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", size = 554277 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/1b/3ba8128f0b6e86d87343a1275e17baeeeee1a89e02d2a0d275f472be3310/black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", size = 2392131 }, + { url = "https://files.pythonhosted.org/packages/31/1a/0233cdbfbcfbc0864d815cf28ca40cdb65acf3601f3bf943d6d04e867858/black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", size = 1339315 }, + { url = "https://files.pythonhosted.org/packages/4f/98/8f775455f99a8e4b16d8d26efdc8292f99b1c0ebfe04357d800ff379c0ae/black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", size = 1212888 }, + { url = "https://files.pythonhosted.org/packages/93/98/6f7c2f7f81d87b5771febcee933ba58640fca29a767262763bc353074f63/black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", size = 1474258 }, + { url = "https://files.pythonhosted.org/packages/5f/10/613ddfc646a1f51f24ad9173e4969025210fe9034a69718f08297ecb9b76/black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d", size = 1137867 }, + { url = "https://files.pythonhosted.org/packages/2e/ef/a38a2189959246543e60859fb65bd3143129f6d18dfc7bcdd79217f81ca2/black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", size = 153859 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "datacosmos" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "oauthlib" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pystac" }, + { name = "python-common" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "shapely" }, +] + +[package.optional-dependencies] +dev = [ + { name = "bandit", extra = ["toml"] }, + { name = "black" }, + { name = "isort" }, + { name = "pydocstyle" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "bandit", extras = ["toml"], marker = "extra == 'dev'", specifier = "==1.7.4" }, + { name = "black", marker = "extra == 'dev'", specifier = "==22.3.0" }, + { name = "isort", marker = "extra == 'dev'", specifier = "==5.11.4" }, + { name = "oauthlib", specifier = "==3.2.0" }, + { name = "pydantic", specifier = "==2.10.6" }, + { name = "pydantic-settings", specifier = ">=2.7.0" }, + { name = "pydocstyle", marker = "extra == 'dev'", specifier = "==6.1.1" }, + { name = "pystac", specifier = "==1.12.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = "==7.2.0" }, + { name = "python-common", specifier = "==1.4.0", index = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" }, + { name = "requests", specifier = "==2.31.0" }, + { name = "requests-oauthlib", specifier = "==1.3.1" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.9.5" }, + { name = "shapely", specifier = "==1.8.0" }, +] + +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, +] + +[[package]] +name = "distconfig3" +version = "1.0.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/9e/5d46d30668e353b618a00bdeb704cd6fa55e64cc902078e49cdd22874487/distconfig3-1.0.1.tar.gz", hash = "sha256:7d2c7f30a57ef494c5683270587ba7593318746c6e22b9b8953e288c9c303c65", size = 11819 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/68/7258f0df8576bd1709a481b96f56a46cd5894bd075b79ccfd233e86ff395/distconfig3-1.0.1-py2.py3-none-any.whl", hash = "sha256:823e35ae044677e8aa77bed8d9be0780862a2500c63cf95ce85544b9d3d9fc89", size = 19723 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, +] + +[[package]] +name = "gitpython" +version = "3.1.44" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "isort" +version = "5.11.4" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/46/004e2dd6c312e8bb7cb40a6c01b770956e0ef137857e82d47bd9c829356b/isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6", size = 187150 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/3b/a63bafb8141b67c397841b36ad46e7469716af2b2d00cb0be2dfb9667130/isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b", size = 104082 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "oauthlib" +version = "3.2.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/7e/a43cec8b2df28b6494a865324f0ac4be213cb2edcf1e2a717547a93279b0/oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2", size = 163829 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/46/5ee2475e1b46a26ca0fa10d3c1d479577fde6ee289f8c6aa6d7ec33e31fd/oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe", size = 151524 }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.30.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "importlib-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/6d/bbbf879826b7f3c89a45252010b5796fb1f1a0d45d9dc4709db0ef9a06c8/opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240", size = 63703 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/0a/eea862fae6413d8181b23acf8e13489c90a45f17986ee9cf4eab8a0b9ad9/opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09", size = 64955 }, +] + +[[package]] +name = "opentelemetry-exporter-jaeger-thrift" +version = "1.21.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "thrift" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/ff/560d975ce09ec123dce1b7a97877bad1a5b2424c5c8e31e55169d4cd85e6/opentelemetry_exporter_jaeger_thrift-1.21.0.tar.gz", hash = "sha256:41119bc7e5602cec83dd7d7060f061ecbc91de231272e8f515b07ef9a4b6e41c", size = 27722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/2b/c6ff9604f5b8469e1fabe2b6ee50c36dadf0f572a430494d8dd5ce37d99f/opentelemetry_exporter_jaeger_thrift-1.21.0-py3-none-any.whl", hash = "sha256:4364b8dfa6965707c72c43d85942b1491982b7d44f0123d593513e8bedafa9e2", size = 35929 }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.30.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/ee/d710062e8a862433d1be0b85920d0c653abe318878fef2d14dfe2c62ff7b/opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18", size = 158633 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/28/64d781d6adc6bda2260067ce2902bd030cf45aec657e02e28c5b4480b976/opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091", size = 118717 }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.51b0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/c0/0f9ef4605fea7f2b83d55dd0b0d7aebe8feead247cd6facd232b30907b4f/opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47", size = 107191 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/75/d7bdbb6fd8630b4cafb883482b75c4fc276b6426619539d266e32ac53266/opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae", size = 177416 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "pbr" +version = "6.1.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/d2/510cc0d218e753ba62a1bc1434651db3cd797a9716a0a66cc714cb4f0935/pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b", size = 125702 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, + { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, + { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, + { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, + { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, + { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, + { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, + { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, + { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, + { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, + { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, + { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, + { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, + { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, + { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, + { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, + { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, + { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, + { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, + { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, + { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, + { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, + { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, + { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, + { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, + { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, + { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, + { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, + { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, + { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, + { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, + { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, + { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, + { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.7.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, +] + +[[package]] +name = "pydocstyle" +version = "6.1.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "snowballstemmer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/30/4cdea3c8342ad343d41603afc1372167c224a04dc5dc0bf4193ccb39b370/pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc", size = 35663 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/67/4df10786068766000518c6ad9c4a614e77585a12ab8f0654c776757ac9dc/pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4", size = 37213 }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + +[[package]] +name = "pystac" +version = "1.12.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/14/25c6eeeb1f34afb69a1b86915be093b05dd3cdc2cf1acf9a7266275dc78d/pystac-1.12.1.tar.gz", hash = "sha256:7735d217c1c596f916d0a048c7bdd08f6bc6cb97369b628fab9b7eba4092fbdd", size = 149815 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/19/64b318a28d152e2ebf29c3e06c0a1147045a951398bc8de1f4bea873ef46/pystac-1.12.1-py3-none-any.whl", hash = "sha256:eb46fe63b494cfd1ccf5dfafa9256a1893ef9b6d4ee69373e040e3888270357e", size = 194151 }, +] + +[[package]] +name = "pytest" +version = "7.2.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "attrs" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/21/055f39bf8861580b43f845f9e8270c7786fe629b2f8562ff09007132e2e7/pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59", size = 1300608 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/68/a5eb36c3a8540594b6035e6cdae40c1ef1b6a2bfacbecc3d1a544583c078/pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", size = 316791 }, +] + +[[package]] +name = "python-common" +version = "1.4.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-jaeger-thrift" }, + { name = "opentelemetry-sdk" }, + { name = "pyjwt" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "structlog" }, + { name = "structlog-gcp" }, + { name = "vyper-config" }, +] +sdist = { url = "https://git.o-c.space/api/v4/projects/689/packages/pypi/files/ba7d2eba24b34e3937a98f4ece6b2b07d90d7ee505bb84ba2c11fde766bbd46b/python_common-1.4.0.tar.gz", hash = "sha256:ba7d2eba24b34e3937a98f4ece6b2b07d90d7ee505bb84ba2c11fde766bbd46b" } +wheels = [ + { url = "https://git.o-c.space/api/v4/projects/689/packages/pypi/files/3d3dcca4cb3ad22bdf47d508d523c4c330f91fce3fde6cdeae17261cbe8f66a6/python_common-1.4.0-py3-none-any.whl", hash = "sha256:3d3dcca4cb3ad22bdf47d508d523c4c330f91fce3fde6cdeae17261cbe8f66a6" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "requests" +version = "2.31.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574 }, +] + +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/52/531ef197b426646f26b53815a7d2a67cb7a331ef098bb276db26a68ac49f/requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a", size = 52027 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/bb/5deac77a9af870143c684ab46a7934038a53eb4aa975bc0687ed6ca2c610/requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", size = 23892 }, +] + +[[package]] +name = "ruff" +version = "0.9.5" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/74/6c359f6b9ed85b88df6ef31febce18faeb852f6c9855651dfb1184a46845/ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c", size = 3634177 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/4b/82b7c9ac874e72b82b19fd7eab57d122e2df44d2478d90825854f9232d02/ruff-0.9.5-py3-none-linux_armv6l.whl", hash = "sha256:d466d2abc05f39018d53f681fa1c0ffe9570e6d73cde1b65d23bb557c846f442", size = 11681264 }, + { url = "https://files.pythonhosted.org/packages/27/5c/f5ae0a9564e04108c132e1139d60491c0abc621397fe79a50b3dc0bd704b/ruff-0.9.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38840dbcef63948657fa7605ca363194d2fe8c26ce8f9ae12eee7f098c85ac8a", size = 11657554 }, + { url = "https://files.pythonhosted.org/packages/2a/83/c6926fa3ccb97cdb3c438bb56a490b395770c750bf59f9bc1fe57ae88264/ruff-0.9.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d56ba06da53536b575fbd2b56517f6f95774ff7be0f62c80b9e67430391eeb36", size = 11088959 }, + { url = "https://files.pythonhosted.org/packages/af/a7/42d1832b752fe969ffdbfcb1b4cb477cb271bed5835110fb0a16ef31ab81/ruff-0.9.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7cb2a01da08244c50b20ccfaeb5972e4228c3c3a1989d3ece2bc4b1f996001", size = 11902041 }, + { url = "https://files.pythonhosted.org/packages/53/cf/1fffa09fb518d646f560ccfba59f91b23c731e461d6a4dedd21a393a1ff1/ruff-0.9.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96d5c76358419bc63a671caac70c18732d4fd0341646ecd01641ddda5c39ca0b", size = 11421069 }, + { url = "https://files.pythonhosted.org/packages/09/27/bb8f1b7304e2a9431f631ae7eadc35550fe0cf620a2a6a0fc4aa3d736f94/ruff-0.9.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:deb8304636ed394211f3a6d46c0e7d9535b016f53adaa8340139859b2359a070", size = 12625095 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/ab00bc9d3df35a5f1b64f5117458160a009f93ae5caf65894ebb63a1842d/ruff-0.9.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df455000bf59e62b3e8c7ba5ed88a4a2bc64896f900f311dc23ff2dc38156440", size = 13257797 }, + { url = "https://files.pythonhosted.org/packages/88/81/c639a082ae6d8392bc52256058ec60f493c6a4d06d5505bccface3767e61/ruff-0.9.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de92170dfa50c32a2b8206a647949590e752aca8100a0f6b8cefa02ae29dce80", size = 12763793 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/0a3d8f56d1e49af466dc770eeec5c125977ba9479af92e484b5b0251ce9c/ruff-0.9.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d28532d73b1f3f627ba88e1456f50748b37f3a345d2be76e4c653bec6c3e393", size = 14386234 }, + { url = "https://files.pythonhosted.org/packages/04/70/e59c192a3ad476355e7f45fb3a87326f5219cc7c472e6b040c6c6595c8f0/ruff-0.9.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c746d7d1df64f31d90503ece5cc34d7007c06751a7a3bbeee10e5f2463d52d2", size = 12437505 }, + { url = "https://files.pythonhosted.org/packages/55/4e/3abba60a259d79c391713e7a6ccabf7e2c96e5e0a19100bc4204f1a43a51/ruff-0.9.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11417521d6f2d121fda376f0d2169fb529976c544d653d1d6044f4c5562516ee", size = 11884799 }, + { url = "https://files.pythonhosted.org/packages/a3/db/b0183a01a9f25b4efcae919c18fb41d32f985676c917008620ad692b9d5f/ruff-0.9.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b9d71c3879eb32de700f2f6fac3d46566f644a91d3130119a6378f9312a38e1", size = 11527411 }, + { url = "https://files.pythonhosted.org/packages/0a/e4/3ebfcebca3dff1559a74c6becff76e0b64689cea02b7aab15b8b32ea245d/ruff-0.9.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2e36c61145e70febcb78483903c43444c6b9d40f6d2f800b5552fec6e4a7bb9a", size = 12078868 }, + { url = "https://files.pythonhosted.org/packages/ec/b2/5ab808833e06c0a1b0d046a51c06ec5687b73c78b116e8d77687dc0cd515/ruff-0.9.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2f71d09aeba026c922aa7aa19a08d7bd27c867aedb2f74285a2639644c1c12f5", size = 12524374 }, + { url = "https://files.pythonhosted.org/packages/e0/51/1432afcc3b7aa6586c480142caae5323d59750925c3559688f2a9867343f/ruff-0.9.5-py3-none-win32.whl", hash = "sha256:134f958d52aa6fdec3b294b8ebe2320a950d10c041473c4316d2e7d7c2544723", size = 9853682 }, + { url = "https://files.pythonhosted.org/packages/b7/ad/c7a900591bd152bb47fc4882a27654ea55c7973e6d5d6396298ad3fd6638/ruff-0.9.5-py3-none-win_amd64.whl", hash = "sha256:78cc6067f6d80b6745b67498fb84e87d32c6fc34992b52bffefbdae3442967d6", size = 10865744 }, + { url = "https://files.pythonhosted.org/packages/75/d9/fde7610abd53c0c76b6af72fc679cb377b27c617ba704e25da834e0a0608/ruff-0.9.5-py3-none-win_arm64.whl", hash = "sha256:18a29f1a005bddb229e580795627d297dfa99f16b30c7039e73278cf6b5f9fa9", size = 10064595 }, +] + +[[package]] +name = "setuptools" +version = "75.8.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, +] + +[[package]] +name = "shapely" +version = "1.8.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/0c/454c80f71bd5ece52fb06d2905bf956b9122f4be539d5ae5df4b10dd3e14/Shapely-1.8.0.tar.gz", hash = "sha256:f5307ee14ba4199f8bbcf6532ca33064661c1433960c432c84f0daa73b47ef9c", size = 278344 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/39/563e6598e74db4e2ac184e980b9c71cbccc2caaa7e40b3df6e63c863fd57/Shapely-1.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c5632cedea6d815b61eb4c264da1c3f24a8ce2ceba2f74e30fba340ca230563", size = 1123044 }, + { url = "https://files.pythonhosted.org/packages/11/24/5e84be7dedaffc2d5a94c1266fc2420813f629500da4d244b6096448a59e/Shapely-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4ce1f18a0c9bb6b483c73bd7a0eb3a5e90676bcc29b9c27120236e662195c9d", size = 1185027 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, +] + +[[package]] +name = "stevedore" +version = "5.4.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "pbr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/e9/4eedccff8332cc40cc60ddd3b28d4c3e255ee7e9c65679fa4533ab98f598/stevedore-5.4.0.tar.gz", hash = "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d", size = 513899 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/73/d0091d22a65b55e8fb6aca7b3b6713b5a261dd01cec4cfd28ed127ac0cfc/stevedore-5.4.0-py3-none-any.whl", hash = "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857", size = 49534 }, +] + +[[package]] +name = "structlog" +version = "24.4.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/a3/e811a94ac3853826805253c906faa99219b79951c7d58605e89c79e65768/structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4", size = 1348634 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610", size = 67180 }, +] + +[[package]] +name = "structlog-gcp" +version = "0.3.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "structlog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/02/d295e618debade6ad54e5a6edabd719436f6f28c8e1089f05b54af2a3f70/structlog_gcp-0.3.0.tar.gz", hash = "sha256:ab88b215386ff0209b80ef2f73bd60d9ce56c5a5fc07e5da9f1947cbc40a0c04", size = 113017 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/22/9a2e089c03f666ba94fe2be9863b020f8c3b9e71997ff4de55ba750a2c03/structlog_gcp-0.3.0-py3-none-any.whl", hash = "sha256:767c9e7a545104e73230913ac9c92f78baffaa5b850c7780fdee5c8955fbc253", size = 9275 }, +] + +[[package]] +name = "thrift" +version = "0.21.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/1c/418058b4750176b638ab60b4d5a554a2969dcd2363ae458519d0f747adff/thrift-0.21.0.tar.gz", hash = "sha256:5e6f7c50f936ebfa23e924229afc95eb219f8c8e5a83202dd4a391244803e402", size = 62509 } + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + +[[package]] +name = "vyper-config" +version = "1.2.1" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +dependencies = [ + { name = "distconfig3" }, + { name = "pyyaml" }, + { name = "toml" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/2f/3371501c32bd27cf4f1bbda2f51d22df7e191e2475628b37eeb6b169c8f2/vyper-config-1.2.1.tar.gz", hash = "sha256:d7e6ecaf363539caab4e3cb85d55a98df00a42547965b2703d1989302d30ec57", size = 22988 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/7e/d7bc18e84f7c45d7cc1857a01237f85220b953c4690768d9fb83702c051b/vyper_config-1.2.1-py3-none-any.whl", hash = "sha256:a9907a5707602e7c289f964498a0d5d6e477e8005a9e3797bdab90771ec421c1", size = 15989 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390 }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389 }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020 }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307 }, + { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486 }, + { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777 }, + { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314 }, + { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947 }, + { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778 }, + { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716 }, + { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548 }, + { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334 }, + { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427 }, + { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774 }, + { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 }, + { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 }, + { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 }, + { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 }, + { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 }, + { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 }, + { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 }, + { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 }, + { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 }, + { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 }, + { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 }, + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 }, + { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, + { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, + { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, + { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, + { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, + { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, + { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, + { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, + { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, + { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, + { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, + { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, + { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, + { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, + { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, + { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, + { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, + { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, + { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, + { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, + { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, + { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +] From 0fd9f683c0f447231a657e324bb783e942cb2cf3 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 10 Feb 2025 14:57:38 +0000 Subject: [PATCH 124/135] Update README.md file --- README.md | 178 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 176 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba5933f..16d4ba1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,177 @@ -# DataCosmos Python SDK +# DataCosmos SDK + +## Overview +The **DataCosmos SDK** allows Open Cosmos' customers to interact with the main APIs required for creating applications that integrate with **DataCosmos**. This SDK provides authentication handling, HTTP request utilities, and a client for interacting with the **STAC API** (SpatioTemporal Asset Catalog). + +## Installation + +### Prerequisites +The SDK uses **uv** as a dependency manager. To install it: + +```sh +pip install uv +``` + +### Setting Up the SDK +To install the SDK and its dependencies: + +```sh +uv venv +uv pip install -r pyproject.toml +uv pip install -r pyproject.toml .[dev] +source .venv/bin/activate +``` + +## Authentication +The SDK requires authentication via a token stored in a `.netrc` file. + +### Setting Up Authentication +Create the `.netrc` file: + +```sh +touch ~/.netrc +``` + +Add the following content: + +``` +machine git.o-c.space +login __token__ +password {provided_token} +``` + +You will need to **request** the token from Open Cosmos. + +## Using the SDK + +### Initializing the Client +The SDK provides the `DatacosmosClient` class to manage authentication and interact with the API. + +```python +from datacosmos.client import DatacosmosClient + +client = DatacosmosClient() +``` + +### STAC Client +The **STACClient** allows interaction with the STAC API, enabling search, retrieval, creation, updating, and deletion of STAC items. + +#### Initialize STACClient + +```python +from datacosmos.stac.stac_client import STACClient + +stac_client = STACClient(client) +``` + +### STACClient Methods + +#### 1. **Search Items** +```python +from datacosmos.stac.models.search_parameters import SearchParameters + +parameters = SearchParameters(collections=["example-collection"], limit=1) +items = list(stac_client.search_items(parameters=parameters)) +``` + +#### 2. **Fetch a Single Item** +```python +item = stac_client.fetch_item(item_id="example-item", collection_id="example-collection") +``` + +#### 3. **Fetch All Items in a Collection** +```python +items = stac_client.fetch_collection_items(collection_id="example-collection") +``` + +#### 4. **Create a New STAC Item** +```python +from pystac import Item, Asset +from datetime import datetime + +stac_item = Item( + id="new-item", + geometry={"type": "Point", "coordinates": [102.0, 0.5]}, + bbox=[101.0, 0.0, 103.0, 1.0], + datetime=datetime.utcnow(), + properties={}, + collection="example-collection" +) + +stac_item.add_asset( + "image", + Asset( + href="https://example.com/sample-image.tiff", + media_type="image/tiff", + roles=["data"], + title="Sample Image" + ) +) + +stac_client.create_item(collection_id="example-collection", item=stac_item) +``` + +#### 5. **Update an Existing STAC Item** +```python +from datacosmos.stac.models.item_update import ItemUpdate +from pystac import Asset, Link + +update_payload = ItemUpdate( + properties={ + "new_property": "updated_value", + "datetime": "2024-11-10T14:58:00Z" + }, + assets={ + "image": Asset( + href="https://example.com/updated-image.tiff", + media_type="image/tiff" + ) + }, + links=[ + Link(rel="self", target="https://example.com/updated-image.tiff") + ], + geometry={ + "type": "Point", + "coordinates": [10, 20] + }, + bbox=[10.0, 20.0, 30.0, 40.0] +) + +stac_client.update_item(item_id="new-item", collection_id="example-collection", update_data=update_payload) +``` + +#### 6. **Delete an Item** +```python +stac_client.delete_item(item_id="new-item", collection_id="example-collection") +``` + +## Configuration +The SDK supports configuration via **YAML files** and **environment variables**. + +- **YAML Configuration** (default: `config/config.yaml`) +- **Environment Variables** (e.g., `OC_AUTH_CLIENT_ID`, `OC_AUTH_CLIENT_SECRET`) + +The SDK loads configuration settings automatically. + +## Contributing +If you would like to contribute: +1. Fork the repository. +2. Create a feature branch. +3. Submit a pull request. + +Before making changes, ensure that: +- The code is formatted using **Black** and **isort**. +- Static analysis and linting are performed using **ruff** and **pydocstyle**. +- Security checks are performed using **bandit**. +- Tests are executed with **pytest**. + +```sh +black . +isort . +ruff . +pydocstyle . +bandit -r . +pytest +``` + -A library for interacting with DataCosmos from Python code. From d996090bcd53bdb15f34444d2e48eed7484e9297 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 10 Feb 2025 15:08:53 +0000 Subject: [PATCH 125/135] Refactor README.md file --- README.md | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 16d4ba1..47038ad 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,28 @@ password {provided_token} You will need to **request** the token from Open Cosmos. +### Configuration Methods +To instantiate the `DatacosmosClient`, it is **recommended** to pass a `Config` object directly: + +```python +from datacosmos.client import DatacosmosClient +from datacosmos.config import Config + +config = Config( + authentication={ + "client_id": "your_client_id", + "client_secret": "your_client_secret", + "token_url": "https://login.open-cosmos.com/oauth/token", + "audience": "https://beeapp.open-cosmos.com" + } +) +client = DatacosmosClient(config=config) +``` + +Alternatively, the SDK can load configuration automatically from: +- A YAML file (`config/config.yaml`) +- Environment variables + ## Using the SDK ### Initializing the Client @@ -146,12 +168,7 @@ stac_client.delete_item(item_id="new-item", collection_id="example-collection") ``` ## Configuration -The SDK supports configuration via **YAML files** and **environment variables**. - -- **YAML Configuration** (default: `config/config.yaml`) -- **Environment Variables** (e.g., `OC_AUTH_CLIENT_ID`, `OC_AUTH_CLIENT_SECRET`) - -The SDK loads configuration settings automatically. +The SDK supports configuration via **manual instantiation of `Config` (recommended)**, **YAML files**, and **environment variables**. ## Contributing If you would like to contribute: @@ -168,10 +185,9 @@ Before making changes, ensure that: ```sh black . isort . -ruff . +ruff check . --select C901 pydocstyle . -bandit -r . +bandit -r -c pyproject.toml . --skip B105,B106,B101 pytest ``` - From eb36654138edda845ff46f8ea226a4165d5312e7 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Mon, 10 Feb 2025 15:09:53 +0000 Subject: [PATCH 126/135] Remove last configuration part --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 47038ad..aa3e5ae 100644 --- a/README.md +++ b/README.md @@ -167,9 +167,6 @@ stac_client.update_item(item_id="new-item", collection_id="example-collection", stac_client.delete_item(item_id="new-item", collection_id="example-collection") ``` -## Configuration -The SDK supports configuration via **manual instantiation of `Config` (recommended)**, **YAML files**, and **environment variables**. - ## Contributing If you would like to contribute: 1. Fork the repository. From a3d8781b841efbc00014176f65a9ce00f2d3fd48 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 14 Feb 2025 10:19:43 +0000 Subject: [PATCH 127/135] Make datacosmos sdk not rely on private external packages; update readme file --- .github/workflows/main.yaml | 36 +- README.md | 72 +- config/models/url.py | 3 +- datacosmos/stac/stac_client.py | 12 +- datacosmos/utils/__init__.py | 1 + datacosmos/utils/http_response/__init__.py | 1 + .../utils/http_response/check_api_response.py | 34 + .../utils/http_response/models/__init__.py | 1 + .../http_response/models/datacosmos_error.py | 26 + .../models/datacosmos_response.py | 11 + datacosmos/utils/url.py | 37 + pyproject.toml | 8 - uv.lock | 1018 +---------------- 13 files changed, 201 insertions(+), 1059 deletions(-) create mode 100644 datacosmos/utils/__init__.py create mode 100644 datacosmos/utils/http_response/__init__.py create mode 100644 datacosmos/utils/http_response/check_api_response.py create mode 100644 datacosmos/utils/http_response/models/__init__.py create mode 100644 datacosmos/utils/http_response/models/datacosmos_error.py create mode 100644 datacosmos/utils/http_response/models/datacosmos_response.py create mode 100644 datacosmos/utils/url.py diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 9680b67..0ae32d6 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -14,14 +14,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" .[dev] + - name: Install dependencies + run: uv pip install -r pyproject.toml .[dev] - name: Activate virtual environment & run linters run: | source .venv/bin/activate @@ -33,14 +31,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" .[dev] + - name: Install dependencies + run: uv pip install -r pyproject.toml .[dev] - name: Activate virtual environment & run bandit run: | source .venv/bin/activate @@ -51,14 +47,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" .[dev] + - name: Install dependencies + run: uv pip install -r pyproject.toml .[dev] - name: Activate virtual environment & run ruff run: | source .venv/bin/activate @@ -69,14 +63,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" .[dev] + - name: Install dependencies + run: uv pip install -r pyproject.toml .[dev] - name: Activate virtual environment & run pydocstyle run: | source .venv/bin/activate @@ -88,14 +80,12 @@ jobs: needs: [bandit, cognitive, lint, pydocstyle] steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" .[dev] + - name: Install dependencies + run: uv pip install -r pyproject.toml .[dev] - name: Activate virtual environment & run tests run: | source .venv/bin/activate @@ -108,14 +98,12 @@ jobs: if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment run: uv venv - - name: Install dependencies with GitLab token - run: uv pip install --index-url "https://__token__:${{ secrets.GITLAB_PYPI_TOKEN }}@git.o-c.space/api/v4/projects/689/packages/pypi/simple" . + - name: Install dependencies + run: uv pip install -r pyproject.toml - name: Version uses: paulhatch/semantic-version@v5.0.0 id: version diff --git a/README.md b/README.md index aa3e5ae..3341326 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,21 @@ # DataCosmos SDK ## Overview -The **DataCosmos SDK** allows Open Cosmos' customers to interact with the main APIs required for creating applications that integrate with **DataCosmos**. This SDK provides authentication handling, HTTP request utilities, and a client for interacting with the **STAC API** (SpatioTemporal Asset Catalog). +The **DataCosmos SDK** allows Open Cosmos' customers to interact with the **DataCosmos APIs** for seamless data management and retrieval. It provides authentication handling, HTTP request utilities, and a client for interacting with the **STAC API** (SpatioTemporal Asset Catalog). ## Installation -### Prerequisites -The SDK uses **uv** as a dependency manager. To install it: +### Install via PyPI +The easiest way to install the SDK is via **pip**: ```sh -pip install uv -``` - -### Setting Up the SDK -To install the SDK and its dependencies: - -```sh -uv venv -uv pip install -r pyproject.toml -uv pip install -r pyproject.toml .[dev] -source .venv/bin/activate +pip install datacosmos ``` -## Authentication -The SDK requires authentication via a token stored in a `.netrc` file. - -### Setting Up Authentication -Create the `.netrc` file: +## Getting Started -```sh -touch ~/.netrc -``` - -Add the following content: - -``` -machine git.o-c.space -login __token__ -password {provided_token} -``` - -You will need to **request** the token from Open Cosmos. - -### Configuration Methods -To instantiate the `DatacosmosClient`, it is **recommended** to pass a `Config` object directly: +### Initializing the Client +The recommended way to initialize the SDK is by passing a `Config` object with authentication credentials: ```python from datacosmos.client import DatacosmosClient @@ -64,19 +36,8 @@ Alternatively, the SDK can load configuration automatically from: - A YAML file (`config/config.yaml`) - Environment variables -## Using the SDK - -### Initializing the Client -The SDK provides the `DatacosmosClient` class to manage authentication and interact with the API. - -```python -from datacosmos.client import DatacosmosClient - -client = DatacosmosClient() -``` - ### STAC Client -The **STACClient** allows interaction with the STAC API, enabling search, retrieval, creation, updating, and deletion of STAC items. +The **STACClient** enables interaction with the STAC API, allowing for searching, retrieving, creating, updating, and deleting STAC items. #### Initialize STACClient @@ -167,12 +128,28 @@ stac_client.update_item(item_id="new-item", collection_id="example-collection", stac_client.delete_item(item_id="new-item", collection_id="example-collection") ``` +## Configuration Options +- **Recommended:** Instantiate `DatacosmosClient` with a `Config` object. +- Alternatively, use **YAML files** (`config/config.yaml`). +- Or, use **environment variables**. + ## Contributing If you would like to contribute: 1. Fork the repository. 2. Create a feature branch. 3. Submit a pull request. +### Development Setup +If you are developing the SDK, you can use `uv` for dependency management: + +```sh +pip install uv +uv venv +uv pip install -r pyproject.toml +uv pip install -r pyproject.toml .[dev] +source .venv/bin/activate +``` + Before making changes, ensure that: - The code is formatted using **Black** and **isort**. - Static analysis and linting are performed using **ruff** and **pydocstyle**. @@ -187,4 +164,3 @@ pydocstyle . bandit -r -c pyproject.toml . --skip B105,B106,B101 pytest ``` - diff --git a/config/models/url.py b/config/models/url.py index 7df9c83..3544876 100644 --- a/config/models/url.py +++ b/config/models/url.py @@ -4,9 +4,10 @@ port, and path. """ -from common.domain.url import URL as DomainURL from pydantic import BaseModel +from datacosmos.utils.url import URL as DomainURL + class URL(BaseModel): """Generic configuration model for a URL. diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py index 2d41ad9..ea7f0f3 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/stac_client.py @@ -5,12 +5,13 @@ from typing import Generator, Optional -from common.sdk.http_response import InvalidRequest, check_api_response from pystac import Item from datacosmos.client import DatacosmosClient +from datacosmos.exceptions.datacosmos_exception import DatacosmosException from datacosmos.stac.models.item_update import ItemUpdate from datacosmos.stac.models.search_parameters import SearchParameters +from datacosmos.utils.http_response import check_api_response class STACClient: @@ -173,9 +174,12 @@ def _extract_pagination_token(self, next_href: str) -> Optional[str]: Optional[str]: The extracted token, or None if parsing fails. Raises: - InvalidRequest: If pagination token extraction fails. + DatacosmosException: If pagination token extraction fails. """ try: return next_href.split("?")[1].split("=")[-1] - except (IndexError, AttributeError): - raise InvalidRequest(f"Failed to parse pagination token from {next_href}") + except (IndexError, AttributeError) as e: + raise DatacosmosException( + f"Failed to parse pagination token from {next_href}", + response=e.response, + ) from e diff --git a/datacosmos/utils/__init__.py b/datacosmos/utils/__init__.py new file mode 100644 index 0000000..fdfe1ee --- /dev/null +++ b/datacosmos/utils/__init__.py @@ -0,0 +1 @@ +"""Http response and url utils for datacosmos.""" diff --git a/datacosmos/utils/http_response/__init__.py b/datacosmos/utils/http_response/__init__.py new file mode 100644 index 0000000..2a6b2d2 --- /dev/null +++ b/datacosmos/utils/http_response/__init__.py @@ -0,0 +1 @@ +"""Validates an API response.""" diff --git a/datacosmos/utils/http_response/check_api_response.py b/datacosmos/utils/http_response/check_api_response.py new file mode 100644 index 0000000..6ada038 --- /dev/null +++ b/datacosmos/utils/http_response/check_api_response.py @@ -0,0 +1,34 @@ +"""Validates an API response and raises a DatacosmosException if an error occurs.""" + +from pydantic import ValidationError +from requests import Response + +from datacosmos.exceptions.datacosmos_exception import DatacosmosException +from datacosmos.utils.http_response.models.datacosmos_response import DatacosmosResponse + + +def check_api_response(response: Response) -> None: + """Validates an API response and raises a DatacosmosException if an error occurs. + + Args: + resp (requests.Response): The response object. + + Raises: + DatacosmosException: If the response status code indicates an error. + """ + if 200 <= response.status_code < 400: + return + + try: + response = DatacosmosResponse.model_validate_json(response.text) + msg = response.errors[0].human_readable() + if len(response.errors) > 1: + msg = "\n * " + "\n * ".join( + error.human_readable() for error in response.errors + ) + raise DatacosmosException(msg, response=response) + + except ValidationError: + raise DatacosmosException( + f"HTTP {response.status_code}: {response.text}", response=response + ) diff --git a/datacosmos/utils/http_response/models/__init__.py b/datacosmos/utils/http_response/models/__init__.py new file mode 100644 index 0000000..6aac997 --- /dev/null +++ b/datacosmos/utils/http_response/models/__init__.py @@ -0,0 +1 @@ +"""Models for validation of API response.""" diff --git a/datacosmos/utils/http_response/models/datacosmos_error.py b/datacosmos/utils/http_response/models/datacosmos_error.py new file mode 100644 index 0000000..62ed3a5 --- /dev/null +++ b/datacosmos/utils/http_response/models/datacosmos_error.py @@ -0,0 +1,26 @@ +"""Structured API error message for Datacosmos.""" + +from pydantic import BaseModel + + +class DatacosmosError(BaseModel): + """Structured API error message for Datacosmos.""" + + message: str + field: str | None = None + type: str | None = None + source: str | None = None + trace_id: str | None = None + + def human_readable(self) -> str: + """Formats the error message into a readable format.""" + msg = self.message + if self.type: + msg += f" (type: {self.type})" + if self.field: + msg += f" (field: {self.field})" + if self.source: + msg += f" (source: {self.source})" + if self.trace_id: + msg += f" (trace_id: {self.trace_id})" + return msg diff --git a/datacosmos/utils/http_response/models/datacosmos_response.py b/datacosmos/utils/http_response/models/datacosmos_response.py new file mode 100644 index 0000000..a668e88 --- /dev/null +++ b/datacosmos/utils/http_response/models/datacosmos_response.py @@ -0,0 +1,11 @@ +"""Structured response for Datacosmos handling multiple API errors.""" + +from pydantic import BaseModel + +from datacosmos.utils.http_response.models.datacosmos_error import DatacosmosError + + +class DatacosmosResponse(BaseModel): + """Structured response for Datacosmos handling multiple API errors.""" + + errors: list[DatacosmosError] diff --git a/datacosmos/utils/url.py b/datacosmos/utils/url.py new file mode 100644 index 0000000..4242f87 --- /dev/null +++ b/datacosmos/utils/url.py @@ -0,0 +1,37 @@ +"""URL utility class for building and handling URLs in the SDK.""" + + +class URL: + """Class to represent and build URLs in a convenient way.""" + + def __init__(self, protocol: str, host: str, port: int, base: str): + """Creates a new basis to build URLs. + + Args: + protocol (str): Protocol to use in the URL (http/https). + host (str): Hostname (e.g., example.com). + port (int): Port number. + base (str): Base path (e.g., /api/v1). + """ + self.protocol = protocol + self.host = host + self.port = port + self.base = base + + def string(self) -> str: + """Returns the full URL as a string.""" + port = "" if self.port in [80, 443] else f":{self.port}" + base = f"/{self.base.lstrip('/')}" if self.base else "" + return f"{self.protocol}://{self.host}{port}{base}" + + def with_suffix(self, suffix: str) -> str: + """Appends a suffix to the URL, ensuring proper formatting. + + Args: + suffix (str): The path to append. + + Returns: + str: Full URL with the suffix. + """ + base = self.string() + return f"{base.rstrip('/')}/{suffix.lstrip('/')}" diff --git a/pyproject.toml b/pyproject.toml index e053441..f36d303 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ classifiers = [ ] dependencies = [ "pydantic_settings>=2.7.0", - "python-common==1.4.0", "requests==2.31.0", "oauthlib==3.2.0", "requests-oauthlib==1.3.1", @@ -51,10 +50,3 @@ include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true line_length = 88 - -[tool.uv.sources] -python-common = { index = "gitlab" } - -[[tool.uv.index]] -name = "gitlab" -url = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" \ No newline at end of file diff --git a/uv.lock b/uv.lock index c1b6715..f660384 100644 --- a/uv.lock +++ b/uv.lock @@ -1,974 +1,44 @@ -version = 1 -requires-python = ">=3.10" - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "attrs" -version = "25.1.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, -] - -[[package]] -name = "bandit" -version = "1.7.4" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "gitpython" }, - { name = "pyyaml" }, - { name = "stevedore" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/36/a37a2f6f8d0ed8c3bc616616ed5019e1df2680bd8b7df49ceae80fd457de/bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2", size = 495104 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/eb/ff828f4ec32c85e10d9c344e6b7f11bcacfb5d70f2fd16bea6fc1ae6df06/bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a", size = 118274 }, -] - -[package.optional-dependencies] -toml = [ - { name = "toml" }, -] - -[[package]] -name = "black" -version = "22.3.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ee/1f/b29c7371958ab41a800f8718f5d285bf4333b8d0b5a5a8650234463ee644/black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", size = 554277 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/1b/3ba8128f0b6e86d87343a1275e17baeeeee1a89e02d2a0d275f472be3310/black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", size = 2392131 }, - { url = "https://files.pythonhosted.org/packages/31/1a/0233cdbfbcfbc0864d815cf28ca40cdb65acf3601f3bf943d6d04e867858/black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", size = 1339315 }, - { url = "https://files.pythonhosted.org/packages/4f/98/8f775455f99a8e4b16d8d26efdc8292f99b1c0ebfe04357d800ff379c0ae/black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", size = 1212888 }, - { url = "https://files.pythonhosted.org/packages/93/98/6f7c2f7f81d87b5771febcee933ba58640fca29a767262763bc353074f63/black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", size = 1474258 }, - { url = "https://files.pythonhosted.org/packages/5f/10/613ddfc646a1f51f24ad9173e4969025210fe9034a69718f08297ecb9b76/black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d", size = 1137867 }, - { url = "https://files.pythonhosted.org/packages/2e/ef/a38a2189959246543e60859fb65bd3143129f6d18dfc7bcdd79217f81ca2/black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", size = 153859 }, -] - -[[package]] -name = "certifi" -version = "2025.1.31" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, - { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, - { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, - { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, - { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, - { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, - { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, - { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, - { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, - { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, - { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, - { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, - { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, - { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, - { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, - { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, - { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, - { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, - { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, - { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, - { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, - { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, - { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, - { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, - { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, - { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, - { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, - { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, - { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, - { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, - { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, - { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, - { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, - { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, - { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, - { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, - { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, - { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, - { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "datacosmos" -version = "0.0.1" -source = { editable = "." } -dependencies = [ - { name = "oauthlib" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pystac" }, - { name = "python-common" }, - { name = "requests" }, - { name = "requests-oauthlib" }, - { name = "shapely" }, -] - -[package.optional-dependencies] -dev = [ - { name = "bandit", extra = ["toml"] }, - { name = "black" }, - { name = "isort" }, - { name = "pydocstyle" }, - { name = "pytest" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "bandit", extras = ["toml"], marker = "extra == 'dev'", specifier = "==1.7.4" }, - { name = "black", marker = "extra == 'dev'", specifier = "==22.3.0" }, - { name = "isort", marker = "extra == 'dev'", specifier = "==5.11.4" }, - { name = "oauthlib", specifier = "==3.2.0" }, - { name = "pydantic", specifier = "==2.10.6" }, - { name = "pydantic-settings", specifier = ">=2.7.0" }, - { name = "pydocstyle", marker = "extra == 'dev'", specifier = "==6.1.1" }, - { name = "pystac", specifier = "==1.12.1" }, - { name = "pytest", marker = "extra == 'dev'", specifier = "==7.2.0" }, - { name = "python-common", specifier = "==1.4.0", index = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" }, - { name = "requests", specifier = "==2.31.0" }, - { name = "requests-oauthlib", specifier = "==1.3.1" }, - { name = "ruff", marker = "extra == 'dev'", specifier = "==0.9.5" }, - { name = "shapely", specifier = "==1.8.0" }, -] - -[[package]] -name = "deprecated" -version = "1.2.18" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, -] - -[[package]] -name = "distconfig3" -version = "1.0.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/9e/5d46d30668e353b618a00bdeb704cd6fa55e64cc902078e49cdd22874487/distconfig3-1.0.1.tar.gz", hash = "sha256:7d2c7f30a57ef494c5683270587ba7593318746c6e22b9b8953e288c9c303c65", size = 11819 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/68/7258f0df8576bd1709a481b96f56a46cd5894bd075b79ccfd233e86ff395/distconfig3-1.0.1-py2.py3-none-any.whl", hash = "sha256:823e35ae044677e8aa77bed8d9be0780862a2500c63cf95ce85544b9d3d9fc89", size = 19723 }, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, -] - -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, -] - -[[package]] -name = "gitpython" -version = "3.1.44" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "importlib-metadata" -version = "8.5.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, -] - -[[package]] -name = "isort" -version = "5.11.4" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/46/004e2dd6c312e8bb7cb40a6c01b770956e0ef137857e82d47bd9c829356b/isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6", size = 187150 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/3b/a63bafb8141b67c397841b36ad46e7469716af2b2d00cb0be2dfb9667130/isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b", size = 104082 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, -] - -[[package]] -name = "oauthlib" -version = "3.2.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/7e/a43cec8b2df28b6494a865324f0ac4be213cb2edcf1e2a717547a93279b0/oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2", size = 163829 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/46/5ee2475e1b46a26ca0fa10d3c1d479577fde6ee289f8c6aa6d7ec33e31fd/oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe", size = 151524 }, -] - -[[package]] -name = "opentelemetry-api" -version = "1.30.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "deprecated" }, - { name = "importlib-metadata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/6d/bbbf879826b7f3c89a45252010b5796fb1f1a0d45d9dc4709db0ef9a06c8/opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240", size = 63703 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/0a/eea862fae6413d8181b23acf8e13489c90a45f17986ee9cf4eab8a0b9ad9/opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09", size = 64955 }, -] - -[[package]] -name = "opentelemetry-exporter-jaeger-thrift" -version = "1.21.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "thrift" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/ff/560d975ce09ec123dce1b7a97877bad1a5b2424c5c8e31e55169d4cd85e6/opentelemetry_exporter_jaeger_thrift-1.21.0.tar.gz", hash = "sha256:41119bc7e5602cec83dd7d7060f061ecbc91de231272e8f515b07ef9a4b6e41c", size = 27722 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/2b/c6ff9604f5b8469e1fabe2b6ee50c36dadf0f572a430494d8dd5ce37d99f/opentelemetry_exporter_jaeger_thrift-1.21.0-py3-none-any.whl", hash = "sha256:4364b8dfa6965707c72c43d85942b1491982b7d44f0123d593513e8bedafa9e2", size = 35929 }, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.30.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/93/ee/d710062e8a862433d1be0b85920d0c653abe318878fef2d14dfe2c62ff7b/opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18", size = 158633 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/28/64d781d6adc6bda2260067ce2902bd030cf45aec657e02e28c5b4480b976/opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091", size = 118717 }, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.51b0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "deprecated" }, - { name = "opentelemetry-api" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1e/c0/0f9ef4605fea7f2b83d55dd0b0d7aebe8feead247cd6facd232b30907b4f/opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47", size = 107191 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/75/d7bdbb6fd8630b4cafb883482b75c4fc276b6426619539d266e32ac53266/opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae", size = 177416 }, -] - -[[package]] -name = "packaging" -version = "24.2" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, -] - -[[package]] -name = "pbr" -version = "6.1.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/d2/510cc0d218e753ba62a1bc1434651db3cd797a9716a0a66cc714cb4f0935/pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b", size = 125702 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997 }, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, -] - -[[package]] -name = "pydantic" -version = "2.10.6" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, -] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, - { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, - { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, - { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, - { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, - { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, - { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, - { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, - { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, - { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, - { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, - { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, - { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, - { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, - { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, - { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, - { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, - { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, - { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, - { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, - { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, - { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, - { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, - { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, - { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, - { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, - { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, - { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, - { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, - { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, - { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, - { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, - { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, - { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, - { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, - { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, - { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, - { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, - { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, - { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, - { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, - { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, - { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, - { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, - { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, - { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, -] - -[[package]] -name = "pydantic-settings" -version = "2.7.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, -] - -[[package]] -name = "pydocstyle" -version = "6.1.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "snowballstemmer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4c/30/4cdea3c8342ad343d41603afc1372167c224a04dc5dc0bf4193ccb39b370/pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc", size = 35663 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/67/4df10786068766000518c6ad9c4a614e77585a12ab8f0654c776757ac9dc/pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4", size = 37213 }, -] - -[[package]] -name = "pyjwt" -version = "2.10.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, -] - -[[package]] -name = "pystac" -version = "1.12.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cc/14/25c6eeeb1f34afb69a1b86915be093b05dd3cdc2cf1acf9a7266275dc78d/pystac-1.12.1.tar.gz", hash = "sha256:7735d217c1c596f916d0a048c7bdd08f6bc6cb97369b628fab9b7eba4092fbdd", size = 149815 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/19/64b318a28d152e2ebf29c3e06c0a1147045a951398bc8de1f4bea873ef46/pystac-1.12.1-py3-none-any.whl", hash = "sha256:eb46fe63b494cfd1ccf5dfafa9256a1893ef9b6d4ee69373e040e3888270357e", size = 194151 }, -] - -[[package]] -name = "pytest" -version = "7.2.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "attrs" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/21/055f39bf8861580b43f845f9e8270c7786fe629b2f8562ff09007132e2e7/pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59", size = 1300608 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/68/a5eb36c3a8540594b6035e6cdae40c1ef1b6a2bfacbecc3d1a544583c078/pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", size = 316791 }, -] - -[[package]] -name = "python-common" -version = "1.4.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-jaeger-thrift" }, - { name = "opentelemetry-sdk" }, - { name = "pyjwt" }, - { name = "requests" }, - { name = "requests-oauthlib" }, - { name = "structlog" }, - { name = "structlog-gcp" }, - { name = "vyper-config" }, -] -sdist = { url = "https://git.o-c.space/api/v4/projects/689/packages/pypi/files/ba7d2eba24b34e3937a98f4ece6b2b07d90d7ee505bb84ba2c11fde766bbd46b/python_common-1.4.0.tar.gz", hash = "sha256:ba7d2eba24b34e3937a98f4ece6b2b07d90d7ee505bb84ba2c11fde766bbd46b" } -wheels = [ - { url = "https://git.o-c.space/api/v4/projects/689/packages/pypi/files/3d3dcca4cb3ad22bdf47d508d523c4c330f91fce3fde6cdeae17261cbe8f66a6/python_common-1.4.0-py3-none-any.whl", hash = "sha256:3d3dcca4cb3ad22bdf47d508d523c4c330f91fce3fde6cdeae17261cbe8f66a6" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, -] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, -] - -[[package]] -name = "requests" -version = "2.31.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574 }, -] - -[[package]] -name = "requests-oauthlib" -version = "1.3.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/52/531ef197b426646f26b53815a7d2a67cb7a331ef098bb276db26a68ac49f/requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a", size = 52027 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/bb/5deac77a9af870143c684ab46a7934038a53eb4aa975bc0687ed6ca2c610/requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", size = 23892 }, -] - -[[package]] -name = "ruff" -version = "0.9.5" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/74/6c359f6b9ed85b88df6ef31febce18faeb852f6c9855651dfb1184a46845/ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c", size = 3634177 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/4b/82b7c9ac874e72b82b19fd7eab57d122e2df44d2478d90825854f9232d02/ruff-0.9.5-py3-none-linux_armv6l.whl", hash = "sha256:d466d2abc05f39018d53f681fa1c0ffe9570e6d73cde1b65d23bb557c846f442", size = 11681264 }, - { url = "https://files.pythonhosted.org/packages/27/5c/f5ae0a9564e04108c132e1139d60491c0abc621397fe79a50b3dc0bd704b/ruff-0.9.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38840dbcef63948657fa7605ca363194d2fe8c26ce8f9ae12eee7f098c85ac8a", size = 11657554 }, - { url = "https://files.pythonhosted.org/packages/2a/83/c6926fa3ccb97cdb3c438bb56a490b395770c750bf59f9bc1fe57ae88264/ruff-0.9.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d56ba06da53536b575fbd2b56517f6f95774ff7be0f62c80b9e67430391eeb36", size = 11088959 }, - { url = "https://files.pythonhosted.org/packages/af/a7/42d1832b752fe969ffdbfcb1b4cb477cb271bed5835110fb0a16ef31ab81/ruff-0.9.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7cb2a01da08244c50b20ccfaeb5972e4228c3c3a1989d3ece2bc4b1f996001", size = 11902041 }, - { url = "https://files.pythonhosted.org/packages/53/cf/1fffa09fb518d646f560ccfba59f91b23c731e461d6a4dedd21a393a1ff1/ruff-0.9.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96d5c76358419bc63a671caac70c18732d4fd0341646ecd01641ddda5c39ca0b", size = 11421069 }, - { url = "https://files.pythonhosted.org/packages/09/27/bb8f1b7304e2a9431f631ae7eadc35550fe0cf620a2a6a0fc4aa3d736f94/ruff-0.9.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:deb8304636ed394211f3a6d46c0e7d9535b016f53adaa8340139859b2359a070", size = 12625095 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/ab00bc9d3df35a5f1b64f5117458160a009f93ae5caf65894ebb63a1842d/ruff-0.9.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df455000bf59e62b3e8c7ba5ed88a4a2bc64896f900f311dc23ff2dc38156440", size = 13257797 }, - { url = "https://files.pythonhosted.org/packages/88/81/c639a082ae6d8392bc52256058ec60f493c6a4d06d5505bccface3767e61/ruff-0.9.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de92170dfa50c32a2b8206a647949590e752aca8100a0f6b8cefa02ae29dce80", size = 12763793 }, - { url = "https://files.pythonhosted.org/packages/b3/d0/0a3d8f56d1e49af466dc770eeec5c125977ba9479af92e484b5b0251ce9c/ruff-0.9.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d28532d73b1f3f627ba88e1456f50748b37f3a345d2be76e4c653bec6c3e393", size = 14386234 }, - { url = "https://files.pythonhosted.org/packages/04/70/e59c192a3ad476355e7f45fb3a87326f5219cc7c472e6b040c6c6595c8f0/ruff-0.9.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c746d7d1df64f31d90503ece5cc34d7007c06751a7a3bbeee10e5f2463d52d2", size = 12437505 }, - { url = "https://files.pythonhosted.org/packages/55/4e/3abba60a259d79c391713e7a6ccabf7e2c96e5e0a19100bc4204f1a43a51/ruff-0.9.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11417521d6f2d121fda376f0d2169fb529976c544d653d1d6044f4c5562516ee", size = 11884799 }, - { url = "https://files.pythonhosted.org/packages/a3/db/b0183a01a9f25b4efcae919c18fb41d32f985676c917008620ad692b9d5f/ruff-0.9.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b9d71c3879eb32de700f2f6fac3d46566f644a91d3130119a6378f9312a38e1", size = 11527411 }, - { url = "https://files.pythonhosted.org/packages/0a/e4/3ebfcebca3dff1559a74c6becff76e0b64689cea02b7aab15b8b32ea245d/ruff-0.9.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2e36c61145e70febcb78483903c43444c6b9d40f6d2f800b5552fec6e4a7bb9a", size = 12078868 }, - { url = "https://files.pythonhosted.org/packages/ec/b2/5ab808833e06c0a1b0d046a51c06ec5687b73c78b116e8d77687dc0cd515/ruff-0.9.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2f71d09aeba026c922aa7aa19a08d7bd27c867aedb2f74285a2639644c1c12f5", size = 12524374 }, - { url = "https://files.pythonhosted.org/packages/e0/51/1432afcc3b7aa6586c480142caae5323d59750925c3559688f2a9867343f/ruff-0.9.5-py3-none-win32.whl", hash = "sha256:134f958d52aa6fdec3b294b8ebe2320a950d10c041473c4316d2e7d7c2544723", size = 9853682 }, - { url = "https://files.pythonhosted.org/packages/b7/ad/c7a900591bd152bb47fc4882a27654ea55c7973e6d5d6396298ad3fd6638/ruff-0.9.5-py3-none-win_amd64.whl", hash = "sha256:78cc6067f6d80b6745b67498fb84e87d32c6fc34992b52bffefbdae3442967d6", size = 10865744 }, - { url = "https://files.pythonhosted.org/packages/75/d9/fde7610abd53c0c76b6af72fc679cb377b27c617ba704e25da834e0a0608/ruff-0.9.5-py3-none-win_arm64.whl", hash = "sha256:18a29f1a005bddb229e580795627d297dfa99f16b30c7039e73278cf6b5f9fa9", size = 10064595 }, -] - -[[package]] -name = "setuptools" -version = "75.8.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, -] - -[[package]] -name = "shapely" -version = "1.8.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/0c/454c80f71bd5ece52fb06d2905bf956b9122f4be539d5ae5df4b10dd3e14/Shapely-1.8.0.tar.gz", hash = "sha256:f5307ee14ba4199f8bbcf6532ca33064661c1433960c432c84f0daa73b47ef9c", size = 278344 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/39/563e6598e74db4e2ac184e980b9c71cbccc2caaa7e40b3df6e63c863fd57/Shapely-1.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c5632cedea6d815b61eb4c264da1c3f24a8ce2ceba2f74e30fba340ca230563", size = 1123044 }, - { url = "https://files.pythonhosted.org/packages/11/24/5e84be7dedaffc2d5a94c1266fc2420813f629500da4d244b6096448a59e/Shapely-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4ce1f18a0c9bb6b483c73bd7a0eb3a5e90676bcc29b9c27120236e662195c9d", size = 1185027 }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, -] - -[[package]] -name = "smmap" -version = "5.0.2" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, -] - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, -] - -[[package]] -name = "stevedore" -version = "5.4.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "pbr" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4a/e9/4eedccff8332cc40cc60ddd3b28d4c3e255ee7e9c65679fa4533ab98f598/stevedore-5.4.0.tar.gz", hash = "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d", size = 513899 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/73/d0091d22a65b55e8fb6aca7b3b6713b5a261dd01cec4cfd28ed127ac0cfc/stevedore-5.4.0-py3-none-any.whl", hash = "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857", size = 49534 }, -] - -[[package]] -name = "structlog" -version = "24.4.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/a3/e811a94ac3853826805253c906faa99219b79951c7d58605e89c79e65768/structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4", size = 1348634 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610", size = 67180 }, -] - -[[package]] -name = "structlog-gcp" -version = "0.3.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "structlog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/02/d295e618debade6ad54e5a6edabd719436f6f28c8e1089f05b54af2a3f70/structlog_gcp-0.3.0.tar.gz", hash = "sha256:ab88b215386ff0209b80ef2f73bd60d9ce56c5a5fc07e5da9f1947cbc40a0c04", size = 113017 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/22/9a2e089c03f666ba94fe2be9863b020f8c3b9e71997ff4de55ba750a2c03/structlog_gcp-0.3.0-py3-none-any.whl", hash = "sha256:767c9e7a545104e73230913ac9c92f78baffaa5b850c7780fdee5c8955fbc253", size = 9275 }, -] - -[[package]] -name = "thrift" -version = "0.21.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/1c/418058b4750176b638ab60b4d5a554a2969dcd2363ae458519d0f747adff/thrift-0.21.0.tar.gz", hash = "sha256:5e6f7c50f936ebfa23e924229afc95eb219f8c8e5a83202dd4a391244803e402", size = 62509 } - -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, -] - -[[package]] -name = "tomli" -version = "2.2.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, -] - -[[package]] -name = "urllib3" -version = "2.3.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, -] - -[[package]] -name = "vyper-config" -version = "1.2.1" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -dependencies = [ - { name = "distconfig3" }, - { name = "pyyaml" }, - { name = "toml" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/2f/3371501c32bd27cf4f1bbda2f51d22df7e191e2475628b37eeb6b169c8f2/vyper-config-1.2.1.tar.gz", hash = "sha256:d7e6ecaf363539caab4e3cb85d55a98df00a42547965b2703d1989302d30ec57", size = 22988 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/7e/d7bc18e84f7c45d7cc1857a01237f85220b953c4690768d9fb83702c051b/vyper_config-1.2.1-py3-none-any.whl", hash = "sha256:a9907a5707602e7c289f964498a0d5d6e477e8005a9e3797bdab90771ec421c1", size = 15989 }, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390 }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389 }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020 }, - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902 }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380 }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, -] - -[[package]] -name = "wrapt" -version = "1.17.2" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307 }, - { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486 }, - { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777 }, - { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314 }, - { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947 }, - { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778 }, - { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716 }, - { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548 }, - { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334 }, - { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427 }, - { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774 }, - { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 }, - { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 }, - { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 }, - { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 }, - { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 }, - { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 }, - { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 }, - { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 }, - { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 }, - { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 }, - { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 }, - { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 }, - { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 }, - { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 }, - { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, - { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, - { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, - { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, - { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, - { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, - { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 }, - { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 }, - { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, - { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, - { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, - { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, - { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, - { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, - { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, - { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, - { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, - { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, - { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, - { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, - { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, - { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, - { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, - { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, - { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, - { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, - { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, - { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, - { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, - { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, - { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, -] - -[[package]] -name = "zipp" -version = "3.21.0" -source = { registry = "https://git.o-c.space/api/v4/projects/689/packages/pypi/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, -] +# This file was autogenerated by uv via the following command: +# uv pip compile -o uv.lock pyproject.toml +annotated-types==0.7.0 + # via pydantic +certifi==2025.1.31 + # via requests +charset-normalizer==3.4.1 + # via requests +idna==3.10 + # via requests +oauthlib==3.2.0 + # via + # datacosmos (pyproject.toml) + # requests-oauthlib +pydantic==2.10.6 + # via + # datacosmos (pyproject.toml) + # pydantic-settings +pydantic-core==2.27.2 + # via pydantic +pydantic-settings==2.7.1 + # via datacosmos (pyproject.toml) +pystac==1.12.1 + # via datacosmos (pyproject.toml) +python-dateutil==2.9.0.post0 + # via pystac +python-dotenv==1.0.1 + # via pydantic-settings +requests==2.31.0 + # via + # datacosmos (pyproject.toml) + # requests-oauthlib +requests-oauthlib==1.3.1 + # via datacosmos (pyproject.toml) +shapely==1.8.0 + # via datacosmos (pyproject.toml) +six==1.17.0 + # via python-dateutil +typing-extensions==4.12.2 + # via + # pydantic + # pydantic-core +urllib3==2.3.0 + # via requests From d55cfdb1f2c8f24eedc26bcf8a2176e00d231670 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 14 Feb 2025 10:21:38 +0000 Subject: [PATCH 128/135] put installation of geos back --- .github/workflows/main.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 0ae32d6..a2b4edb 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -14,6 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -31,6 +33,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -47,6 +51,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -63,6 +69,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -80,6 +88,8 @@ jobs: needs: [bandit, cognitive, lint, pydocstyle] steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -98,6 +108,8 @@ jobs: if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 + - name: Install GEOS + run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment From 569ece214598c24b148d3fe80310437280fa8063 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Tue, 11 Feb 2025 16:40:03 +0000 Subject: [PATCH 129/135] Rename client with datacosmos_client; Move current stac api functionalities to a item dedicated folder --- datacosmos/{client.py => datacosmos_client.py} | 0 datacosmos/stac/item/__init__.py | 4 ++++ .../stac/{stac_client.py => item/item_client.py} | 8 +++++++- datacosmos/stac/{ => item}/models/item_update.py | 0 datacosmos/stac/{ => item}/models/search_parameters.py | 0 .../test_client_authentication.py | 6 +++--- .../test_client_delete_request.py | 6 ++++-- .../test_client_get_request.py | 6 ++++-- .../test_client_patch_request.py | 6 ++++-- .../test_client_post_request.py | 6 ++++-- .../test_client_put_request.py | 6 ++++-- .../test_client_token_refreshing.py | 6 ++++-- .../item_client}/test_create_item.py | 8 ++++---- .../item_client}/test_delete_item.py | 8 ++++---- .../item_client}/test_fetch_collection_items.py | 10 +++++----- .../item_client}/test_fetch_item.py | 8 ++++---- .../item_client}/test_search_items.py | 10 +++++----- .../item_client}/test_update_item.py | 10 +++++----- .../stac/{ => item}/models/test_item_update.py | 2 +- 19 files changed, 66 insertions(+), 44 deletions(-) rename datacosmos/{client.py => datacosmos_client.py} (100%) create mode 100644 datacosmos/stac/item/__init__.py rename datacosmos/stac/{stac_client.py => item/item_client.py} (94%) rename datacosmos/stac/{ => item}/models/item_update.py (100%) rename datacosmos/stac/{ => item}/models/search_parameters.py (100%) rename tests/unit/datacosmos/{client => datacosmos_client}/test_client_authentication.py (93%) rename tests/unit/datacosmos/{client => datacosmos_client}/test_client_delete_request.py (87%) rename tests/unit/datacosmos/{client => datacosmos_client}/test_client_get_request.py (88%) rename tests/unit/datacosmos/{client => datacosmos_client}/test_client_patch_request.py (89%) rename tests/unit/datacosmos/{client => datacosmos_client}/test_client_post_request.py (89%) rename tests/unit/datacosmos/{client => datacosmos_client}/test_client_put_request.py (89%) rename tests/unit/datacosmos/{client => datacosmos_client}/test_client_token_refreshing.py (91%) rename tests/unit/datacosmos/stac/{stac_client => item/item_client}/test_create_item.py (88%) rename tests/unit/datacosmos/stac/{stac_client => item/item_client}/test_delete_item.py (83%) rename tests/unit/datacosmos/stac/{stac_client => item/item_client}/test_fetch_collection_items.py (83%) rename tests/unit/datacosmos/stac/{stac_client => item/item_client}/test_fetch_item.py (88%) rename tests/unit/datacosmos/stac/{stac_client => item/item_client}/test_search_items.py (86%) rename tests/unit/datacosmos/stac/{stac_client => item/item_client}/test_update_item.py (86%) rename tests/unit/datacosmos/stac/{ => item}/models/test_item_update.py (96%) diff --git a/datacosmos/client.py b/datacosmos/datacosmos_client.py similarity index 100% rename from datacosmos/client.py rename to datacosmos/datacosmos_client.py diff --git a/datacosmos/stac/item/__init__.py b/datacosmos/stac/item/__init__.py new file mode 100644 index 0000000..05bd8ad --- /dev/null +++ b/datacosmos/stac/item/__init__.py @@ -0,0 +1,4 @@ +"""STAC package for interacting with items from the STAC API, providing query and fetch functionalities. + +It enables interaction with items from the STAC using an authenticated Datacosmos client. +""" diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/item/item_client.py similarity index 94% rename from datacosmos/stac/stac_client.py rename to datacosmos/stac/item/item_client.py index ea7f0f3..a8f8426 100644 --- a/datacosmos/stac/stac_client.py +++ b/datacosmos/stac/item/item_client.py @@ -7,14 +7,20 @@ from pystac import Item +<<<<<<< HEAD:datacosmos/stac/stac_client.py from datacosmos.client import DatacosmosClient from datacosmos.exceptions.datacosmos_exception import DatacosmosException from datacosmos.stac.models.item_update import ItemUpdate from datacosmos.stac.models.search_parameters import SearchParameters from datacosmos.utils.http_response import check_api_response +======= +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.item.models.item_update import ItemUpdate +from datacosmos.stac.item.models.search_parameters import SearchParameters +>>>>>>> 77a15fc (Rename client with datacosmos_client; Move current stac api functionalities to a item dedicated folder):datacosmos/stac/item/item_client.py -class STACClient: +class ItemClient: """Client for interacting with the STAC API.""" def __init__(self, client: DatacosmosClient): diff --git a/datacosmos/stac/models/item_update.py b/datacosmos/stac/item/models/item_update.py similarity index 100% rename from datacosmos/stac/models/item_update.py rename to datacosmos/stac/item/models/item_update.py diff --git a/datacosmos/stac/models/search_parameters.py b/datacosmos/stac/item/models/search_parameters.py similarity index 100% rename from datacosmos/stac/models/search_parameters.py rename to datacosmos/stac/item/models/search_parameters.py diff --git a/tests/unit/datacosmos/client/test_client_authentication.py b/tests/unit/datacosmos/datacosmos_client/test_client_authentication.py similarity index 93% rename from tests/unit/datacosmos/client/test_client_authentication.py rename to tests/unit/datacosmos/datacosmos_client/test_client_authentication.py index 1e536f1..69ea619 100644 --- a/tests/unit/datacosmos/client/test_client_authentication.py +++ b/tests/unit/datacosmos/datacosmos_client/test_client_authentication.py @@ -6,7 +6,7 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient +from datacosmos.datacosmos_client import DatacosmosClient @pytest.mark.usefixtures("mock_fetch_token", "mock_auth_client") @@ -16,7 +16,7 @@ class TestClientAuthentication: @pytest.fixture def mock_fetch_token(self): """Fixture to mock OAuth2 token fetch.""" - with patch("datacosmos.client.OAuth2Session.fetch_token") as mock: + with patch("datacosmos.datacosmos_client.OAuth2Session.fetch_token") as mock: mock.return_value = { "access_token": "mock-access-token", "expires_in": 3600, @@ -27,7 +27,7 @@ def mock_fetch_token(self): def mock_auth_client(self, mock_fetch_token): """Fixture to mock the authentication client initialization.""" with patch( - "datacosmos.client.DatacosmosClient._authenticate_and_initialize_client", + "datacosmos.datacosmos_client.DatacosmosClient._authenticate_and_initialize_client", autospec=True, ) as mock: diff --git a/tests/unit/datacosmos/client/test_client_delete_request.py b/tests/unit/datacosmos/datacosmos_client/test_client_delete_request.py similarity index 87% rename from tests/unit/datacosmos/client/test_client_delete_request.py rename to tests/unit/datacosmos/datacosmos_client/test_client_delete_request.py index 6b61f0b..8d61786 100644 --- a/tests/unit/datacosmos/client/test_client_delete_request.py +++ b/tests/unit/datacosmos/datacosmos_client/test_client_delete_request.py @@ -2,10 +2,12 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient +from datacosmos.datacosmos_client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.datacosmos_client.DatacosmosClient._authenticate_and_initialize_client" +) def test_delete_request(mock_auth_client): """Test that the client performs a DELETE request correctly.""" # Mock the HTTP client diff --git a/tests/unit/datacosmos/client/test_client_get_request.py b/tests/unit/datacosmos/datacosmos_client/test_client_get_request.py similarity index 88% rename from tests/unit/datacosmos/client/test_client_get_request.py rename to tests/unit/datacosmos/datacosmos_client/test_client_get_request.py index 4963756..50e22f9 100644 --- a/tests/unit/datacosmos/client/test_client_get_request.py +++ b/tests/unit/datacosmos/datacosmos_client/test_client_get_request.py @@ -2,10 +2,12 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient +from datacosmos.datacosmos_client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.datacosmos_client.DatacosmosClient._authenticate_and_initialize_client" +) def test_client_get_request(mock_auth_client): """Test that the client performs a GET request correctly.""" # Mock the HTTP client diff --git a/tests/unit/datacosmos/client/test_client_patch_request.py b/tests/unit/datacosmos/datacosmos_client/test_client_patch_request.py similarity index 89% rename from tests/unit/datacosmos/client/test_client_patch_request.py rename to tests/unit/datacosmos/datacosmos_client/test_client_patch_request.py index fb92c52..3a0d528 100644 --- a/tests/unit/datacosmos/client/test_client_patch_request.py +++ b/tests/unit/datacosmos/datacosmos_client/test_client_patch_request.py @@ -2,10 +2,12 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient +from datacosmos.datacosmos_client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.datacosmos_client.DatacosmosClient._authenticate_and_initialize_client" +) def test_patch_request(mock_auth_client): """Test that the client performs a PATCH request correctly.""" # Mock the HTTP client diff --git a/tests/unit/datacosmos/client/test_client_post_request.py b/tests/unit/datacosmos/datacosmos_client/test_client_post_request.py similarity index 89% rename from tests/unit/datacosmos/client/test_client_post_request.py rename to tests/unit/datacosmos/datacosmos_client/test_client_post_request.py index 5023602..3235eb1 100644 --- a/tests/unit/datacosmos/client/test_client_post_request.py +++ b/tests/unit/datacosmos/datacosmos_client/test_client_post_request.py @@ -2,10 +2,12 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient +from datacosmos.datacosmos_client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.datacosmos_client.DatacosmosClient._authenticate_and_initialize_client" +) def test_post_request(mock_auth_client): """Test that the client performs a POST request correctly.""" # Mock the HTTP client diff --git a/tests/unit/datacosmos/client/test_client_put_request.py b/tests/unit/datacosmos/datacosmos_client/test_client_put_request.py similarity index 89% rename from tests/unit/datacosmos/client/test_client_put_request.py rename to tests/unit/datacosmos/datacosmos_client/test_client_put_request.py index c894fc0..09e0335 100644 --- a/tests/unit/datacosmos/client/test_client_put_request.py +++ b/tests/unit/datacosmos/datacosmos_client/test_client_put_request.py @@ -2,10 +2,12 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient +from datacosmos.datacosmos_client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.datacosmos_client.DatacosmosClient._authenticate_and_initialize_client" +) def test_put_request(mock_auth_client): """Test that the client performs a PUT request correctly.""" # Mock the HTTP client diff --git a/tests/unit/datacosmos/client/test_client_token_refreshing.py b/tests/unit/datacosmos/datacosmos_client/test_client_token_refreshing.py similarity index 91% rename from tests/unit/datacosmos/client/test_client_token_refreshing.py rename to tests/unit/datacosmos/datacosmos_client/test_client_token_refreshing.py index ad44784..0585ff7 100644 --- a/tests/unit/datacosmos/client/test_client_token_refreshing.py +++ b/tests/unit/datacosmos/datacosmos_client/test_client_token_refreshing.py @@ -3,10 +3,12 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient +from datacosmos.datacosmos_client import DatacosmosClient -@patch("datacosmos.client.DatacosmosClient._authenticate_and_initialize_client") +@patch( + "datacosmos.datacosmos_client.DatacosmosClient._authenticate_and_initialize_client" +) def test_client_token_refreshing(mock_auth_client): """Test that the client refreshes the token when it expires.""" # Mock the HTTP client returned by _authenticate_and_initialize_client diff --git a/tests/unit/datacosmos/stac/stac_client/test_create_item.py b/tests/unit/datacosmos/stac/item/item_client/test_create_item.py similarity index 88% rename from tests/unit/datacosmos/stac/stac_client/test_create_item.py rename to tests/unit/datacosmos/stac/item/item_client/test_create_item.py index 16ac088..c90992e 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_create_item.py +++ b/tests/unit/datacosmos/stac/item/item_client/test_create_item.py @@ -4,13 +4,13 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient -from datacosmos.stac.stac_client import STACClient +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.item.item_client import ItemClient @patch("requests_oauthlib.OAuth2Session.fetch_token") @patch.object(DatacosmosClient, "post") -@patch("datacosmos.stac.stac_client.check_api_response") +@patch("datacosmos.stac.item.item_client.check_api_response") def test_create_item(mock_check_api_response, mock_post, mock_fetch_token): """Test creating a new STAC item.""" mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} @@ -38,7 +38,7 @@ def test_create_item(mock_check_api_response, mock_post, mock_fetch_token): ) ) client = DatacosmosClient(config=config) - stac_client = STACClient(client) + stac_client = ItemClient(client) item = Item.from_dict(mock_response.json()) diff --git a/tests/unit/datacosmos/stac/stac_client/test_delete_item.py b/tests/unit/datacosmos/stac/item/item_client/test_delete_item.py similarity index 83% rename from tests/unit/datacosmos/stac/stac_client/test_delete_item.py rename to tests/unit/datacosmos/stac/item/item_client/test_delete_item.py index d014220..d75240a 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_delete_item.py +++ b/tests/unit/datacosmos/stac/item/item_client/test_delete_item.py @@ -2,13 +2,13 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient -from datacosmos.stac.stac_client import STACClient +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.item.item_client import ItemClient @patch("requests_oauthlib.OAuth2Session.fetch_token") @patch.object(DatacosmosClient, "delete") -@patch("datacosmos.stac.stac_client.check_api_response") +@patch("datacosmos.stac.item.item_client.check_api_response") def test_delete_item(mock_check_api_response, mock_delete, mock_fetch_token): """Test deleting a STAC item.""" mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} @@ -28,7 +28,7 @@ def test_delete_item(mock_check_api_response, mock_delete, mock_fetch_token): ) client = DatacosmosClient(config=config) - stac_client = STACClient(client) + stac_client = ItemClient(client) stac_client.delete_item("item-1", "test-collection") diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py b/tests/unit/datacosmos/stac/item/item_client/test_fetch_collection_items.py similarity index 83% rename from tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py rename to tests/unit/datacosmos/stac/item/item_client/test_fetch_collection_items.py index 11b8bc3..b81f43b 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_collection_items.py +++ b/tests/unit/datacosmos/stac/item/item_client/test_fetch_collection_items.py @@ -2,13 +2,13 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient -from datacosmos.stac.stac_client import STACClient +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.item.item_client import ItemClient @patch("requests_oauthlib.OAuth2Session.fetch_token") -@patch("datacosmos.stac.stac_client.check_api_response") -@patch.object(STACClient, "search_items") +@patch("datacosmos.stac.item.item_client.check_api_response") +@patch.object(ItemClient, "search_items") def test_fetch_collection_items( mock_search_items, mock_check_api_response, mock_fetch_token ): @@ -33,7 +33,7 @@ def test_fetch_collection_items( ) client = DatacosmosClient(config=config) - stac_client = STACClient(client) + stac_client = ItemClient(client) results = list(stac_client.fetch_collection_items("test-collection")) diff --git a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py b/tests/unit/datacosmos/stac/item/item_client/test_fetch_item.py similarity index 88% rename from tests/unit/datacosmos/stac/stac_client/test_fetch_item.py rename to tests/unit/datacosmos/stac/item/item_client/test_fetch_item.py index 35c2cbb..fa9edeb 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_fetch_item.py +++ b/tests/unit/datacosmos/stac/item/item_client/test_fetch_item.py @@ -2,12 +2,12 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient -from datacosmos.stac.stac_client import STACClient +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.item.item_client import ItemClient @patch("requests_oauthlib.OAuth2Session.fetch_token") -@patch("datacosmos.stac.stac_client.check_api_response") +@patch("datacosmos.stac.item.item_client.check_api_response") @patch.object(DatacosmosClient, "get") def test_fetch_item(mock_get, mock_check_api_response, mock_fetch_token): """Test fetching a single STAC item by ID.""" @@ -40,7 +40,7 @@ def test_fetch_item(mock_get, mock_check_api_response, mock_fetch_token): ) client = DatacosmosClient(config=config) - stac_client = STACClient(client) + stac_client = ItemClient(client) item = stac_client.fetch_item("item-1", "test-collection") diff --git a/tests/unit/datacosmos/stac/stac_client/test_search_items.py b/tests/unit/datacosmos/stac/item/item_client/test_search_items.py similarity index 86% rename from tests/unit/datacosmos/stac/stac_client/test_search_items.py rename to tests/unit/datacosmos/stac/item/item_client/test_search_items.py index 572a2cf..15b16c9 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_search_items.py +++ b/tests/unit/datacosmos/stac/item/item_client/test_search_items.py @@ -2,13 +2,13 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient -from datacosmos.stac.models.search_parameters import SearchParameters -from datacosmos.stac.stac_client import STACClient +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.item.item_client import ItemClient +from datacosmos.stac.item.models.search_parameters import SearchParameters @patch("requests_oauthlib.OAuth2Session.fetch_token") -@patch("datacosmos.stac.stac_client.check_api_response") +@patch("datacosmos.stac.item.item_client.check_api_response") @patch.object(DatacosmosClient, "post") def test_search_items(mock_post, mock_check_api_response, mock_fetch_token): """Test searching STAC items with filters and pagination.""" @@ -46,7 +46,7 @@ def test_search_items(mock_post, mock_check_api_response, mock_fetch_token): ) client = DatacosmosClient(config=config) - stac_client = STACClient(client) + stac_client = ItemClient(client) parameters = SearchParameters(collections=["test-collection"]) results = list(stac_client.search_items(parameters)) diff --git a/tests/unit/datacosmos/stac/stac_client/test_update_item.py b/tests/unit/datacosmos/stac/item/item_client/test_update_item.py similarity index 86% rename from tests/unit/datacosmos/stac/stac_client/test_update_item.py rename to tests/unit/datacosmos/stac/item/item_client/test_update_item.py index de466c1..e845183 100644 --- a/tests/unit/datacosmos/stac/stac_client/test_update_item.py +++ b/tests/unit/datacosmos/stac/item/item_client/test_update_item.py @@ -2,14 +2,14 @@ from config.config import Config from config.models.m2m_authentication_config import M2MAuthenticationConfig -from datacosmos.client import DatacosmosClient -from datacosmos.stac.models.item_update import ItemUpdate -from datacosmos.stac.stac_client import STACClient +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.item.item_client import ItemClient +from datacosmos.stac.item.models.item_update import ItemUpdate @patch("requests_oauthlib.OAuth2Session.fetch_token") @patch.object(DatacosmosClient, "patch") -@patch("datacosmos.stac.stac_client.check_api_response") +@patch("datacosmos.stac.item.item_client.check_api_response") def test_update_item(mock_check_api_response, mock_patch, mock_fetch_token): """Test updating an existing STAC item.""" mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} @@ -37,7 +37,7 @@ def test_update_item(mock_check_api_response, mock_patch, mock_fetch_token): ) ) client = DatacosmosClient(config=config) - stac_client = STACClient(client) + stac_client = ItemClient(client) update_data = ItemUpdate( properties={"new_property": "value", "datetime": "2023-12-01T12:00:00Z"} diff --git a/tests/unit/datacosmos/stac/models/test_item_update.py b/tests/unit/datacosmos/stac/item/models/test_item_update.py similarity index 96% rename from tests/unit/datacosmos/stac/models/test_item_update.py rename to tests/unit/datacosmos/stac/item/models/test_item_update.py index d1ff225..6338ea7 100644 --- a/tests/unit/datacosmos/stac/models/test_item_update.py +++ b/tests/unit/datacosmos/stac/item/models/test_item_update.py @@ -1,6 +1,6 @@ import pytest -from datacosmos.stac.models.item_update import ItemUpdate +from datacosmos.stac.item.models.item_update import ItemUpdate class TestItemUpdate: From 5b0e4f64b88a3e84815dbbc6398bb66cb69b4dde Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 12 Feb 2025 10:50:30 +0000 Subject: [PATCH 130/135] Add client for interacting with collections from the STAC API --- datacosmos/stac/collection/__init__.py | 4 + .../stac/collection/collection_client.py | 149 ++++++++++++++++++ datacosmos/stac/collection/models/__init__.py | 1 + .../collection/models/collection_update.py | 46 ++++++ datacosmos/stac/item/item_client.py | 2 +- datacosmos/stac/item/models/__init__.py | 1 + datacosmos/stac/stac_client.py | 12 ++ 7 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 datacosmos/stac/collection/__init__.py create mode 100644 datacosmos/stac/collection/collection_client.py create mode 100644 datacosmos/stac/collection/models/__init__.py create mode 100644 datacosmos/stac/collection/models/collection_update.py create mode 100644 datacosmos/stac/item/models/__init__.py create mode 100644 datacosmos/stac/stac_client.py diff --git a/datacosmos/stac/collection/__init__.py b/datacosmos/stac/collection/__init__.py new file mode 100644 index 0000000..f6642d5 --- /dev/null +++ b/datacosmos/stac/collection/__init__.py @@ -0,0 +1,4 @@ +"""STAC package for interacting with collections from the STAC API, providing query and fetch functionalities. + +It enables interaction with collections from the STAC using an authenticated Datacosmos client. +""" diff --git a/datacosmos/stac/collection/collection_client.py b/datacosmos/stac/collection/collection_client.py new file mode 100644 index 0000000..e9396e0 --- /dev/null +++ b/datacosmos/stac/collection/collection_client.py @@ -0,0 +1,149 @@ +"""Handles operations related to STAC collections.""" + +from typing import Generator, Optional + +from common.sdk.http_response import check_api_response +from pystac import Collection, Extent, SpatialExtent, TemporalExtent +from pystac.utils import str_to_datetime + +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.collection.models.collection_update import CollectionUpdate + + +class CollectionClient: + """Handles operations related to STAC collections.""" + + def __init__(self, client: DatacosmosClient): + """Initialize the CollectionClient with a DatacosmosClient.""" + self.client = client + self.base_url = client.config.stac.as_domain_url() + + def fetch_collection(self, collection_id: str) -> Collection: + """Fetch details of an existing STAC collection.""" + url = self.base_url.with_suffix(f"/collections/{collection_id}") + response = self.client.get(url) + check_api_response(response) + return Collection.from_dict(response.json()) + + def create_collection(self, collection: Collection) -> None: + """Create a new STAC collection. + + Args: + collection (Collection): The STAC collection to create. + + Raises: + InvalidRequest: If the collection data is malformed. + """ + if isinstance(collection.extent, dict): + spatial_data = collection.extent.get("spatial", {}).get("bbox", [[]]) + temporal_data = collection.extent.get("temporal", {}).get("interval", [[]]) + + # Convert string timestamps to datetime objects + parsed_temporal = [] + for interval in temporal_data: + start = str_to_datetime(interval[0]) if interval[0] else None + end = ( + str_to_datetime(interval[1]) + if len(interval) > 1 and interval[1] + else None + ) + parsed_temporal.append([start, end]) + + collection.extent = Extent( + spatial=SpatialExtent(spatial_data), + temporal=TemporalExtent(parsed_temporal), + ) + + url = self.base_url.with_suffix("/collections") + response = self.client.post(url, json=collection.to_dict()) + check_api_response(response) + + def update_collection( + self, collection_id: str, update_data: CollectionUpdate + ) -> None: + """Update an existing STAC collection.""" + url = self.base_url.with_suffix(f"/collections/{collection_id}") + response = self.client.patch( + url, json=update_data.model_dump(by_alias=True, exclude_none=True) + ) + check_api_response(response) + + def delete_collection(self, collection_id: str) -> None: + """Delete a STAC collection by its ID.""" + url = self.base_url.with_suffix(f"/collections/{collection_id}") + response = self.client.delete(url) + check_api_response(response) + + def fetch_all_collections(self) -> Generator[Collection, None, None]: + """Fetch all STAC collections with pagination support.""" + url = self.base_url.with_suffix("/collections") + params = {"limit": 10} + + while True: + data = self._fetch_collections_page(url, params) + yield from self._parse_collections(data) + + next_cursor = self._get_next_pagination_cursor(data) + if not next_cursor: + break + + params["cursor"] = next_cursor + + def _fetch_collections_page(self, url: str, params: dict) -> dict: + """Fetch a single page of collections from the API.""" + response = self.client.get(url, params=params) + check_api_response(response) + + data = response.json() + + if isinstance(data, list): + return {"collections": data} + + return data + + def _parse_collections(self, data: dict) -> Generator[Collection, None, None]: + """Convert API response data to STAC Collection objects, ensuring required fields exist.""" + return ( + Collection.from_dict( + { + **collection, + "type": collection.get("type", "Collection"), + "id": collection.get("id", ""), + "stac_version": collection.get("stac_version", "1.0.0"), + "extent": collection.get( + "extent", + {"spatial": {"bbox": []}, "temporal": {"interval": []}}, + ), + "links": collection.get("links", []) or [], + "properties": collection.get("properties", {}), + } + ) + for collection in data.get("collections", []) + if collection.get("type") == "Collection" + ) + + def _get_next_pagination_cursor(self, data: dict) -> Optional[str]: + """Extract the next pagination token from the response.""" + next_href = self._get_next_link(data) + return self._extract_pagination_token(next_href) if next_href else None + + def _get_next_link(self, data: dict) -> Optional[str]: + """Extract the next page link from the response.""" + next_link = next( + (link for link in data.get("links", []) if link.get("rel") == "next"), None + ) + return next_link.get("href", "") if next_link else None + + def _extract_pagination_token(self, next_href: str) -> Optional[str]: + """Extract the pagination token from the next link URL. + + Args: + next_href (str): The next page URL. + + Returns: + Optional[str]: The extracted token, or None if parsing fails. + """ + try: + return next_href.split("?")[1].split("=")[-1] + except (IndexError, AttributeError): + raise InvalidRequest(f"Failed to parse pagination token from {next_href}") diff --git a/datacosmos/stac/collection/models/__init__.py b/datacosmos/stac/collection/models/__init__.py new file mode 100644 index 0000000..f4c32e1 --- /dev/null +++ b/datacosmos/stac/collection/models/__init__.py @@ -0,0 +1 @@ +"""Models for the Collection Client.""" diff --git a/datacosmos/stac/collection/models/collection_update.py b/datacosmos/stac/collection/models/collection_update.py new file mode 100644 index 0000000..7d44619 --- /dev/null +++ b/datacosmos/stac/collection/models/collection_update.py @@ -0,0 +1,46 @@ +"""Represents a structured update model for STAC collections. + +Allows partial updates where only the provided fields are modified. +""" +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field +from pystac import Extent, Link, Provider, Summaries + + +class CollectionUpdate(BaseModel): + """Represents a structured update model for STAC collections. + + Allows partial updates where only the provided fields are modified. + """ + + model_config = {"arbitrary_types_allowed": True} + + title: Optional[str] = Field(None, description="Title of the STAC collection.") + description: Optional[str] = Field( + None, description="Description of the collection." + ) + keywords: Optional[List[str]] = Field( + None, description="List of keywords associated with the collection." + ) + license: Optional[str] = Field(None, description="Collection license information.") + providers: Optional[List[Provider]] = Field( + None, description="List of data providers." + ) + extent: Optional[Extent] = Field( + None, description="Spatial and temporal extent of the collection." + ) + summaries: Optional[Summaries] = Field( + None, description="Summaries for the collection." + ) + links: Optional[List[Link]] = Field( + None, description="List of links associated with the collection." + ) + + def to_dict(self) -> Dict[str, Any]: + """Convert the model into a dictionary, excluding `None` values. + + Returns: + Dict[str, Any]: Dictionary representation of the update payload. + """ + return self.model_dump(by_alias=True, exclude_none=True) diff --git a/datacosmos/stac/item/item_client.py b/datacosmos/stac/item/item_client.py index a8f8426..6d0dbb8 100644 --- a/datacosmos/stac/item/item_client.py +++ b/datacosmos/stac/item/item_client.py @@ -24,7 +24,7 @@ class ItemClient: """Client for interacting with the STAC API.""" def __init__(self, client: DatacosmosClient): - """Initialize the STACClient with a DatacosmosClient. + """Initialize the ItemClient with a DatacosmosClient. Args: client (DatacosmosClient): The authenticated Datacosmos client instance. diff --git a/datacosmos/stac/item/models/__init__.py b/datacosmos/stac/item/models/__init__.py new file mode 100644 index 0000000..1a5b17b --- /dev/null +++ b/datacosmos/stac/item/models/__init__.py @@ -0,0 +1 @@ +"""Models for the Item Client.""" diff --git a/datacosmos/stac/stac_client.py b/datacosmos/stac/stac_client.py new file mode 100644 index 0000000..fad514f --- /dev/null +++ b/datacosmos/stac/stac_client.py @@ -0,0 +1,12 @@ +"""Unified interface for STAC API, combining Item & Collection operations.""" + +from datacosmos.stac.collection.collection_client import CollectionClient +from datacosmos.stac.item.item_client import ItemClient + + +class STACClient(ItemClient, CollectionClient): + """Unified interface for STAC API, combining Item & Collection operations.""" + + def __init__(self, client): + """Initialize the STACClient with a DatacosmosClient.""" + super().__init__(client) From 9fb35f2f691aadfea051305e4bea114ee2141a7a Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Wed, 12 Feb 2025 12:43:11 +0000 Subject: [PATCH 131/135] Add unit tests for the CollectionClient --- .../test_create_collection.py | 63 ++++++++++++++++ .../test_delete_collection.py | 44 +++++++++++ .../test_fetch_collection.py | 73 +++++++++++++++++++ .../test_update_collection.py | 54 ++++++++++++++ 4 files changed, 234 insertions(+) create mode 100644 tests/unit/datacosmos/stac/collection/collection_client/test_create_collection.py create mode 100644 tests/unit/datacosmos/stac/collection/collection_client/test_delete_collection.py create mode 100644 tests/unit/datacosmos/stac/collection/collection_client/test_fetch_collection.py create mode 100644 tests/unit/datacosmos/stac/collection/collection_client/test_update_collection.py diff --git a/tests/unit/datacosmos/stac/collection/collection_client/test_create_collection.py b/tests/unit/datacosmos/stac/collection/collection_client/test_create_collection.py new file mode 100644 index 0000000..fb7caae --- /dev/null +++ b/tests/unit/datacosmos/stac/collection/collection_client/test_create_collection.py @@ -0,0 +1,63 @@ +from unittest.mock import MagicMock, patch + +from pystac import Collection, Extent, SpatialExtent, TemporalExtent +from pystac.utils import str_to_datetime + +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.collection.collection_client import CollectionClient + + +@patch("requests_oauthlib.OAuth2Session.fetch_token") +@patch.object(DatacosmosClient, "post") +@patch("datacosmos.stac.collection.collection_client.check_api_response") +def test_create_collection(mock_check_api_response, mock_post, mock_fetch_token): + """Test creating a STAC collection.""" + + mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + mock_check_api_response.return_value = None + + collection = Collection( + id="test-collection", + description="A test STAC collection", + extent=Extent( + SpatialExtent([[-180.0, -90.0, 180.0, 90.0]]), + TemporalExtent( + [ + [ + str_to_datetime("2020-01-01T00:00:00Z"), + str_to_datetime("2023-12-31T23:59:59Z"), + ] + ] + ), + ), + license="proprietary", + stac_extensions=[], + ) + + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + ) + + client = DatacosmosClient(config=config) + collection_client = CollectionClient(client) + + collection_client.create_collection(collection) + + mock_post.assert_called_once_with( + client.config.stac.as_domain_url().with_suffix("/collections"), + json=collection.to_dict(), + ) + + mock_check_api_response.assert_called_once_with(mock_response) diff --git a/tests/unit/datacosmos/stac/collection/collection_client/test_delete_collection.py b/tests/unit/datacosmos/stac/collection/collection_client/test_delete_collection.py new file mode 100644 index 0000000..9f7c93e --- /dev/null +++ b/tests/unit/datacosmos/stac/collection/collection_client/test_delete_collection.py @@ -0,0 +1,44 @@ +from unittest.mock import MagicMock, patch + +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.collection.collection_client import CollectionClient + + +@patch("requests_oauthlib.OAuth2Session.fetch_token") +@patch.object(DatacosmosClient, "delete") +@patch("datacosmos.stac.collection.collection_client.check_api_response") +def test_delete_collection(mock_check_api_response, mock_delete, mock_fetch_token): + """Test deleting a STAC collection.""" + + mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} + + mock_response = MagicMock() + mock_response.status_code = 204 + mock_delete.return_value = mock_response + mock_check_api_response.return_value = None + + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + ) + + client = DatacosmosClient(config=config) + collection_client = CollectionClient(client) + + collection_client.delete_collection("test-collection") + + mock_delete.assert_called_once_with( + client.config.stac.as_domain_url().with_suffix("/collections/test-collection") + ) + + mock_check_api_response.assert_called_once_with(mock_response) + mock_delete.assert_called_with( + collection_client.base_url.with_suffix("/collections/test-collection") + ) diff --git a/tests/unit/datacosmos/stac/collection/collection_client/test_fetch_collection.py b/tests/unit/datacosmos/stac/collection/collection_client/test_fetch_collection.py new file mode 100644 index 0000000..3daa667 --- /dev/null +++ b/tests/unit/datacosmos/stac/collection/collection_client/test_fetch_collection.py @@ -0,0 +1,73 @@ +from unittest.mock import MagicMock, patch + +from pystac import Collection +from pystac.utils import datetime_to_str + +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.collection.collection_client import CollectionClient + + +@patch("requests_oauthlib.OAuth2Session.fetch_token") +@patch.object(DatacosmosClient, "get") +@patch("datacosmos.stac.collection.collection_client.check_api_response") +def test_fetch_collection(mock_check_api_response, mock_get, mock_fetch_token): + """Test fetching a single STAC collection by ID.""" + + mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} + + mock_response = MagicMock() + mock_response.json.return_value = { + "type": "Collection", + "id": "test-collection", + "stac_version": "1.1.0", + "description": "A test STAC collection", + "license": "proprietary", + "extent": { + "spatial": {"bbox": [[-180.0, -90.0, 180.0, 90.0]]}, + "temporal": { + "interval": [["2020-01-01T00:00:00Z", "2023-12-31T23:59:59Z"]] + }, + }, + "links": [], + "stac_extensions": [], + } + mock_response.status_code = 200 + mock_get.return_value = mock_response + mock_check_api_response.return_value = None + + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + ) + + client = DatacosmosClient(config=config) + collection_client = CollectionClient(client) + + collection = collection_client.fetch_collection("test-collection") + + assert isinstance(collection, Collection) + assert collection.id == "test-collection" + assert collection.description == "A test STAC collection" + assert collection.license == "proprietary" + assert collection.extent.spatial.bboxes == [[-180.0, -90.0, 180.0, 90.0]] + + actual_temporal_intervals = [ + [datetime_to_str(interval[0]), datetime_to_str(interval[1])] + for interval in collection.extent.temporal.intervals + ] + expected_temporal_intervals = [["2020-01-01T00:00:00Z", "2023-12-31T23:59:59Z"]] + + assert actual_temporal_intervals == expected_temporal_intervals + + mock_get.assert_called_once() + mock_check_api_response.assert_called_once_with(mock_response) + mock_get.assert_called_with( + collection_client.base_url.with_suffix("/collections/test-collection") + ) diff --git a/tests/unit/datacosmos/stac/collection/collection_client/test_update_collection.py b/tests/unit/datacosmos/stac/collection/collection_client/test_update_collection.py new file mode 100644 index 0000000..3994620 --- /dev/null +++ b/tests/unit/datacosmos/stac/collection/collection_client/test_update_collection.py @@ -0,0 +1,54 @@ +from unittest.mock import MagicMock, patch + +from config.config import Config +from config.models.m2m_authentication_config import M2MAuthenticationConfig +from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.stac.collection.collection_client import CollectionClient +from datacosmos.stac.collection.models.collection_update import CollectionUpdate + + +@patch("requests_oauthlib.OAuth2Session.fetch_token") +@patch.object(DatacosmosClient, "patch") +@patch("datacosmos.stac.collection.collection_client.check_api_response") +def test_update_collection(mock_check_api_response, mock_patch, mock_fetch_token): + """Test updating a STAC collection.""" + + mock_fetch_token.return_value = {"access_token": "mock-token", "expires_in": 3600} + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_patch.return_value = mock_response + mock_check_api_response.return_value = None + + update_data = CollectionUpdate( + title="Updated Collection Title", + description="Updated description", + keywords=["updated", "collection"], + license="proprietary", + ) + + config = Config( + authentication=M2MAuthenticationConfig( + type="m2m", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://mock.token.url/oauth/token", + audience="https://mock.audience", + ) + ) + + client = DatacosmosClient(config=config) + collection_client = CollectionClient(client) + + collection_client.update_collection("test-collection", update_data) + + mock_patch.assert_called_once_with( + client.config.stac.as_domain_url().with_suffix("/collections/test-collection"), + json=update_data.model_dump(by_alias=True, exclude_none=True), + ) + + mock_check_api_response.assert_called_once_with(mock_response) + mock_patch.assert_called_with( + collection_client.base_url.with_suffix("/collections/test-collection"), + json=update_data.model_dump(by_alias=True, exclude_none=True), + ) From 508d8e2034dbd9f2823a5530b045efaf4a4890f6 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 14 Feb 2025 10:31:47 +0000 Subject: [PATCH 132/135] rebase --- README.md | 2 +- datacosmos/stac/collection/collection_client.py | 2 +- datacosmos/stac/item/item_client.py | 12 +++--------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3341326..c641097 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ pip install datacosmos The recommended way to initialize the SDK is by passing a `Config` object with authentication credentials: ```python -from datacosmos.client import DatacosmosClient +from datacosmos.datacosmos_client import DatacosmosClient from datacosmos.config import Config config = Config( diff --git a/datacosmos/stac/collection/collection_client.py b/datacosmos/stac/collection/collection_client.py index e9396e0..013ec27 100644 --- a/datacosmos/stac/collection/collection_client.py +++ b/datacosmos/stac/collection/collection_client.py @@ -2,12 +2,12 @@ from typing import Generator, Optional -from common.sdk.http_response import check_api_response from pystac import Collection, Extent, SpatialExtent, TemporalExtent from pystac.utils import str_to_datetime from datacosmos.datacosmos_client import DatacosmosClient from datacosmos.stac.collection.models.collection_update import CollectionUpdate +from datacosmos.utils.http_response import check_api_response class CollectionClient: diff --git a/datacosmos/stac/item/item_client.py b/datacosmos/stac/item/item_client.py index 6d0dbb8..fc991e4 100644 --- a/datacosmos/stac/item/item_client.py +++ b/datacosmos/stac/item/item_client.py @@ -7,24 +7,18 @@ from pystac import Item -<<<<<<< HEAD:datacosmos/stac/stac_client.py -from datacosmos.client import DatacosmosClient -from datacosmos.exceptions.datacosmos_exception import DatacosmosException -from datacosmos.stac.models.item_update import ItemUpdate -from datacosmos.stac.models.search_parameters import SearchParameters -from datacosmos.utils.http_response import check_api_response -======= from datacosmos.datacosmos_client import DatacosmosClient +from datacosmos.exceptions.datacosmos_exception import DatacosmosException from datacosmos.stac.item.models.item_update import ItemUpdate from datacosmos.stac.item.models.search_parameters import SearchParameters ->>>>>>> 77a15fc (Rename client with datacosmos_client; Move current stac api functionalities to a item dedicated folder):datacosmos/stac/item/item_client.py +from datacosmos.utils.http_response import check_api_response class ItemClient: """Client for interacting with the STAC API.""" def __init__(self, client: DatacosmosClient): - """Initialize the ItemClient with a DatacosmosClient. + """Initialize the STACClient with a DatacosmosClient. Args: client (DatacosmosClient): The authenticated Datacosmos client instance. From be4c9c392e171b6dc69e42885bb1e2e0b807fbd9 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 14 Feb 2025 10:40:59 +0000 Subject: [PATCH 133/135] Update readme file with the new methods --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c641097..8b343c0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # DataCosmos SDK ## Overview + The **DataCosmos SDK** allows Open Cosmos' customers to interact with the **DataCosmos APIs** for seamless data management and retrieval. It provides authentication handling, HTTP request utilities, and a client for interacting with the **STAC API** (SpatioTemporal Asset Catalog). ## Installation ### Install via PyPI + The easiest way to install the SDK is via **pip**: ```sh @@ -15,6 +17,7 @@ pip install datacosmos ## Getting Started ### Initializing the Client + The recommended way to initialize the SDK is by passing a `Config` object with authentication credentials: ```python @@ -33,11 +36,13 @@ client = DatacosmosClient(config=config) ``` Alternatively, the SDK can load configuration automatically from: + - A YAML file (`config/config.yaml`) - Environment variables ### STAC Client -The **STACClient** enables interaction with the STAC API, allowing for searching, retrieving, creating, updating, and deleting STAC items. + +The STACClient enables interaction with the STAC API, allowing for searching, retrieving, creating, updating, and deleting STAC items and collections. #### Initialize STACClient @@ -49,7 +54,65 @@ stac_client = STACClient(client) ### STACClient Methods +#### 1. **Fetch a Collection** + +```python +from datacosmos.stac.stac_client import STACClient +from datacosmos.datacosmos_client import DatacosmosClient + +datacosmos_client = DatacosmosClient() +stac_client = STACClient(datacosmos_client) + +collection = stac_client.fetch_collection("test-collection") +``` + +#### 2. **Fetch All Collections** + +```python +collections = list(stac_client.fetch_all_collections()) +``` + +#### 3. **Create a Collection** + +```python +from pystac import Collection + +new_collection = Collection( + id="test-collection", + title="Test Collection", + description="This is a test collection", + license="proprietary", + extent={ + "spatial": {"bbox": [[-180, -90, 180, 90]]}, + "temporal": {"interval": [["2023-01-01T00:00:00Z", None]]}, + }, +) + +stac_client.create_collection(new_collection) +``` + +#### 4. **Update a Collection** + +```python +from datacosmos.stac.collection.models.collection_update import CollectionUpdate + +update_data = CollectionUpdate( + title="Updated Collection Title version 2", + description="Updated description version 2", +) + +stac_client.update_collection("test-collection", update_data) +``` + +#### 5. **Delete a Collection** + +```python +collection_id = "test-collection" +stac_client.delete_collection(collection_id) +``` + #### 1. **Search Items** + ```python from datacosmos.stac.models.search_parameters import SearchParameters @@ -58,16 +121,19 @@ items = list(stac_client.search_items(parameters=parameters)) ``` #### 2. **Fetch a Single Item** + ```python item = stac_client.fetch_item(item_id="example-item", collection_id="example-collection") ``` #### 3. **Fetch All Items in a Collection** + ```python items = stac_client.fetch_collection_items(collection_id="example-collection") ``` #### 4. **Create a New STAC Item** + ```python from pystac import Item, Asset from datetime import datetime @@ -95,6 +161,7 @@ stac_client.create_item(collection_id="example-collection", item=stac_item) ``` #### 5. **Update an Existing STAC Item** + ```python from datacosmos.stac.models.item_update import ItemUpdate from pystac import Asset, Link @@ -124,22 +191,27 @@ stac_client.update_item(item_id="new-item", collection_id="example-collection", ``` #### 6. **Delete an Item** + ```python stac_client.delete_item(item_id="new-item", collection_id="example-collection") ``` ## Configuration Options + - **Recommended:** Instantiate `DatacosmosClient` with a `Config` object. - Alternatively, use **YAML files** (`config/config.yaml`). - Or, use **environment variables**. ## Contributing + If you would like to contribute: + 1. Fork the repository. 2. Create a feature branch. 3. Submit a pull request. ### Development Setup + If you are developing the SDK, you can use `uv` for dependency management: ```sh @@ -151,6 +223,7 @@ source .venv/bin/activate ``` Before making changes, ensure that: + - The code is formatted using **Black** and **isort**. - Static analysis and linting are performed using **ruff** and **pydocstyle**. - Security checks are performed using **bandit**. From 9669f05f928d4630e76235767589539aaf8c9ef6 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 14 Feb 2025 11:30:56 +0000 Subject: [PATCH 134/135] Remove shapely from dependencies --- .github/workflows/main.yaml | 12 ------------ datacosmos/stac/item/models/item_update.py | 12 ++++++++---- pyproject.toml | 3 +-- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index a2b4edb..0ae32d6 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -14,8 +14,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -33,8 +31,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -51,8 +47,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -69,8 +63,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -88,8 +80,6 @@ jobs: needs: [bandit, cognitive, lint, pydocstyle] steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment @@ -108,8 +98,6 @@ jobs: if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - - name: Install GEOS - run: sudo apt update && sudo apt install libgeos-dev - name: Install uv run: pip install uv - name: Set up uv environment diff --git a/datacosmos/stac/item/models/item_update.py b/datacosmos/stac/item/models/item_update.py index 338df78..3fbf8cf 100644 --- a/datacosmos/stac/item/models/item_update.py +++ b/datacosmos/stac/item/models/item_update.py @@ -4,7 +4,6 @@ from pydantic import BaseModel, Field, model_validator from pystac import Asset, Link -from shapely.geometry import mapping class ItemUpdate(BaseModel): @@ -21,9 +20,14 @@ class ItemUpdate(BaseModel): assets: Optional[dict[str, Asset]] = None links: Optional[list[Link]] = None - def set_geometry(self, geom) -> None: - """Convert a shapely geometry to GeoJSON format.""" - self.geometry = mapping(geom) + def set_geometry(self, geom_type: str, coordinates: list[Any]) -> None: + """Set the geometry manually without using shapely. + + Args: + geom_type (str): The type of geometry (e.g., 'Point', 'Polygon'). + coordinates (list[Any]): The coordinates defining the geometry. + """ + self.geometry = {"type": geom_type, "coordinates": coordinates} @staticmethod def has_valid_datetime(properties: dict[str, Any]) -> bool: diff --git a/pyproject.toml b/pyproject.toml index f36d303..ab723d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,7 @@ dependencies = [ "oauthlib==3.2.0", "requests-oauthlib==1.3.1", "pydantic==2.10.6", - "pystac==1.12.1", - "shapely==1.8.0" + "pystac==1.12.1" ] [project.optional-dependencies] From 4e76dfd21ef9433d17e964e0c806b6cb484a1319 Mon Sep 17 00:00:00 2001 From: "tiago.peres.sousa" Date: Fri, 14 Feb 2025 14:59:30 +0000 Subject: [PATCH 135/135] update uv.lock --- uv.lock | 2 -- 1 file changed, 2 deletions(-) diff --git a/uv.lock b/uv.lock index f660384..34c641c 100644 --- a/uv.lock +++ b/uv.lock @@ -32,8 +32,6 @@ requests==2.31.0 # requests-oauthlib requests-oauthlib==1.3.1 # via datacosmos (pyproject.toml) -shapely==1.8.0 - # via datacosmos (pyproject.toml) six==1.17.0 # via python-dateutil typing-extensions==4.12.2