diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 0c5332639..000000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,75 +0,0 @@ -[bumpversion] -current_version = 0.11.7 -commit = True -tag = True -tag_name = {new_version} -parse = - (?P\d+)\.(?P\d+)\.(?P\d+) - ((?P
a|b|rc)(?P\d+))?
-serialize = 
-	{major}.{minor}.{patch}{pre}{prenum}
-	{major}.{minor}.{patch}
-
-[bumpversion:file (global):pyproject.toml]
-search = version="{current_version}"
-replace = version="{new_version}"
-
-[bumpversion:file (core):pyproject.toml]
-search = titiler.core=={current_version}
-replace = titiler.core=={new_version}
-
-[bumpversion:file (extensions):pyproject.toml]
-search = titiler.extensions=={current_version}
-replace = titiler.extensions=={new_version}
-
-[bumpversion:file (mosaic):pyproject.toml]
-search = titiler.mosaic=={current_version}
-replace = titiler.mosaic=={new_version}
-
-[bumpversion:file (application):pyproject.toml]
-search = titiler.application=={current_version}
-replace = titiler.application=={new_version}
-
-[bumpversion:file:src/titiler/core/titiler/core/__init__.py]
-search = __version__ = "{current_version}"
-replace = __version__ = "{new_version}"
-
-[bumpversion:file:src/titiler/extensions/titiler/extensions/__init__.py]
-search = __version__ = "{current_version}"
-replace = __version__ = "{new_version}"
-
-[bumpversion:file:src/titiler/mosaic/titiler/mosaic/__init__.py]
-search = __version__ = "{current_version}"
-replace = __version__ = "{new_version}"
-
-[bumpversion:file:src/titiler/application/titiler/application/__init__.py]
-search = __version__ = "{current_version}"
-replace = __version__ = "{new_version}"
-
-[bumpversion:file:src/titiler/mosaic/pyproject.toml]
-search = titiler.core=={current_version}
-replace = titiler.core=={new_version}
-
-[bumpversion:file:src/titiler/extensions/pyproject.toml]
-search = titiler.core=={current_version}
-replace = titiler.core=={new_version}
-
-[bumpversion:file (core):src/titiler/application/pyproject.toml]
-search = titiler.core=={current_version}
-replace = titiler.core=={new_version}
-
-[bumpversion:file (extensions):src/titiler/application/pyproject.toml]
-search = titiler.extensions[cogeo,stac]=={current_version}
-replace = titiler.extensions[cogeo,stac]=={new_version}
-
-[bumpversion:file (mosaic):src/titiler/application/pyproject.toml]
-search = titiler.mosaic=={current_version}
-replace = titiler.mosaic=={new_version}
-
-[bumpversion:file:deployment/aws/lambda/Dockerfile]
-search = titiler.application=={current_version}
-replace = titiler.application=={new_version}
-
-[bumpversion:file:deployment/k8s/charts/Chart.yaml]
-search = appVersion: {current_version}
-replace = appVersion: {new_version}
diff --git a/.github/data/urls.txt b/.github/data/urls.txt
index 4782485da..40b810e3c 100644
--- a/.github/data/urls.txt
+++ b/.github/data/urls.txt
@@ -1,7 +1,7 @@
 PROT=http
 HOST=localhost
 PORT=8000
-PATH=cog/tiles/
+PATH=cog/tiles/WebMercatorQuad/
 EXT=.png
 QUERYSTRING=?url=/data/world.tif
 $(PROT)://$(HOST):$(PORT)/$(PATH)0/0/0$(EXT)$(QUERYSTRING)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..4fa1bd99b
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+# Set update schedule for GitHub Actions
+
+version: 2
+updates:
+
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      # Check for updates to GitHub Actions every week
+      interval: "weekly"
+    groups:
+      all:
+        patterns:
+        - "*"
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index e2e9bd880..c3d1ff2a7 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -5,23 +5,27 @@ on:
   push:
     branches:
     - main
+    - dev
     tags:
     - '*'
   pull_request:
 
 jobs:
   benchmark:
+    if: github.repository == 'developmentseed/titiler'
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v6
 
       - name: install siege
         run: |
           sudo apt update
-          sudo apt install --yes siege
+          sudo apt install --yes siege jq
+          siege -C
+
       - name: Start containers
-        run: docker-compose -f "docker-compose.yml" up -d --build benchmark
+        run: docker compose -f "docker-compose.yml" up -d --build benchmark
 
       # Let's wait a bit to make sure the docker are up
       - name: Sleep for 10 seconds
@@ -30,12 +34,39 @@ jobs:
 
       - name: Run siege (WebMercator TMS)
         run: |
-          siege --file .github/data/urls.txt -b -c 1 -r 100 > /dev/null
+          siege --file .github/data/urls.txt -b -c 1 -r 100 --json-output 2>&1 | jq -c > results.json
+          echo "Benchmark Results"
+          cat results.json | jq
+          echo "Parse Results"
+          cat results.json | jq '{"name": "WebMercator elapsed_time", "unit": "s", "value": .elapsed_time}, {"name": "WebMercator data_transferred", "unit": "Megabytes", "value": .data_transferred}, {"name": "WebMercator response_time", "unit": "s", "value": .response_time}, {"name": "WebMercator longest_transaction", "unit": "s", "value": .longest_transaction}' > output.json
 
       - name: Run siege (WGS1984Quad TMS)
         run: |
-          siege --file .github/data/urls_wgs84.txt -b -c 1 -r 100 > /dev/null
+          siege --file .github/data/urls_wgs84.txt -b -c 1 -r 100 --json-output 2>&1 | jq -c > results.json
+          echo "Benchmark Results"
+          cat results.json | jq
+          echo "Parse Results"
+          cat results.json | jq '{"name": "WGS1984Quad elapsed_time", "unit": "s", "value": .elapsed_time}, {"name": "WGS1984Quad data_transferred", "unit": "Megabytes", "value": .data_transferred}, {"name": "WGS1984Quad response_time", "unit": "s", "value": .response_time}, {"name": "WGS1984Quad longest_transaction", "unit": "s", "value": .longest_transaction}' >> output.json
 
       - name: Stop containers
         if: always()
-        run: docker-compose -f "docker-compose.yml" down
+        run: docker compose -f "docker-compose.yml" down
+
+      - name: Merge Outputs
+        run: |
+          cat output.json | jq '[inputs]' > benchmark.json
+
+      - name: Check and Store benchmark result
+        uses: benchmark-action/github-action-benchmark@v1
+        with:
+          name: TiTiler performance Benchmarks
+          tool: 'customSmallerIsBetter'
+          output-file-path: benchmark.json
+          alert-threshold: '130%'
+          comment-on-alert: true
+          fail-on-alert: false
+          # GitHub API token to make a commit comment
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          gh-pages-branch: 'gh-benchmarks'
+          # Make a commit only if main
+          auto-push: ${{ github.ref == 'refs/heads/main' }}
diff --git a/.github/workflows/check_charts.yaml b/.github/workflows/check_charts.yaml
index e79f20937..d2feb49ba 100644
--- a/.github/workflows/check_charts.yaml
+++ b/.github/workflows/check_charts.yaml
@@ -19,13 +19,13 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v6
         with:
           fetch-depth: 0
 
       - name: Check Version
         run: |
-          current_version=$(grep 'version=' pyproject.toml | cut -f2 -d= | tr -d ' ' | tr -d '"')
+          current_version=$(grep '^version=' pyproject.toml | cut -f2 -d= | tr -d ' ' | tr -d '"')
           app_version=$(grep 'appVersion:' deployment/k8s/charts/Chart.yaml | cut -f2 -d: | tr -d ' ')
           if [[ "$current_version" != "$app_version" ]]; then
             echo "❌ current version from pyproject.toml ($current_version) and appVersion from Chart.yaml ($app_version) differs";
@@ -33,16 +33,16 @@ jobs:
           fi
 
       - name: Set up Helm
-        uses: azure/setup-helm@v1
+        uses: azure/setup-helm@v4
         with:
           version: v3.9.2
 
-      - uses: actions/setup-python@v2
+      - uses: actions/setup-python@v6
         with:
-          python-version: 3.7
+          python-version: '3.x'
 
       - name: Set up chart-testing
-        uses: helm/chart-testing-action@v2.2.1
+        uses: helm/chart-testing-action@v2.8.0
 
       - name: Run chart-testing (list-changed)
         id: list-changed
@@ -56,18 +56,18 @@ jobs:
         run: ct lint --chart-dirs deployment/k8s --target-branch ${{ github.event.repository.default_branch }}
 
       - name: Build container
-        uses: docker/build-push-action@v2
+        uses: docker/build-push-action@v6
         if: steps.list-changed.outputs.changed == 'true'
         with:
           # See https://github.com/developmentseed/titiler/discussions/387
           platforms: linux/amd64
           context: .
-          file: dockerfiles/Dockerfile.uvicorn
+          file: dockerfiles/Dockerfile
           push: false
           tags: "titiler:dev"
 
       - name: Create kind cluster
-        uses: helm/kind-action@v1.2.0
+        uses: helm/kind-action@v1.13.0
         if: steps.list-changed.outputs.changed == 'true'
 
       - name: Load container image in kind cluster
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d44a4ef80..3a7da45c1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@ on:
   push:
     branches:
     - main
+    - dev
     tags:
     - '*'
     paths:
@@ -15,94 +16,103 @@ on:
       - '.pre-commit-config.yaml'
       - '.github/codecov.yml'
       - 'dockerfiles/**'
+      - '.github/workflows/ci.yml'
+      - 'uv.lock'
   pull_request:
+  workflow_dispatch:
+
 env:
-  LATEST_PY_VERSION: '3.10'
+  LATEST_PY_VERSION: '3.14'
 
 jobs:
   tests:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ['3.8', '3.9', '3.10', '3.11']
+        python-version: ['3.11', '3.12', '3.13', '3.14']
 
     steps:
-      - uses: actions/checkout@v2
-      - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
+      - uses: actions/checkout@v6
+     
+      - name: Install uv
+        uses: astral-sh/setup-uv@v7
         with:
+          version: "0.9.*" 
+          enable-cache: true
           python-version: ${{ matrix.python-version }}
 
       - name: Install dependencies
         run: |
-          python -m pip install --upgrade pip
+          uv sync
 
       - name: Test titiler.core
         run: |
-          python -m pip install -e src/titiler/core["test"]
-          python -m pytest src/titiler/core --cov=titiler.core --cov-report=xml --cov-append --cov-report=term-missing
+          uv run pytest src/titiler/core --cov=titiler.core --cov-report=xml:titiler-core.xml --cov-report=term-missing
 
       - name: Test titiler.extensions
         run: |
-          python -m pip install -e src/titiler/extensions["test,cogeo,stac"]
-          python -m pytest src/titiler/extensions --cov=titiler.extensions --cov-report=xml --cov-append --cov-report=term-missing
+          uv run pytest src/titiler/extensions --cov=titiler.extensions --cov-report=xml:titiler-extensions.xml --cov-report=term-missing
+
+      - name: Test titiler.xarray
+        run: |
+          uv run pytest src/titiler/xarray --cov=titiler.xarray --cov-report=xml:titiler-xarray.xml --cov-report=term-missing
 
       - name: Test titiler.mosaic
         run: |
-          python -m pip install -e src/titiler/mosaic["test"]
-          python -m pytest src/titiler/mosaic --cov=titiler.mosaic --cov-report=xml --cov-append --cov-report=term-missing
+          uv run pytest src/titiler/mosaic --cov=titiler.mosaic --cov-report=xml:titiler-mosaic.xml --cov-report=term-missing
 
       - name: Test titiler.application
         run: |
-          python -m pip install -e src/titiler/application["test"]
-          python -m pytest src/titiler/application --cov=titiler.application --cov-report=xml --cov-append --cov-report=term-missing
+          uv run pytest src/titiler/application --cov=titiler.application --cov-report=xml:titiler-application.xml  --cov-report=term-missing
 
-      - name: run pre-commit
+      - name: run pre-commit and mypy
         if: ${{ matrix.python-version == env.LATEST_PY_VERSION }}
         run: |
-          python -m pip install pre-commit
-          pre-commit run --all-files
+          uv run pre-commit run --all-files
+          uv run --with mypy --with types-attrs --with types-simplejson mypy -p titiler.core -p titiler.extensions -p titiler.xarray -p titiler.application -p titiler.mosaic  --ignore-missing-imports
 
       - name: Upload Results
         if: ${{ matrix.python-version == env.LATEST_PY_VERSION }}
-        uses: codecov/codecov-action@v1
+        uses: codecov/codecov-action@v5
         with:
-          file: ./coverage.xml
+          files: ./titiler-core.xml,./titiler-extensions.xml,./titiler-xarray.xml,./titiler-mosaic.xml,./titiler-application.xml
           flags: unittests
-          name: ${{ matrix.python-version }}
           fail_ci_if_error: false
+          token: ${{ secrets.CODECOV_TOKEN }}
 
   publish:
     needs: [tests]
     runs-on: ubuntu-latest
     if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
     steps:
-      - uses: actions/checkout@v2
-      - name: Set up Python
-        uses: actions/setup-python@v1
+      - uses: actions/checkout@v6
+      
+      - name: Install uv
+        uses: astral-sh/setup-uv@v7
         with:
-          python-version: ${{ env.LATEST_PY_VERSION }}
+          version: "0.9.*" 
+          enable-cache: true
+          python-version: ${{ matrix.python-version }}
 
       - name: Install dependencies
         run: |
-          python -m pip install --upgrade pip
-          pip install twine build hatch
+          uv sync
 
       - name: Set tag version
         id: tag
-        # https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions
-        run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
+        run: |
+          echo "version=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
 
       - name: Set module version
         id: module
-        # https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions
-        run: echo ::set-output name=version::$(hatch --quiet version)
+        run: |
+          echo "version=$(uv version --short)" >> $GITHUB_OUTPUT
 
       - name: Build and publish titiler packages
-        if: steps.tag.outputs.tag == steps.module.outputs.version
+        if: ${{ steps.tag.outputs.version }} == ${{ steps.module.outputs.version}}
         env:
-          TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
-          TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+          UV_PUBLISH_USERNAME: ${{ secrets.PYPI_USERNAME }}
+          UV_PUBLISH_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
         run: |
           scripts/publish
 
@@ -110,141 +120,49 @@ jobs:
     needs: [tests]
     if: github.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
     runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          - image: ghcr.io/${{ github.repository }}
+            file: dockerfiles/Dockerfile
+          - image: ghcr.io/${{ github.repository }}-xarray
+            file: dockerfiles/Dockerfile.xarray
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v6
 
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v1
+        uses: docker/setup-qemu-action@v3
 
       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v1
+        uses: docker/setup-buildx-action@v3
 
-      - name: Login to DockerHub
-        uses: docker/login-action@v1
-        with:
-          username: ${{ secrets.DOCKERHUB_USERNAME }}
-          password: ${{ secrets.DOCKERHUB_TOKEN }}
-
-      - name: Login to Github
-        uses: docker/login-action@v1
+      - name: Log in to the GitHub Container registry
+        uses: docker/login-action@v3
         with:
           registry: ghcr.io
           username: ${{ github.actor }}
           password: ${{ secrets.GITHUB_TOKEN }}
 
-      - name: Set tag version
-        id: tag
-        # https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions
-        run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
-      # Uvicorn
-      # Push `latest` when commiting to main
-      - name: Build and push uvicorn
-        if: github.ref == 'refs/heads/main'
-        uses: docker/build-push-action@v2
-        with:
-          # See https://github.com/developmentseed/titiler/discussions/387
-          platforms: linux/amd64
-          context: .
-          file: dockerfiles/Dockerfile.uvicorn
-          push: true
-          tags: |
-            ghcr.io/${{ github.repository }}-uvicorn:latest
-
-      # Push `{VERSION}` when pushing a new tag
-      - name: Build and push uvicorn
-        if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
-        uses: docker/build-push-action@v2
+      - name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
         with:
-          # See https://github.com/developmentseed/titiler/discussions/387
-          platforms: linux/amd64
-          context: .
-          file: dockerfiles/Dockerfile.uvicorn
-          push: true
-          tags: |
-            ghcr.io/${{ github.repository }}-uvicorn:${{ steps.tag.outputs.tag }}
-
-      # Gunicorn
-      # Push `latest` when commiting to main
-      - name: Build and push
-        if: github.ref == 'refs/heads/main'
-        uses: docker/build-push-action@v2
-        with:
-          # See https://github.com/developmentseed/titiler/discussions/387
-          platforms: linux/amd64
-          context: .
-          file: dockerfiles/Dockerfile.gunicorn
-          push: true
+          images: ${{ matrix.image }}
+          flavor: |
+            latest=false
           tags: |
-            ghcr.io/${{ github.repository }}:latest
+            type=semver,pattern={{version}}
+            type=raw,value=latest,enable={{is_default_branch}}
 
-      # Push `{VERSION}` when pushing a new tag
       - name: Build and push
-        if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
-        uses: docker/build-push-action@v2
+        uses: docker/build-push-action@v6
         with:
-          # See https://github.com/developmentseed/titiler/discussions/387
-          platforms: linux/amd64
+          platforms: linux/amd64,linux/arm64
           context: .
-          file: dockerfiles/Dockerfile.gunicorn
-          push: true
-          tags: |
-            ghcr.io/${{ github.repository }}:${{ steps.tag.outputs.tag }}
-
-  deploy:
-    needs: [tests, publish]
-    runs-on: ubuntu-latest
-    if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
-
-    defaults:
-      run:
-        working-directory: deployment/aws
-
-    steps:
-      - uses: actions/checkout@v3
-
-      # Let's wait a bit to make sure Pypi is up to date
-      - name: Sleep for 120 seconds
-        run: sleep 120s
-        shell: bash
-
-      - name: Configure AWS credentials
-        uses: aws-actions/configure-aws-credentials@v1
-        with:
-          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
-          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-          aws-region: us-east-1
-
-      - name: Set up Node.js
-        uses: actions/setup-node@v1
-        with:
-          node-version: '14.x'
-
-      - name: Install cdk
-        run: npm install -g
-
-      - name: Set up Python
-        uses: actions/setup-python@v4
-        with:
-          python-version: '3.x'
-
-      - name: Install dependencies
-        run: |
-          python -m pip install --upgrade pip
-          python -m pip install -r requirements-cdk.txt
-
-      # Let's wait a bit to make sure package is available on pypi
-      - name: Sleep for 120 seconds
-        run: sleep 120s
-        shell: bash
-
-      # Build and Deploy CDK application
-      - name: Build & Deploy
-        run: npm run cdk -- deploy ${{ secrets.STACK_NAME }}-lambda-${{ secrets.STACK_STAGE }} --require-approval never
-        env:
-          TITILER_STACK_NAME: ${{ secrets.STACK_NAME }}
-          TITILER_STACK_STAGE: ${{ secrets.STACK_STAGE }}
-          TITILER_STACK_MEMORY: ${{ secrets.STACK_MEMORY }}
-          TITILER_STACK_OWNER: ${{ secrets.STACK_OWNER }}
-          TITILER_STACK_CLIENT: ${{ secrets.STACK_CLIENT }}
-          TITILER_STACK_BUCKETS: ${{ secrets.STACK_BUCKETS }}
+          file: ${{ matrix.file }}
+          push: ${{ github.event_name != 'pull_request' }}
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 000000000..8c9275669
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,63 @@
+name: Deploy application
+
+# On every pull request, but only on push to main
+on:
+  # push:
+  #   tags:
+  #   - '*'
+  workflow_dispatch:
+  # workflow_run:
+  #   workflows: ["CI"]   # Name of the workflow to listen for
+  #   types:
+  #     - completed
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    if: ${{ github.repository == 'developmentseed/titiler' }}
+    # if: |
+    #   (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) ||
+    #   (github.event_name == 'workflow_run' &&
+    #     github.event.workflow_run.conclusion == 'success' &&
+    #     startsWith(github.event.workflow_run.head_branch, 'refs/tags') &&
+    #     github.repository == 'developmentseed/titiler')
+    defaults:
+      run:
+        working-directory: deployment/aws
+
+    steps:
+      - uses: actions/checkout@v6
+
+      # Let's wait a bit to make sure Pypi is up to date
+      - name: Sleep for 120 seconds
+        run: sleep 120s
+        shell: bash
+
+      - name: Configure AWS credentials
+        uses: aws-actions/configure-aws-credentials@v5
+        with:
+          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          aws-region: us-east-1
+
+      - name: Set up uv
+        uses: astral-sh/setup-uv@v7
+      
+      - name: Install dependencies
+        run: uv sync --all-packages && npm install -g aws-cdk@^2.232.2
+
+      # Let's wait a bit to make sure package is available on pypi
+      - name: Sleep for 120 seconds
+        run: sleep 120s
+        shell: bash
+
+      # Build and Deploy CDK application
+      - name: Build & Deploy
+        run: uv run cdk deploy ${{ secrets.STACK_NAME }}-lambda-${{ secrets.STACK_STAGE }} --require-approval never
+        env:
+          TITILER_STACK_NAME: ${{ secrets.STACK_NAME }}
+          TITILER_STACK_STAGE: ${{ secrets.STACK_STAGE }}
+          TITILER_STACK_MEMORY: ${{ secrets.STACK_MEMORY }}
+          TITILER_STACK_OWNER: ${{ secrets.STACK_OWNER }}
+          TITILER_STACK_CLIENT: ${{ secrets.STACK_CLIENT }}
+          TITILER_STACK_BUCKETS: ${{ secrets.STACK_BUCKETS }}
diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml
index f9b537459..bb91736d1 100644
--- a/.github/workflows/deploy_mkdocs.yml
+++ b/.github/workflows/deploy_mkdocs.yml
@@ -19,47 +19,13 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout main
-        uses: actions/checkout@v2
+        uses: actions/checkout@v6
 
-      - name: Set up Python 3.8
-        uses: actions/setup-python@v2
+      - name: Install uv
+        uses: astral-sh/setup-uv@v7
         with:
-          python-version: 3.8
-
-      - name: Install dependencies
-        run: |
-          python -m pip install --upgrade pip
-          python -m pip install src/titiler/core src/titiler/extensions["cogeo,stac"] src/titiler/mosaic src/titiler/application
-          python -m pip install nbconvert==6.5.3 mkdocs mkdocs-material mkdocs-jupyter pygments pdocs
-
-      - name: update API docs
-        run: |
-          pdocs as_markdown \
-            --output_dir docs/src/api \
-            --exclude_source \
-            --overwrite \
-            titiler.core.dependencies \
-            titiler.core.factory \
-            titiler.core.routing \
-            titiler.core.errors \
-            titiler.core.resources.enums \
-            titiler.core.middleware
-
-          pdocs as_markdown \
-            --output_dir docs/src/api \
-            --exclude_source \
-            --overwrite \
-            titiler.extensions.cogeo \
-            titiler.extensions.viewer \
-            titiler.extensions.stac
-
-          pdocs as_markdown \
-            --output_dir docs/src/api \
-            --exclude_source \
-            --overwrite \
-            titiler.mosaic.factory \
-            titiler.mosaic.resources.enums \
-            titiler.mosaic.errors
-
+          version: "0.9.*" 
+          enable-cache: true
+    
       - name: Deploy docs
-        run: mkdocs gh-deploy --force -f docs/mkdocs.yml
+        run: uv run --group docs mkdocs gh-deploy --force -f docs/mkdocs.yml
diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml
new file mode 100644
index 000000000..c1dc447d8
--- /dev/null
+++ b/.github/workflows/upstream.yml
@@ -0,0 +1,34 @@
+name: Upstream
+
+# Only on pushes to main, tags, or workflow dispatches
+on:
+  push:
+    branches:
+    - main
+    - dev
+    paths:
+      # Only run test and docker publish if some code have changed
+      - 'src/titiler/xarray/**'
+  workflow_dispatch:
+
+jobs:
+  test-upstream:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ['3.11', '3.12', '3.13', '3.14']
+
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Install uv
+        uses: astral-sh/setup-uv@v7
+        with:
+          version: "0.9.*" 
+          enable-cache: true
+          python-version: ${{ matrix.python-version }}
+
+      - name: Test titiler.xarray
+        working-directory: src/titiler/xarray
+        run: |
+          uv run --group test --group upstream pytest
diff --git a/.gitignore b/.gitignore
index 95640297c..1b41d9cd7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,7 @@ htmlcov/
 .cache
 nosetests.xml
 coverage.xml
+titiler-*.xml
 *.cover
 .hypothesis/
 
@@ -107,4 +108,7 @@ ENV/
 
 cdk.out/
 deployment/k8s/titiler/values-test.yaml
-docs/src/api/
+
+.vscode/
+
+dev_notebooks/*
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 63ed17680..98ef26be2 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,29 +1,24 @@
 repos:
   - repo: https://github.com/abravalheri/validate-pyproject
-    rev: v0.12.1
+    rev: v0.24
     hooks:
       - id: validate-pyproject
 
-  - repo: https://github.com/psf/black
-    rev: 22.12.0
-    hooks:
-      - id: black
-        language_version: python
-
   - repo: https://github.com/PyCQA/isort
     rev: 5.12.0
     hooks:
       - id: isort
         language_version: python
 
-  - repo: https://github.com/charliermarsh/ruff-pre-commit
-    rev: v0.0.238
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.8.4
     hooks:
       - id: ruff
         args: ["--fix"]
+      - id: ruff-format
 
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v0.991
+    rev: v1.19.0
     hooks:
       - id: mypy
         language_version: python
@@ -31,3 +26,4 @@ repos:
         additional_dependencies:
         - types-simplejson
         - types-attrs
+        - pydantic~=2.0
diff --git a/CHANGES.md b/CHANGES.md
index 2481fa831..da79a5bb5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,1116 @@
 # Release Notes
 
+## Unreleased
+
+### Misc
+
+* change: rio-tiler requirement to `>=9.0.0rc2,<10.0`
+
+## 2.0.0b2 (2026-02-22)
+
+### titiler.extensions
+
+* fix: render expression adapter
+
+## 2.0.0b1 (2026-02-22)\
+
+### titiler.core
+
+* change: rio-tiler requirement to `>=9.0.0b1,<10.0`
+* add: asset's options parsing in `AssetsParams` dependency
+
+## 2.0.0a2 (2026-02-13)
+
+### titiler.extensions
+
+* **render**:
+    * change: make `assets` optional in `RenderItem` model
+    * change: add `valid: True|False` key to the `RenderItemWithLinks` model
+    * add: convert old `asset_bix` and `asset_expression` keys to `{asset}|indexes=...&expression=...`
+
+### titiler.mosaic
+
+* fix: tilesize for `/map.html` endpoint
+
+## 2.0.0a1 (2026-02-11)
+
+### Misc
+
+* remove: 256x256 tilesize default
+* remove: `MultiBandTilerFactory` factory
+* remove: `@{scale}x` suffix for tile endpoints
+* remove: `tile_scale` option in `/tilejson.json` and `/map.html` endpoints
+* change: default to TileMatrix's `tileHeight x tileWidth` for tile endpoints
+* change: set `tilesize=512` for `/tilejson.json` endpoints
+* change: set `tilesize=256` for `/map.html` endpoints
+* change: use `band_descriptions` instead of `band_names`
+* add: `tilesize` optional query-parameter for tile and tilejson endpoints
+* remove: titiler-xarray in docker-compose file
+* remove: titiler-xarray single application deployement in AWS lambda deployement
+* change: rio-tiler requirement to `>=9.0.0a4,<10.0`
+
+### titiler.core
+
+* change: `bidx` option is now ignored by `MultiBaseFactory` endpoints
+* change: `assets` option is now **required** for `MultiBaseFactory` endpoints
+* change: users can use `assets=:all:` in `MultiBaseFactory`'s `/info` and `/statistics` endpoints
+* change: `expression` cannot be used to declare `assets` in `MultiBaseFactory` endpoints. Use `assets=Red&assets=Green&expression=b1/b2`.
+* remove: `asset_indexes` and `asset_expression` options in `dependencies.py`
+* rename: `dependencies.AssetsBidxExprParams` to `dependencies.AssetsExprParams` 
+* remove: methods or classes:
+    - `titiler.core.dependencies.AssetsBidxParams`
+    - `titiler.core.dependencies.AssetsBidxExprParamsOptional`
+    - `titiler.core.dependencies.BandsParams`
+    - `titiler.core.dependencies.BandsExprParamsOptional`
+    - `titiler.core.dependencies.BandsExprParams`
+    - `titiler.core.dependencies.parse_asset_indexes()`
+    - `titiler.core.dependencies.parse_asset_expression()`
+    - `titiler.core.routing.apiroute_factory()`
+
+### titiler.extensions
+
+* change: force `tilesize=256` in `cog` and `stac` viewers
+* remove: `tile_scale` option in `/WMTSCapabilities.xml` endpoints
+
+### titiler.mosaic
+
+* remove: `tile_scale` option in `/WMTSCapabilities.xml` endpoints
+
+### titiler.application
+
+* add: titiler.xarray dependency
+* add: `/zarr` endpoints
+
+## 1.2.0 (2026-02-09)
+
+### Input Validation
+
+* fix: address several instances of validation failures returning 500 instead of 4xx HTTP responses
+
+## 1.1.1 (2026-01-22)
+
+### titiler.extensions
+
+* add: support for `float16`, `int64` and `uint64` datatype in viewers
+* fix: make sure TMS have ids in WMTS XML documents
+
+### titiler.mosaic
+
+* fix: make sure TMS have ids in WMTS XML documents
+
+### titiler.xarray
+
+* fix: add `h5py` in optional dependency
+
+## 1.1.0 (2026-01-12)
+
+### Misc
+
+* add `get_renders` attribute to `wmtsExtension` extensions
+* refactor WMTS endpoints to enable renders metadata (e.g STAC renders)
+* update `wmts.xml` template **breaking change**
+
+### titiler.core 
+
+* Improve `dem` algorithms by using numpy functions (author @manand881, https://github.com/developmentseed/titiler/pull/1294)
+
+## 1.0.2 (2025-12-18)
+
+### titiler.core
+
+* add more TMS utilities 
+* remove `bump-my-version` from dev dependencies
+
+## 1.0.1 (2025-12-18)
+
+### titiler.core
+
+* add `utils.tms_limits` options to avoid code duplication
+
+## 1.0.0 (2025-12-17)
+
+### Misc 
+
+* update rio-tiler requirement to `>=8.0,<9.0`
+* return `UINT8` datatype JPEG/PNG when no output format is specified **breaking change**
+* remove `/{tileMatrixSetId}/WMTSCapabilities.xml` endpoints from factories **breaking change**
+* add python 3.14 support
+* add `linux/arm64` docker image 
+* update/fix type hints 
+* add arm64 docker image
+* add titiler-xarray docker image in  ghcr.io
+
+### titiler.core
+
+* add `band_description` attribute to `Point` output model (returned by /point endpoints) **breaking change**
+
+### titiler.extensions
+
+* update rio-cogeo requirement to `7.0,<8.0`
+* add `wmtsExtension` which adds `/WMTSCapabilities.xml` to factories
+* `WMTSCapabilities.xml` response now support all TileMatrixSets as separate layers **breaking change**
+
+### titiler.mosaic
+
+* change Response model for `/point` endpoint **breaking change**
+
+    ```python
+    # before
+    class Point(BaseModel):
+        coordinates: List[float]
+        values: List[Tuple[str, List[Optional[float]], List[str]]]
+
+    # now
+    class AssetPoint(BaseModel):
+        name: str
+        values: list[float | None]
+        band_names: list[str]
+        band_descriptions: list[str] | None = None
+
+    class Point(BaseModel):
+        coordinates: list[float]
+        assets: list[AssetPoint]
+    ```
+
+* add `/feature`, `/bbox` and `/statistics` optional endpoints 
+* make `cogeo-mosaic` an optional dependency **breaking change**
+* remove default for `MosaicTilerFactory.backend` attribute **breaking change**
+* add `titiler.mosaic.extensions.mosaicjson.MosaicJSONExtension` which adds MosaicJSON specific `/` and `/validate` endpoints
+* add `titiler.mosaic.extension.wmts.wmtsExtension` which adds `/WMTSCapabilities.xml` endpoint
+* add optional OGC Maps API `/map` endpoint
+
+## 0.26.0 (2025-11-25)
+
+### titiler.xarray
+
+* use `sel={dim}={method}::{value}` notation  to specify selector method instead of `sel-method` query-parameter **breaking change** 
+
+    ```python
+    # before
+    .../info?tore.zarr?sel=time=2023-01-01&sel_method=nearest`
+
+    # now
+    .../info?tore.zarr?sel=time=nearest::2023-01-01`
+    ```
+
+* add `/validate` endpoint via `ValidateExtension` extension
+* add `Latitude` and `Longitude` as compatible spatial dimensions (@abarciauskas-bgse, https://github.com/developmentseed/titiler/pull/1268)
+
+### titiler.mosaic
+
+* remove usage of `mosaic_def.center` and calculate from bounds
+
+## 0.25.0 (2025-11-07)
+
+### Misc 
+
+* remove `/bounds` endpoints **breaking change**
+* update docker image to python:3.13
+* switch to `uv` for development
+* switch to `hatch` for python package build-system
+* remove `titiler` metapackage **breaking change**
+* bump minimum python version to 3.11
+
+### titiler.xarray
+
+* add `opener_options` arg to `titiler.xarray.io.Reader` to allow users to pass args through to a custom opener function ([#1248(https://github.com/developmentseed/titiler/pull/1248)])
+* add `obstore` and `zarr-python` as dependency and add `open_zarr` dataset opener
+* default to `titiler.xarray.io.open_zarr` for `titiler.xarray.io.Reader.dataset_opener` attribute
+* rename `titiler.xarray.io.xarray_open_dataset` to `fs_open_dataset`
+* add `FsReader` which use `fs_open_dataset` as `dataset_opener`
+* create offical application `titiler.xarray.main:app`
+
+### titiler.mosaic
+
+* move `/` and `/validate`  to a `MosaicJSONExtension`
+
+## 0.24.2 (2025-10-16)
+
+### titiler.core
+
+* update `TileJSON` spec from 2.2.0 to 3.0.0
+* fix OpenAPI spec for `histogram_range` examples (@guillemc23, https://github.com/developmentseed/titiler/pull/1239)
+
+## 0.24.1 (2025-10-10)
+
+* add `grayscale` and `bitonal` algorithms
+* add `transform` and `crs` for `tiff` outputs
+
+
+## 0.24.0 (2025-09-23)
+
+### Misc
+
+* add attribution in `/tilejson.json` response. Controled with `TITILER_DEFAULT_ATTRIBUTION` environment variable.
+* enable `jinja2.autoescape` for HTML/XML templates (ref: https://jinja.palletsprojects.com/en/stable/api/#autoescaping)
+* remove python 3.9 support
+
+### titiler.extension 
+
+* update rio-stac requirement
+
+### titiler.application
+
+* add `description` in `ApiSettings`
+
+### titiler.core
+
+* delete `titiler.core.templating` submodule **breaking change**
+* move `create_html_response` function to `titiler.core.utils` submodule
+* move all HTML templates in `titiler/core/templates` directory  **breaking change**
+* add HTML responses for tilesets, tilematrixsets, algorithms and colormaps endpoints
+* rename response model `ColorMapsList` -> `ColorMapList` and change it's attibutes to `colormaps` **breaking change**
+* add `templates` in the `BaseFactory` class definition
+
+## 0.23.1 (2025-08-27)
+
+### titiler.core
+
+* add `sum` algorithm
+
+## 0.23.0 (2025-08-26)
+
+### titiler.core
+
+* add OpenTelemetry instrumentation to the tiler factory classes
+* add `OGC Maps API` support (`/map` endpoint)
+
+### titiler.application
+
+* add OpenTelemetry tracing to the FastAPI application
+* update `starlette-cramjam` requirement to `>=0.4,<0.6`
+
+### titiler.xarray
+
+* add `add_preview` in factory attribute (default to `False`)
+
+### Misc
+
+* Add otel-collector and jaeger to the docker network
+* fix layer's bounds for non-wgs84 CRS in WMTS document
+* switch from bitnami to official python:3.12 docker image
+
+## 0.22.4 (2025-07-02)
+
+* fix `rel` values for tiling scheme link (OGC Tiles specification)
+
+## 0.22.3 (2025-06-17)
+
+### titiler.xarray
+
+* use dimension's `dtype` to cast user *selection*
+
+## 0.22.2 (2025-06-02)
+
+### titiler.application
+
+* remove unused templates
+
+### titiler.xarray
+
+* fix `xarray_open_dataset` for cloud hosted files
+
+## 0.22.1 (2025-05-13)
+
+### titiler.xarray
+
+* update `reader` and `path_dependency` type informations
+
+## 0.22.0 (2025-05-06)
+
+### Misc
+
+* rename `/map` endpoint to `/map.html` **breaking change**
+* add `name` attribute to `BaseFactory` to define endpoint's `operationId`
+* add `operationId` on all endpoints
+* add `/preview/{width}x{height}.{format}` endpoints
+* update rio-tiler requirement to `>=7.7,<8.0`
+* allow users to pass only one of `width` or `heigh` size parameters for `preview`, `part` and `feature` requests
+* use `minZoom` instead of `minNativeZoom` in the `/map.html` html template
+* update geojson-pydantic requirement to `>=1.1.2,<3.0` and change featureCollection iteration
+
+### titiler.application
+
+* fix Landing page links when app is behind proxy
+* use `titiler.core` templates for Landing page
+* enable JSON and HTML rendering of the `/` landing page
+* add OGC Common `/conformance` endpoint
+
+### titiler.core
+
+* add `conforms_to` attribute to `BaseFactory` to indicate which conformance the TileFactory implement
+
+* remove deprecated `ColorFormulaParams` and `RescalingParams` dependencies **breaking change**
+
+* remove deprecated `DefaultDependency` dict-unpacking feature **breaking change**
+
+* add `min`, `max`, `mean`, `median`, `std` and `var` algorithms
+
+* Fix TerrainRGB algorithm and param user-controlled nodata-height (@jo-chemla, https://github.com/developmentseed/titiler/pull/1116)
+
+* add `output_min` and `output_max` metadata attributes to `slope` algorithm (@tayden, https://github.com/developmentseed/titiler/pull/1089)
+
+* add point value query on right-click to map viewer (@hrodmn, https://github.com/developmentseed/titiler/pull/1100)
+
+* refactor middlewares to use python's dataclasses
+
+* update `LoggerMiddleware` output format and options **breaking change**
+
+    ```python
+    from fastapi import FastAPI
+
+    from titiler.core.middlewares import LoggerMiddleware
+
+    # before
+    app = FastAPI()
+    app.add_middlewares(LoggerMiddleware, querystrings=True, headers=True)
+
+    # now
+    app = FastAPI()
+    app.add_middlewares(
+        LoggerMiddleware,
+        # custom Logger
+        logger=logging.getLogger("mytiler.requests"),  # default to logging.getLogger("titiler.requests")
+    )
+    ```
+
+    Note: logger needs then to be `configured` at runtime. e.g :
+
+    ```python
+    from logging import config
+    config.dictConfig(
+        {
+            "version": 1,
+            "disable_existing_loggers": False,
+            "formatters": {
+                "detailed": {
+                    "format": "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
+                },
+                "request": {
+                    "format": (
+                        "%(asctime)s - %(levelname)s - %(name)s - %(message)s "
+                        + json.dumps(
+                            {
+                                k: f"%({k})s"
+                                for k in [
+                                    "method",
+                                    "referer",
+                                    "origin",
+                                    "route",
+                                    "path",
+                                    "path_params",
+                                    "query_params",
+                                    "headers",
+                                ]
+                            }
+                        )
+                    ),
+                },
+            },
+            "handlers": {
+                "console_request": {
+                    "class": "logging.StreamHandler",
+                    "level": "DEBUG",
+                    "formatter": "request",
+                    "stream": "ext://sys.stdout",
+                },
+            },
+            "loggers": {
+                "mytiler.requests": {
+                    "level": "INFO",
+                    "handlers": ["console_request"],
+                    "propagate": False,
+                },
+            },
+        }
+    )
+    ```
+
+### titiler.extensions
+
+* update `wms` extension to remove usage of `ColorFormulaParams` and `RescalingParams` dependencies
+* update `render` extension to better validate query-parameters from render expression
+
+### titiler.xarray
+
+* update `rio-tiler` requirement to `>=7.6.1`
+* add `sel` and `sel_method` options to select dimension
+
+    ```
+    # before
+    https://.../0/0/0.png?url=dataset.zarr&drop_dim=time=2023-01-01
+
+    # now
+    https://.../0/0/0.png?url=dataset.zarr&sel=time=2023-01-01
+
+    # method
+    https://.../0/0/0.png?url=dataset.zarr&sel=time=2023-01-02&sel_method=nearest
+
+    # Can use `slice` when providing 2 values
+    https://.../0/0/0.png?url=dataset.zarr&sel=time=2023-01-01&time=2023-01-31
+    ```
+* add support for `bidx` parameter
+* remove `first` **time** dim selection **breaking change**
+* add support for 3D dataset
+* remove `drop_dim` option **breaking change**
+* remove `datetime` option **breaking change**
+* deprecate `VariablesExtension` extension
+* add `DatasetMetadataExtension` extension (`/dataset/keys`, `/dataset/` and `/dataset/dict` endpoints)
+
+### titiler.mosaic
+
+* add `/bbox` prefix to `/{minx},{miny},{maxx},{maxy}/assets` endpoint -> `/bbox/{minx},{miny},{maxx},{maxy}/assets` **breaking change**
+* add `/point` prefix to `{lon},{lat}/assets` endpoint -> `/point/{lon},{lat}/assets` **breaking change**
+* add `/tiles` prefix to `/{tileMatrixSetId}/{z}/{x}/{y}/assets` endpoint -> `/tiles/{tileMatrixSetId}/{z}/{x}/{y}/assets` **breaking change**
+* add `assets_accessor_dependency` dependency to the MosaicTileFactory to pass options to the backend's `get_assets` method.
+
+## 0.21.1 (2025-01-29)
+
+### titiler.core
+
+* add `slope` algorithm (@tayden, https://github.com/developmentseed/titiler/pull/1088)
+
+### titiler.xarray
+
+* Support Zarr-Python >=3 (author @maxrjones, https://github.com/developmentseed/titiler/pull/1082)
+
+## 0.21.0 (2025-01-24)
+
+### Misc
+
+* use `URN` style CRS notation in WMTS document
+
+* Unify Docker images (deprecate `titiler-uvicorn`)
+
+    ```
+    # Uvicorn
+    # before
+    docker run \
+        --platform=linux/amd64 \
+        -p 8000:8000 \
+        --env PORT=8000 \
+        --rm -it ghcr.io/developmentseed/titiler-uvicorn:latest
+
+    # now
+    docker run \
+        --platform=linux/amd64 \
+        -p 8000:8000 \
+        --rm -it ghcr.io/developmentseed/titiler:latest \
+        uvicorn titiler.application.main:app --host 0.0.0.0 --port 8000 --workers 1
+
+    # Gunicorn
+    # before
+    docker run \
+        --platform=linux/amd64 \
+        -p 8000:8000 \
+        --env PORT=8000 \
+        --rm -it ghcr.io/developmentseed/titiler:latest
+
+    # now
+    docker run \
+        --platform=linux/amd64 \
+        -p 8000:8000 \
+        --rm -it ghcr.io/developmentseed/titiler:latest \
+        gunicorn -k uvicorn.workers.UvicornWorker titiler.application.main:app --bind 0.0.0.0:8000 --workers 1
+    ```
+
+## 0.20.1 (2025-01-09)
+
+### titiler.xarray
+
+* pin python `zarr` to `>2,<3.0` to avoid zarr 3.0 breaking changes
+
+## 0.20.0 (2025-01-07)
+
+### titiler.core
+
+* add layer control to map viewer template (author @hrodmn, https://github.com/developmentseed/titiler/pull/1051)
+* improve query string handling in LowerCaseQueryStringMiddleware using urlencode (author @pratapvardhan, https://github.com/developmentseed/titiler/pull/1050)
+* add `titiler.core.utils.bounds_to_geometry` and reduce code duplication in factories (author @PratapVardhan, https://github.com/developmentseed/titiler/pull/1047)
+* simplify image format dtype validation in `render_image` (author @PratapVardhan, https://github.com/developmentseed/titiler/pull/1046)
+* remove `rescale_dependency` and `color_formula_dependency` attributes in `TilerFactory` class  **breaking change**
+* move `rescale` and `color_formula` QueryParameters dependencies in `ImageRenderingParams` class  **breaking change**
+* handle image rescaling and color_formula within `titiler.core.utils.render_image` function  **breaking change**
+* add `render_func: Callable[..., Tuple[bytes, str]] = render_image` attribute in `TilerFactory` class
+* add `castToInt`, `Floor`, `Ceil` algorithms
+
+### titiler.application
+
+* update `/healthz` endpoint to return dependencies versions (titiler, rasterio, gdal, ...) (author @scottyhq, https://github.com/developmentseed/titiler/pull/1056)
+* migrate `templates/index.html` to bootstrap5, remove unused css, reuse bs classes (author @PratapVardhan, https://github.com/developmentseed/titiler/pull/1048)
+
+### titiler.mosaic
+
+* remove `rescale_dependency` and `color_formula_dependency` attributes in `MosaicTilerFactory` class  **breaking change**
+* add `render_func: Callable[..., Tuple[bytes, str]] = render_image` attribute in `MosaicTilerFactory` class  **breaking change**
+
+### titiler.extensions
+
+* use `factory.render_func` as render function in `wmsExtension` endpoints
+* add `stacRenderExtension` which adds two endpoints: `/renders` (lists all renders) and `/renders/` (render metadata and links) (author @alekzvik, https://github.com/developmentseed/titiler/pull/1038)
+
+### Misc
+
+* Updated WMTS Capabilities template to avoid inserting extra new lines (author @AndrewAnnex, https://github.com/developmentseed/titiler/pull/1052).
+* Updated WMTS endpoint in titiler.mosaic and titiler.core to return layer bounds in coordinate ordering matching CRS order if WGS84 is not used (author @AndrewAnnex, https://github.com/developmentseed/titiler/pull/1052).
+* Remove `python3.8` support (author @pratapvardhan, https://github.com/developmentseed/titiler/pull/1058)
+* Add `python3.13` support (author @pratapvardhan, https://github.com/developmentseed/titiler/pull/1058)
+
+## 0.19.3 (2025-01-09)
+
+### titiler.xarray
+
+* pin python zarr to >2,<3.0 to avoid zarr 3.0 breaking changes [Backported from 0.20.1]
+
+
+## 0.19.2 (2024-11-28)
+
+### Misc
+
+* drop python 3.8 and add python 3.13 support (author @pratapvardhan, https://github.com/developmentseed/titiler/pull/1058)
+
+* Update package build backend from `pdm-pep517` to `pdm-backend` (https://backend.pdm-project.org/#migrate-from-pdm-pep517)
+
+* Update namespace package from using `.` to `-` as separator to comply with PEP-625 (https://peps.python.org/pep-0625/)
+
+### titiler.mosaic
+
+* Define variable (`MOSAIC_CONCURRENCY` and `MOSAIC_STRICT_ZOOM`) from env-variable outside endpoint code
+
+## 0.19.1 (2024-11-14)
+
+* Add `titiler` links in Map attributions
+
+## 0.19.0 (2024-11-07)
+
+### Misc
+
+* Remove default `WebMercatorQuad` tile matrix set in `/tiles`, `/tilesjson.json`, `/map` and `/WMTSCapabilities.xml` endpoints **breaking change**
+
+    ```
+    # Before
+    /tiles/{z}/{x}/{y}
+    /tilejson.json
+    /map
+    /WMTSCapabilities.xml
+
+    # Now
+    /tiles/WebMercatorQuad/{z}/{x}/{y}
+    /WebMercatorQuad/tilejson.json
+    /WebMercatorQuad/map
+    /WebMercatorQuad/WMTSCapabilities.xml
+    ```
+
+* Use `@attrs.define` instead of dataclass for factories **breaking change**
+* Use `@attrs.define` instead of dataclass for factory extensions **breaking change**
+* Handle `numpy` types in JSON/GeoJSON response
+* In the `map.html` template, use the tilejson's `minzoom` and `maxzoom` to populate `minNativeZoom` and `maxNativeZoom` parameters in leaflet `tileLayer` instead of `minZoom` and `maxZoom`
+
+### titiler.core
+
+* Update `rio-tiler` dependency to `>=7.0,<8.0`
+
+* Update `geojson-pydantic` dependency to `>=1.1.2,<2.0` which better handle antimeridian crossing dataset
+
+* handle `antimeridian` crossing bounds in `/info.geojson` endpoints (returning MultiPolygon instead of Polygon)
+
+* Improve XSS security for HTML templates (author @jcary741, https://github.com/developmentseed/titiler/pull/953)
+
+* Remove all default values to the dependencies **breaking change**
+
+    * `DatasetParams.unscale`: `False` -> `None` (default to `False` in rio-tiler)
+    * `DatasetParams.resampling_method`: `nearest` -> `None` (default to `nearest` in rio-tiler)
+    * `DatasetParams.reproject_method`: `nearest` -> `None` (default to `nearest` in rio-tiler)
+    * `ImageRenderingParams.add_mask`: `True` -> `None` (default to `True` in rio-tiler)
+    * `StatisticsParams.categorical`: `False` -> `None` (default to `False` in rio-tiler)
+
+* Add `as_dict(exclude_none=True/False)` method to the `DefaultDependency` class.
+
+    ```python
+    from typing import Optional
+    from titiler.core.dependencies import DefaultDependency
+    from dataclasses import dataclass
+
+    @dataclass
+    class Deps(DefaultDependency):
+        value: Optional[int] = None
+
+    print({**Deps().__dict__.items()})
+    >> {'value': None}
+
+    Deps().as_dict()  # `exclude_none` defaults to True
+    >> {}
+
+    Deps(value=1).as_dict()
+    >> {'value': 1}
+    ```
+
+* Fix Hillshade algorithm (bad `azimuth` angle)
+
+* Set default `azimuth` and `altitude` angles to 45º for the Hillshade algorithm **breaking change**
+
+* Use `.as_dict()` method when passing option to rio-tiler Reader's methods to avoid parameter conflicts when using custom Readers.
+
+* Rename `BaseTilerFactory` to `BaseFactory` **breaking change**
+
+* Remove useless attribute in `BaseFactory` (and moved them to `TilerFactory`) **breaking change**
+
+* Add `crs` option to `/bounds` endpoints to enable geographic_crs selection by the user
+
+* `/bounds` endpoints now return a `crs: str` attribute in the response
+
+* update `wmts.xml` template to support multiple layers
+
+* re-order endpoints parameters
+
+* avoid `lat/lon` overflow in `map` viewer
+
+* add OGC Tiles `/tiles` and `/tiles/{tileMatrixSet}` endpoints
+
+* add `gif` media type
+
+* `/point` endpoint returned masked values (`None` is nodata)
+
+### titiler.mosaic
+
+* Rename `reader` attribute to `backend` in `MosaicTilerFactory`  **breaking change**
+
+* Add `crs` option to `/bounds` endpoints to enable geographic_crs selection by the user
+
+* `/bounds` endpoints now return a `crs: str` attribute in the response
+
+* Update `cogeo-mosaic` dependency to `>=8.0,<9.0`
+
+* re-order endpoints parameters
+
+* add OGC Tiles `/tiles` and `/tiles/{tileMatrixSet}` endpoints
+
+* `/point` endpoint returned masked values (`None` is nodata)
+
+### titiler.extensions
+
+* Encode URL for cog_viewer and stac_viewer (author @guillemc23, https://github.com/developmentseed/titiler/pull/961)
+
+* Add links for render parameters and `/map` link to **viewer** dashboard (author @hrodmn, https://github.com/developmentseed/titiler/pull/987)
+
+* Update viewers to use `/info.geojson` endpoint instead of `/info`
+
+## 0.18.10 (2024-10-17)
+
+### titiler.application
+
+* update `starlette-cramjam` dependency and set compression-level default to `6`
+
+## 0.18.9 (2024-09-23)
+
+* fix release 0.18.8
+
+## 0.18.8 (2024-09-23)
+
+### titiler.extensions
+
+* Add links for render parameters and /map link to viewer dashboard (author @hrodmn, https://github.com/developmentseed/titiler/pull/987)
+
+## 0.18.7 (2024-09-19)
+
+* fix Hillshade algorithm (bad `azimuth` angle) (https://github.com/developmentseed/titiler/pull/985) [Backported]
+* Encode URL for cog_viewer and stac_viewer (author @guillemc23, https://github.com/developmentseed/titiler/pull/961) [Backported]
+* Improve XSS security for HTML templates (author @jcary741, https://github.com/developmentseed/titiler/pull/953) [Backported]
+
+## 0.18.6 (2024-08-27)
+
+* Switch back to `fastapi` instead of `fastapi-slim` and use `>=0.109.0` version
+
+## 0.18.5 (2024-07-03)
+
+* Set version requirement for FastAPI to `>=0.111.0`
+
+## 0.18.4 (2024-06-26)
+
+* fix Tiles URL encoding for WMTSCapabilities XML document
+
+## 0.18.3 (2024-05-20)
+
+* fix `WMTSCapabilities.xml` response for ArcMap compatibility
+    * replace `Cloud Optimized GeoTIFF` with dataset URL or `TiTiler` for the *ows:ServiceIdentification* **title**
+    * replace `cogeo` with `Dataset` for the `layer` *ows:Identifier*
+
+## 0.18.2 (2024-05-07)
+
+* move to `fastapi-slim` to avoid unwanted dependencies (author @n8sty, https://github.com/developmentseed/titiler/pull/815)
+
+## 0.18.1 (2024-04-12)
+
+### titiler.core
+
+* fix `TerrainRGB` algorithm name (author @JinIgarashi, https://github.com/developmentseed/titiler/pull/804)
+* add more tests for `RescalingParams` and `HistogramParams` dependencies
+* make sure to return *empty* content for `204` Error code
+
+## 0.18.0 (2024-03-22)
+
+### titiler.core
+
+* Add `ColorMapFactory` to create colorMap metadata endpoints (https://github.com/developmentseed/titiler/pull/796)
+* **Deprecation** remove default `WebMercatorQuad` tile matrix set in `/tiles`, `/tilesjson.json`, `/map` and `/WMTSCapabilities.xml` endpoints (https://github.com/developmentseed/titiler/pull/802)
+
+    ```
+    # Before
+    /tiles/{z}/{x}/{y}
+    /tilejson.json
+    /map
+    /WMTSCapabilities.xml
+
+    # Now
+    /tiles/WebMercatorQuad/{z}/{x}/{y}
+    /WebMercatorQuad/tilejson.json
+    /WebMercatorQuad/map
+    /WebMercatorQuad/WMTSCapabilities.xml
+    ```
+
+* **Deprecation** `default_tms` attribute in `BaseTilerFactory` (because `tileMatrixSetId` is now required in endpoints).
+
+### titiler.mosaic
+
+* **Deprecation** remove default `WebMercatorQuad` tile matrix set in `/tiles`, `/tilesjson.json`, `/map` and `/WMTSCapabilities.xml` endpoints (https://github.com/developmentseed/titiler/pull/802)
+
+    ```
+    # Before
+    /tiles/{z}/{x}/{y}
+    /tilejson.json
+    /map
+    /WMTSCapabilities.xml
+
+    # Now
+    /tiles/WebMercatorQuad/{z}/{x}/{y}
+    /WebMercatorQuad/tilejson.json
+    /WebMercatorQuad/map
+    /WebMercatorQuad/WMTSCapabilities.xml
+    ```
+
+* **Deprecation** `default_tms` attribute in `MosaicTilerFactory` (because `tileMatrixSetId` is now required in endpoints).
+
+### Misc
+
+* add `request` as first argument in `TemplateResponse` to adapt with latest starlette version
+
+## 0.17.3 (2024-03-21)
+
+### titiler.application
+
+* Add `extra="ignore"` option `ApiSettings` to fix pydantic issue when using `.env` file (author @imanshafiei540, https://github.com/developmentseed/titiler/pull/800)
+
+## 0.17.2 (2024-03-15)
+
+### titiler.core
+
+* fix OpenAPI metadata for algorithm (author @JinIgarashi, https://github.com/developmentseed/titiler/pull/797)
+
+## 0.17.1 (2024-03-13)
+
+* add python 3.12 support
+
+### titiler.core
+
+* Add `use_epsg` parameter to WMTS endpoint to resolve ArcMAP issues and fix XML formating (author @gadomski, https://github.com/developmentseed/titiler/pull/782)
+* Add more OpenAPI metadata for algorithm (author @JinIgarashi, https://github.com/developmentseed/titiler/pull/783)
+
+### titiler.application
+
+* fix invalid url parsing in HTML responses
+
+## 0.17.0 (2024-01-17)
+
+### titiler.core
+
+* update `rio-tiler` version to `>6.3.0`
+* use new `align_bounds_with_dataset=True` rio-tiler option in GeoJSON statistics methods for more precise calculation
+
+## 0.16.2 (2024-01-17)
+
+### titiler.core
+
+* fix leafletjs template maxZoom to great than 18 for `/map` endpoint (author @Firefishy, https://github.com/developmentseed/titiler/pull/749)
+
+## 0.16.1 (2024-01-08)
+
+### titiler.core
+
+* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method
+
+### titiler.mosaic
+
+* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method
+
+## 0.16.0 (2024-01-08)
+
+### titiler.core
+
+* update FastAPI version lower limit to `>=0.107.0`
+* fix template loading for starlette >= 0.28 by using `jinja2.Environment` argument (author @jasongi, https://github.com/developmentseed/titiler/pull/744)
+
+### titiler.extensions
+
+* fix template loading for starlette >= 0.28 by using `jinja2.Environment` argument (author @jasongi, https://github.com/developmentseed/titiler/pull/744)
+
+### titiler.application
+
+* fix template loading for starlette >= 0.28 by using `jinja2.Environment` argument (author @jasongi, https://github.com/developmentseed/titiler/pull/744)
+
+## 0.15.8 (2024-01-08)
+
+### titiler.core
+
+* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method [backported from 0.16.1]
+
+### titiler.mosaic
+
+* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method [backported from 0.16.1]
+
+## 0.15.7 (2024-01-08)
+
+### titiler.core
+
+* update FastAPI version upper limit to `<0.107.0` to avoid starlette breaking change (`0.28`)
+
+### titiler.application
+
+* add simple *auth* (optional) based on `global_access_token` string, set with `TITILER_API_GLOBAL_ACCESS_TOKEN` environment variable (author @DeflateAwning, https://github.com/developmentseed/titiler/pull/735)
+
+## 0.15.6 (2023-11-16)
+
+### titiler.core
+
+* in `/map` HTML response, add Lat/Lon buffer to AOI to avoid creating wrong AOI (when data covers the whole world).
+
+## 0.15.5 (2023-11-09)
+
+### titiler.core
+
+* add `algorithm` options for `/statistics` endpoints
+
+* switch from `BaseReader.statistics()` method to a combination of `BaseReader.preview()` and `ImageData.statistics()` methods to get the statistics
+
+## 0.15.4 (2023-11-06)
+
+### titiler.core
+
+* update `rio-tiler` requirement to `>=6.2.5,<7.0`
+
+* allow `bidx` option in `titiler.core.dependencies.AssetsBidxExprParams` and `titiler.core.dependencies.AssetsBidxParams`
+
+    ```python
+    # merge band 1 form asset1 and asset2
+    # before
+    httpx.get(
+        "/stac/preview",
+        params=(
+            ("url", "stac.json"),
+            ("assets", "asset1"),
+            ("assets", "asset2"),
+            ("asset_bidx", "asset1|1"),
+            ("asset_bidx", "asset2|1"),
+        )
+    )
+
+    # now
+    httpx.get(
+        "/stac/preview",
+        params=(
+            ("url", "stac.json"),
+            ("assets", "asset1"),
+            ("assets", "asset2"),
+            ("bidx", 1),
+        )
+    )
+    ```
+
+* fix openapi examples
+
+## 0.15.3 (2023-11-02)
+
+* add `dst_crs` options in `/statistics [POST]` and `/feature [POST]` endpoints
+
+## 0.15.2 (2023-10-23)
+
+### titiler.core
+
+* add `dependencies.TileParams` dependency with `buffer` and `padding` options
+* add `tile_dependency` attribute in `TilerFactory` class (defaults to `TileParams`)
+* add `reproject` (alias to `reproject_method`) option in `DatasetParams` dependency
+
+### titiler.mosaic
+
+*  Change `HTTP_404_NOT_FOUND` to `HTTP_204_NO_CONTENT` when no asset is found or tile is empty (author @simouel, https://github.com/developmentseed/titiler/pull/713)
+* add `tile_dependency` attribute in `MosaicTilerFactory` class (defaults to `TileParams`)
+
+### cdk application
+
+* Support non-root paths in AWS API Gateway Lambda handler (author @DanSchoppe, https://github.com/developmentseed/titiler/pull/716)
+
+## 0.15.1 (2023-10-17)
+
+* Allow a default `color_formula` parameter to be set via a dependency (author @samn, https://github.com/developmentseed/titiler/pull/707)
+* add `titiler.core.dependencies.create_colormap_dependency` to create ColorMapParams dependency from `rio_tiler.colormap.ColorMaps` object
+* add `py.typed` files in titiler submodules (https://peps.python.org/pep-0561)
+
+## 0.15.0 (2023-09-28)
+
+### titiler.core
+
+- added `PartFeatureParams` dependency
+
+**breaking changes**
+
+- `max_size` is now set to `None` for `/statistics [POST]`, `/bbox` and `/feature` endpoints, meaning the tiler will create image from the highest resolution.
+
+- renamed `titiler.core.dependencies.ImageParams` to `PreviewParams`
+
+- split TileFactory `img_dependency` attribute in two:
+  - `img_preview_dependency`: used in `/preview` and `/statistics [GET]`, default to `PreviewParams` (with `max_size=1024`)
+
+  - `img_part_dependency`: used in `/bbox`, `/feature` and `/statistics [POST]`, default to `PartFeatureParams` (with `max_size=None`)
+
+- renamed `/crop` endpoints to `/bbox/...` or `/feature/...`
+  - `/crop/{minx},{miny},{maxx},{maxy}.{format}` -> `/bbox/{minx},{miny},{maxx},{maxy}.{format}`
+
+  - `/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` -> `/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}`
+
+  - `/crop [POST]` -> `/feature [POST]`
+
+  - `/crop.{format} [POST]` -> `/feature.{format} [POST]`
+
+  - `/crop/{width}x{height}.{format}  [POST]` -> `/feature/{width}x{height}.{format} [POST]`
+
+- update `rio-tiler` requirement to `>=6.2.1`
+
+- Take coverage weights in account when generating statistics from GeoJSON features
+
+## 0.14.1 (2023-09-14)
+
+### titiler.extension
+
+* add `GetFeatureInfo` capability in `wmsExtension` (author @benjaminleighton, https://github.com/developmentseed/titiler/pull/698)
+
+## 0.14.0 (2023-08-30)
+
+### titiler.core
+
+* replace `-` by `_` in query parameters **breaking change**
+  - `coord-crs` -> `coord_crs`
+  - `dst-crs` -> `dst_crs`
+
+* replace `buffer` and `color_formula` endpoint parameters by external dependencies (`BufferParams` and `ColorFormulaParams`)
+
+* add `titiler.core.utils.render_image` which allow non-binary alpha band created with custom colormap. `render_image` replace `ImageData.render` method.
+
+    ```python
+    # before
+    if cmap := colormap or dst_colormap:
+        image = image.apply_colormap(cmap)
+
+    if not format:
+        format = ImageType.jpeg if image.mask.all() else ImageType.png
+
+    content = image.render(
+        img_format=format.driver,
+        **format.profile,
+        **render_params,
+    )
+
+    # now
+    # render_image will:
+    # - apply the colormap
+    # - choose the right output format if `None`
+    # - create the binary data
+    content, media_type = render_image(
+        image,
+        output_format=format,
+        colormap=colormap or dst_colormap,
+        **render_params,
+    )
+    ```
+
+### titiler.extension
+
+* rename `geom-densify-pts` to `geometry_densify` **breaking change**
+* rename `geom-precision` to `geometry_precision` **breaking change**
+
+## 0.13.3 (2023-08-27)
+
+* fix Factories `url_for` method and avoid changing `Request.path_params` object
+
+## 0.13.2 (2023-08-24)
+
+### titiler.extensions
+
+* replace mapbox-gl by maplibre
+* replace Stamen by OpenStreetMap tiles
+* simplify band selection handling (author @tayden, https://github.com/developmentseed/titiler/pull/688)
+
+## 0.13.1 (2023-08-21)
+
+### titiler.core
+
+* fix `LowerCaseQueryStringMiddleware` unexpectedly truncating query parameters (authors @jthetzel and @jackharrhy, @https://github.com/developmentseed/titiler/pull/677)
+
+## titiler.application
+
+* add `cors_allow_methods` in `ApiSettings` to control the CORS allowed methods (author @ubi15, https://github.com/developmentseed/titiler/pull/684)
+
+## 0.13.0 (2023-07-27)
+
+* update core requirements to libraries using pydantic **~=2.0**
+
+### titiler.core
+
+* update requirements:
+  * fastapi `>=0.95.1` --> `>=0.100.0`
+  * pydantic `~=1.0` --> `~=2.0`
+  * rio-tiler `>=5.0,<6.0` --> `>=6.0,<7.0`
+  * morecantile`>=4.3,<5.0` --> `>=5.0,<6.0`
+  * geojson-pydantic `>=0.4,<0.7` --> `>=1.0,<2.0`
+  * typing_extensions `>=4.6.1`
+
+### titiler.extension
+
+* update requirements:
+  * rio-cogeo `>=4.0,<5.0"` --> `>=5.0,<6.0"`
+
+### titiler.mosaic
+
+* update requirements:
+  * cogeo-mosaic `>=6.0,<7.0` --> `>=7.0,<8.0`
+
+### titiler.application
+
+* use `/api` and `/api.html` for documentation (instead of `/openapi.json` and `/docs`)
+* update landing page
+
+## 0.12.0 (2023-07-17)
+
+* use `Annotated` Type for Query/Path parameters
+* replace variable `TileMatrixSetId` by `tileMatrixSetId`
+
+### titiler.core
+
+* update FastAPI dependency to `>=0.95.1`
+* set `pydantic` dependency to `~=1.0`
+* update `rio-tiler` dependency to `>=5.0,<6.0`
+* update TMS endpoints to match OGC Tiles specification
+
+### titiler.extensions
+
+* use TiTiler's custom JSONResponse for the `/validate` endpoint to avoid issue when COG has `NaN` nodata value
+* update `rio-cogeo` dependency to `>=4.0,<5.0`
+* update `rio-stac` requirement to `>=0.8,<0.9` and add `geom-densify-pts` and `geom-precision` options
+
+## titiler.mosaic
+
+* update `cogeo-mosaic` dependency to `>=6.0,<7.0`
+* remove `titiler.mosaic.resources.enum.PixelSelectionMethod` and use `rio_tiler.mosaic.methods.PixelSelectionMethod`
+* allow more TileMatrixSet (than only `WebMercatorQuad`)
+
 ## 0.11.7 (2023-05-18)
 
 ### titiler.core
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a7105795c..58a1b1379 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,18 +2,19 @@
 
 Issues and pull requests are more than welcome: https://github.com/developmentseed/titiler/issues
 
+We recommand using [`uv`](https://docs.astral.sh/uv) as project manager for development.
+
+See https://docs.astral.sh/uv/getting-started/installation/ for installation 
+
 **dev install**
 
 ```bash
 git clone https://github.com/developmentseed/titiler.git
 cd titiler
 
-python -m pip install \
-   pre-commit \
-   -e src/titiler/core["test"] \
-   -e src/titiler/extensions["test,cogeo,stac"] \
-   -e src/titiler/mosaic["test"] \
-   -e src/titiler/application["test"]
+# Install the package in editable mode, plus the "dev" dependency group.
+# You can add `--group` arguments to add more groups, e.g. `--group notebook`.
+uv sync
 ```
 
 **pre-commit**
@@ -21,7 +22,10 @@ python -m pip install \
 This repo is set to use `pre-commit` to run *isort*, *flake8*, *pydocstring*, *black* ("uncompromising Python code formatter") and mypy when committing new code.
 
 ```bash
-pre-commit install
+uv run pre-commit install
+
+# If needed, you can run pre-commit script manually 
+uv run pre-commit run --all-files 
 ```
 
 ### Run tests
@@ -30,16 +34,19 @@ Each `titiler`'s modules has its own test suite which can be ran independently
 
 ```
 # titiler.core
-python -m pytest src/titiler/core --cov=titiler.core --cov-report=xml --cov-append --cov-report=term-missing
+uv run pytest src/titiler/core --cov=titiler.core --cov-report=xml --cov-append --cov-report=term-missing
 
 # titiler.extensions
-python -m pytest src/titiler/extensions --cov=titiler.extensions --cov-report=xml --cov-append --cov-report=term-missing
+uv run pytest src/titiler/extensions --cov=titiler.extensions --cov-report=xml --cov-append --cov-report=term-missing
 
 # titiler.mosaic
-python -m pytest src/titiler/mosaic --cov=titiler.mosaic --cov-report=xml --cov-append --cov-report=term-missing
+uv run pytest src/titiler/mosaic --cov=titiler.mosaic --cov-report=xml --cov-append --cov-report=term-missing
+
+# titiler.xarray
+uv run pytest src/titiler/xarray --cov=titiler.xarray --cov-report=xml --cov-append --cov-report=term-missing
 
 # titiler.application
-python -m pytest src/titiler/application --cov=titiler.application --cov-report=xml --cov-append --cov-report=term-missing
+uv run pytest src/titiler/application --cov=titiler.application --cov-report=xml --cov-append --cov-report=term-missing
 ```
 
 ### Docs
@@ -47,48 +54,20 @@ python -m pytest src/titiler/application --cov=titiler.application --cov-report=
 ```bash
 git clone https://github.com/developmentseed/titiler.git
 cd titiler
-python -m pip install nbconvert mkdocs mkdocs-material mkdocs-jupyter pygments pdocs
+
+# Build docs
+uv run --group docs mkdocs build -f docs/mkdocs.yml
 ```
 
 Hot-reloading docs:
 
 ```bash
-mkdocs serve -f docs/mkdocs.yml
+uv run --group docs mkdocs serve -f docs/mkdocs.yml --livereload
 ```
 
 To manually deploy docs (note you should never need to do this because Github
 Actions deploys automatically for new commits.):
 
 ```bash
-mkdocs gh-deploy -f docs/mkdocs.yml
-```
-
-```bash
-pdocs as_markdown \
-   --output_dir docs/src/api \
-   --exclude_source \
-   --overwrite \
-   titiler.core.dependencies \
-   titiler.core.factory \
-   titiler.core.utils \
-   titiler.core.routing \
-   titiler.core.errors \
-   titiler.core.resources.enums \
-   titiler.core.middleware
-
-pdocs as_markdown \
-   --output_dir docs/src/api \
-   --exclude_source \
-   --overwrite \
-   titiler.extensions.cogeo \
-   titiler.extensions.viewer \
-   titiler.extensions.stac
-
-pdocs as_markdown \
-   --output_dir docs/src/api \
-   --exclude_source \
-   --overwrite \
-   titiler.mosaic.factory \
-   titiler.mosaic.resources.enums \
-   titiler.mosaic.errors
+uv run --group docs mkdocs gh-deploy -f docs/mkdocs.yml
 ```
diff --git a/README.md b/README.md
index 44f4aed17..032ff3e16 100644
--- a/README.md
+++ b/README.md
@@ -10,8 +10,8 @@
   
       Coverage
   
-  
-      Package version
+  
+      Package version
   
   
       Downloads
@@ -26,13 +26,13 @@
 
 ---
 
-**Documentation**: https://devseed.com/titiler/
+**Documentation**: https://devseed.com/titiler/
 
 **Source Code**: https://github.com/developmentseed/titiler
 
 ---
 
-`Titiler`, pronounced **tee-tiler** (*ti* is the diminutive version of the french *petit* which means small), is a set of python modules that focus on creating FastAPI application for dynamic tiling.
+`TiTiler`, pronounced **tee-tiler** (*ti* is the diminutive version of the french *petit* which means small), is a set of python modules that focus on creating FastAPI application for dynamic tiling.
 
 Note: This project is the descendant of [`cogeo-tiler`](https://github.com/developmentseed/cogeo-tiler) and [`cogeo-mosaic-tiler`](https://github.com/developmentseed/cogeo-mosaic-tiler).
 
@@ -42,11 +42,13 @@ Note: This project is the descendant of [`cogeo-tiler`](https://github.com/devel
 - [Cloud Optimized GeoTIFF](http://www.cogeo.org/) support
 - [SpatioTemporal Asset Catalog](https://stacspec.org) support
 - Multiple projections support (see [TileMatrixSets](https://www.ogc.org/standards/tms)) via [`morecantile`](https://github.com/developmentseed/morecantile).
+- Multi-Dimensional (Zarr) dataset support via [Xarray](https://github.com/pydata/xarray)
 - JPEG / JP2 / PNG / WEBP / GTIFF / NumpyTile output format support
-- OGC WMTS support
+- OGC RESTful WMTS / OGC Tiles API support
+- Partial support of OGC Maps API
 - Automatic OpenAPI documentation (FastAPI builtin)
 - Virtual mosaic support (via [MosaicJSON](https://github.com/developmentseed/mosaicjson-spec/))
-- Example of AWS Lambda / ECS deployment (via CDK)
+- Example of AWS Lambda / ECS deployment (via CDK) / K8s Helm chart
 
 ## Packages
 
@@ -55,13 +57,20 @@ Starting with version `0.3.0`, the `TiTiler` python module has been split into a
 | Package | Version |  Description
 | ------- | ------- |-------------
 [**titiler.core**](https://github.com/developmentseed/titiler/tree/main/src/titiler/core) | [![titiler.core](https://img.shields.io/pypi/v/titiler.core?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.core) | The `Core` package contains libraries to help create a  dynamic tiler for COG and STAC
+[**titiler.xarray**](https://github.com/developmentseed/titiler/tree/main/src/titiler/xarray) | [![titiler.xarray](https://img.shields.io/pypi/v/titiler.xarray?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.xarray) | The `xarray` package contains libraries to help create a  dynamic tiler for Zarr/NetCDF datasets
 [**titiler.extensions**](https://github.com/developmentseed/titiler/tree/main/src/titiler/extensions) | [![titiler.extensions](https://img.shields.io/pypi/v/titiler.extensions?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.extensions) | TiTiler's extensions package. Contains extensions for Tiler Factories.
 [**titiler.mosaic**](https://github.com/developmentseed/titiler/tree/main/src/titiler/mosaic) | [![titiler.mosaic](https://img.shields.io/pypi/v/titiler.mosaic?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.mosaic) | The `mosaic` package contains libraries to help create a dynamic tiler for MosaicJSON (adds `cogeo-mosaic` requirement)
-[**titiler.application**](https://github.com/developmentseed/titiler/tree/main/src/titiler/application) | [![titiler.application](https://img.shields.io/pypi/v/titiler.application?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.application) | TiTiler's `demo` package. Contains a FastAPI application with full support of COG, STAC and MosaicJSON
+[**titiler.application**](https://github.com/developmentseed/titiler/tree/main/src/titiler/application) | [![titiler.application](https://img.shields.io/pypi/v/titiler.application?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.application) | TiTiler's `demo` package. Contains a FastAPI application with full support of COG, Zarr, STAC and MosaicJSON
 
 
 ## Installation
 
+> [!WARNING]
+>
+> Do not install the package named `titiler` from PyPI.
+> In late 2025, we [dropped support for this metapackage](https://github.com/developmentseed/titiler/issues/294);
+> now you must install TiTiler from the package names shown below.
+
 To install from PyPI and run:
 
 ```bash
@@ -71,9 +80,10 @@ python -m pip install -U pip
 python -m pip  install titiler.{package}
 # e.g.,
 # python -m pip  install titiler.core
+# python -m pip  install titiler.xarray
 # python -m pip  install titiler.extensions
 # python -m pip  install titiler.mosaic
-# python -m pip  install titiler.application (also installs core, extensions and mosaic)
+# python -m pip  install titiler.application (also installs core, extensions, xarray and mosaic)
 
 # Install uvicorn to run the FastAPI application locally
 python -m pip install uvicorn
@@ -88,11 +98,8 @@ To install from sources and run for development:
 git clone https://github.com/developmentseed/titiler.git
 cd titiler
 
-python -m pip install -U pip
-python -m pip install -e src/titiler/core -e src/titiler/extensions -e src/titiler/mosaic -e src/titiler/application
-python -m pip install uvicorn
-
-uvicorn titiler.application.main:app --reload
+uv sync --group server
+uv run uvicorn titiler.application.main:app --reload
 ```
 
 ## Docker
@@ -102,11 +109,11 @@ Ready to use/deploy images can be found on Github registry.
 - https://github.com/developmentseed/titiler/pkgs/container/titiler
 
 ```bash
-docker run --name titiler \
+docker run \
+    --platform=linux/amd64 \
     -p 8000:8000 \
-    --env PORT=8000 \
-    --env WORKERS_PER_CORE=1 \
-    --rm -it ghcr.io/developmentseed/titiler:latest
+    --rm -it ghcr.io/developmentseed/titiler:latest \
+    uvicorn titiler.application.main:app --host 0.0.0.0 --port 8000 --workers 1
 ```
 
 - Built the docker locally
@@ -114,17 +121,16 @@ docker run --name titiler \
 git clone https://github.com/developmentseed/titiler.git
 cd titiler
 
-docker-compose up --build titiler  # or titiler-uvicorn
+docker compose up --build titiler
 ```
 
-Some options can be set via environment variables, see: https://github.com/tiangolo/uvicorn-gunicorn-docker#advanced-usage
-
 ## Project structure
 
 ```
 src/titiler/                     - titiler modules.
  ├── application/                - Titiler's `Application` package
  ├── extensions/                 - Titiler's `Extensions` package
+ ├── xarray/                     - Titiler's `Xarray` package
  ├── core/                       - Titiler's `Core` package
  └── mosaic/                     - Titiler's `Mosaic` package
 ```
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 000000000..b37701696
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,19 @@
+# Releasing
+
+This is a checklist for releasing a new version of **titiler**.
+
+1. Create a release branch named `release/vX.Y.Z`, where `X.Y.Z` is the new version
+
+2. Make sure the [Changelog](CHANGES.md) is up to date with latest changes and release date set
+
+3. Update `version: {chart_version}` (e.g: `version: 1.1.6 -> version: 1.1.7`) in `deployment/k8s/charts/Chart.yaml`
+
+4. Run [`bump-my-version`](https://callowayproject.github.io/bump-my-version/) to update all titiler's module versions: `uv run --with bump-my-version --isolated bump-my-version bump minor --new-version 0.20.0`
+
+5. Push your release branch, create a PR, and get approval
+
+6. Once the PR is merged, create a new (annotated, signed) tag on the appropriate commit. Name the tag `X.Y.Z`, and include `vX.Y.Z` as its annotation message
+
+7. Push your tag to Github, which will kick off the publishing workflow
+
+8. Create a [new release](https://github.com/developmentseed/titiler/releases/new) targeting the new tag, and use the "Generate release notes" feature to populate the description. Publish the release and mark it as the latest
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..63e114021
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,73 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+If you find any vulnerabilities in `titiler`, don't hesitate to _report them_.
+
+1. Use GitHub's security reporting tools.
+
+see https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability
+
+2. Describe the vulnerability.
+
+   If you have a fix, that is most welcome -- please attach or summarize it in your message!
+
+3. We will evaluate the vulnerability and, if necessary, release a fix or mitigating steps to address it. We will contact you to let you know the outcome, and will credit you in the report.
+
+   Please **do not disclose the vulnerability publicly** until a fix is released!
+
+4. Once we have either a) published a fix, or b) declined to address the vulnerability for whatever reason, you are free to publicly disclose it.
+
+
+## GDAL
+
+`TiTiler` is built on top of Rasterio which is a python wrapper for the [GDAL](https://gdal.org/en/stable/) C++ library. At the time of writing, GDAL is responsible for most of the I/O and thus is where vulnerabilities could be harmful. For any `I/O` issues please first check [GDAL documentation](https://gdal.org/en/stable/user/security.html#security-considerations).
+
+#### GDAL VRT Driver
+
+There is a known security vulnerability with the VRT Driver:
+
+> It can be used to access any valid GDAL dataset. If a hostile party, with knowledge of the location on the filesystem of a valid GDAL dataset, convinces a user to run gdal_translate a VRT file and give it back the result, it might be able to steal data. That could potentially be able for a web service accepting data from the user, converting it, and sending back the result.
+
+see https://gdal.org/en/stable/user/security.html#gdal-vrt-driver
+
+Thus we recommend deploying titiler in infrastructure with limited access to the filesystem. Users can also `disable` the VRT driver completely by using `GDAL_SKIP=VRT` environment variable.
+
+In GDAL 3.12, new environment variables might be introduced to enable more control over the VRT driver: https://github.com/OSGeo/gdal/pull/12669
+
+#### Limit source's host
+
+If users want to limit the sources that the application can access, they can also create custom `path_dependency` such as this one which limits valid sources to a list of known hosts:
+
+```python
+from urllib.parse import urlparse
+
+from typing import Annotated
+from titiler.core.factory import TilerFactory
+from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers
+
+from fastapi import FastAPI, Query, HTTPException
+
+# List of known host where dataset can be read from
+known_host = [
+   "devseed.org",
+]
+
+def DatasetPathParams(url: Annotated[str, Query(description="Dataset URL")]) -> str:
+   """Create dataset path from args"""
+   # validate Dataset host
+   parsed = urlparse(url)
+   if parsed.netloc not in known_host:
+      raise HTTPException(
+         status_code=400,
+         detail="Nope, this is not a valid File - Please Try Again",
+      )
+
+   return url
+
+
+app = FastAPI(title="My simple app")
+app.include_router(TilerFactory(path_dependency=DatasetPathParams).router)
+
+add_exception_handlers(app, DEFAULT_STATUS_CODES)
+```
diff --git a/deployment/aws/README.md b/deployment/aws/README.md
index de6c243f9..742810b87 100644
--- a/deployment/aws/README.md
+++ b/deployment/aws/README.md
@@ -4,4 +4,3 @@ Intro: https://developmentseed.org/titiler/deployment/aws/intro/
 
 AWS lambda: https://developmentseed.org/titiler/deployment/aws/lambda/
 
-ECS/Fargate: https://developmentseed.org/titiler/deployment/aws/ecs/
diff --git a/deployment/aws/app.py b/deployment/aws/app.py
new file mode 100644
index 000000000..449100f4a
--- /dev/null
+++ b/deployment/aws/app.py
@@ -0,0 +1,161 @@
+"""Construct App."""
+
+import os
+from typing import Any, Dict, List, Optional
+
+from aws_cdk import App, CfnOutput, Duration, Stack, Tags
+from aws_cdk import aws_apigatewayv2 as apigw
+from aws_cdk import aws_iam as iam
+from aws_cdk import aws_lambda
+from aws_cdk import aws_logs as logs
+from aws_cdk.aws_apigatewayv2_integrations import HttpLambdaIntegration
+from constructs import Construct
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+class StackSettings(BaseSettings):
+    """Application settings"""
+
+    name: str = "titiler"
+    stage: str = "production"
+
+    owner: Optional[str] = None
+    client: Optional[str] = None
+
+    # Default options are optimized for CloudOptimized GeoTIFF
+    # For more information on GDAL env see: https://gdal.org/user/configoptions.html
+    # or https://developmentseed.org/titiler/advanced/performance_tuning/
+    env: Dict = {
+        "GDAL_CACHEMAX": "200",  # 200 mb
+        "GDAL_DISABLE_READDIR_ON_OPEN": "EMPTY_DIR",
+        "GDAL_INGESTED_BYTES_AT_OPEN": "32768",  # get more bytes when opening the files.
+        "GDAL_HTTP_MERGE_CONSECUTIVE_RANGES": "YES",
+        "GDAL_HTTP_MULTIPLEX": "YES",
+        "GDAL_HTTP_VERSION": "2",
+        "PYTHONWARNINGS": "ignore",
+        "VSI_CACHE": "TRUE",
+        "VSI_CACHE_SIZE": "5000000",  # 5 MB (per file-handle)
+    }
+
+    # S3 bucket names where TiTiler could do HEAD and GET Requests
+    # specific private and public buckets MUST be added if you want to use s3:// urls
+    # You can whitelist all bucket by setting `*`.
+    # ref: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-arn-format.html
+    buckets: List = []
+
+    # S3 key pattern to limit the access to specific items (e.g: "my_data/*.tif")
+    key: str = "*"
+
+    ###########################################################################
+    # AWS LAMBDA
+    # The following settings only apply to AWS Lambda deployment
+    # more about lambda config: https://www.sentiatechblog.com/aws-re-invent-2020-day-3-optimizing-lambda-cost-with-multi-threading
+    timeout: int = 10
+    memory: int = 1536
+
+    # The maximum of concurrent executions you want to reserve for the function.
+    # Default: - No specific limit - account limit.
+    max_concurrent: Optional[int] = None
+
+    model_config = SettingsConfigDict(env_prefix="TITILER_STACK_", env_file=".env")
+
+
+class titilerLambdaStack(Stack):
+    """
+    Titiler Lambda Stack
+
+    This code is freely adapted from
+    - https://github.com/leothomas/titiler/blob/10df64fbbdd342a0762444eceebaac18d8867365/stack/app.py author: @leothomas
+    - https://github.com/ciaranevans/titiler/blob/3a4e04cec2bd9b90e6f80decc49dc3229b6ef569/stack/app.py author: @ciaranevans
+
+    """
+
+    def __init__(
+        self,
+        scope: Construct,
+        id: str,
+        memory: int = 1024,
+        timeout: int = 30,
+        runtime: aws_lambda.Runtime = aws_lambda.Runtime.PYTHON_3_14,
+        concurrent: Optional[int] = None,
+        permissions: Optional[List[iam.PolicyStatement]] = None,
+        environment: Optional[Dict] = None,
+        code_dir: str = "./",
+        **kwargs: Any,
+    ) -> None:
+        """Define stack."""
+        super().__init__(scope, id, **kwargs)
+
+        permissions = permissions or []
+        environment = environment or {}
+
+        # COG / STAC / MosaicJSON / Zarr endpoints
+        lambda_function = aws_lambda.Function(
+            self,
+            f"{id}-lambda",
+            runtime=runtime,
+            code=aws_lambda.Code.from_docker_build(
+                path=os.path.abspath(code_dir),
+                file="lambda/Dockerfile",
+                platform="linux/amd64",
+                build_args={
+                    "PYTHON_VERSION": "3.14",
+                },
+            ),
+            handler="handler.handler",
+            memory_size=memory,
+            reserved_concurrent_executions=concurrent,
+            timeout=Duration.seconds(timeout),
+            environment=environment,
+            log_retention=logs.RetentionDays.ONE_WEEK,
+        )
+
+        for perm in permissions:
+            lambda_function.add_to_role_policy(perm)
+
+        api = apigw.HttpApi(
+            self,
+            f"{id}-endpoint",
+            default_integration=HttpLambdaIntegration(
+                f"{id}-integration", handler=lambda_function
+            ),
+        )
+        CfnOutput(self, "Endpoint", value=api.url)
+
+
+app = App()
+settings = StackSettings()
+
+perms = []
+if settings.buckets:
+    perms.append(
+        iam.PolicyStatement(
+            actions=["s3:GetObject"],
+            resources=[
+                f"arn:aws:s3:::{bucket}/{settings.key}" for bucket in settings.buckets
+            ],
+        )
+    )
+
+lambda_stack = titilerLambdaStack(
+    app,
+    f"{settings.name}-lambda-{settings.stage}",
+    memory=settings.memory,
+    timeout=settings.timeout,
+    concurrent=settings.max_concurrent,
+    permissions=perms,
+    environment=settings.env,
+)
+
+# Tag infrastructure
+for key, value in {
+    "Project": settings.name,
+    "Stack": settings.stage,
+    "Owner": settings.owner,
+    "Client": settings.client,
+}.items():
+    if value:
+        Tags.of(lambda_stack).add(key, value)
+
+
+app.synth()
diff --git a/deployment/aws/cdk.json b/deployment/aws/cdk.json
index 57cc60ce6..787a71dd6 100644
--- a/deployment/aws/cdk.json
+++ b/deployment/aws/cdk.json
@@ -1,3 +1,3 @@
 {
-    "app": "python3 cdk/app.py"
+    "app": "python3 app.py"
 }
diff --git a/deployment/aws/cdk/__init__.py b/deployment/aws/cdk/__init__.py
deleted file mode 100644
index 4955682f0..000000000
--- a/deployment/aws/cdk/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""AWS App."""
diff --git a/deployment/aws/cdk/app.py b/deployment/aws/cdk/app.py
deleted file mode 100644
index 235baa35f..000000000
--- a/deployment/aws/cdk/app.py
+++ /dev/null
@@ -1,212 +0,0 @@
-"""Construct App."""
-
-import os
-from typing import Any, Dict, List, Optional, Union
-
-from aws_cdk import App, CfnOutput, Duration, Stack, Tags
-from aws_cdk import aws_apigatewayv2_alpha as apigw
-from aws_cdk import aws_ec2 as ec2
-from aws_cdk import aws_ecs as ecs
-from aws_cdk import aws_ecs_patterns as ecs_patterns
-from aws_cdk import aws_iam as iam
-from aws_cdk import aws_lambda
-from aws_cdk import aws_logs as logs
-from aws_cdk.aws_apigatewayv2_integrations_alpha import HttpLambdaIntegration
-from config import StackSettings
-from constructs import Construct
-
-settings = StackSettings()
-
-
-class titilerLambdaStack(Stack):
-    """
-    Titiler Lambda Stack
-
-    This code is freely adapted from
-    - https://github.com/leothomas/titiler/blob/10df64fbbdd342a0762444eceebaac18d8867365/stack/app.py author: @leothomas
-    - https://github.com/ciaranevans/titiler/blob/3a4e04cec2bd9b90e6f80decc49dc3229b6ef569/stack/app.py author: @ciaranevans
-
-    """
-
-    def __init__(
-        self,
-        scope: Construct,
-        id: str,
-        memory: int = 1024,
-        timeout: int = 30,
-        runtime: aws_lambda.Runtime = aws_lambda.Runtime.PYTHON_3_10,
-        concurrent: Optional[int] = None,
-        permissions: Optional[List[iam.PolicyStatement]] = None,
-        environment: Optional[Dict] = None,
-        code_dir: str = "./",
-        **kwargs: Any,
-    ) -> None:
-        """Define stack."""
-        super().__init__(scope, id, **kwargs)
-
-        permissions = permissions or []
-        environment = environment or {}
-
-        lambda_function = aws_lambda.Function(
-            self,
-            f"{id}-lambda",
-            runtime=runtime,
-            code=aws_lambda.Code.from_docker_build(
-                path=os.path.abspath(code_dir),
-                file="lambda/Dockerfile",
-            ),
-            handler="handler.handler",
-            memory_size=memory,
-            reserved_concurrent_executions=concurrent,
-            timeout=Duration.seconds(timeout),
-            environment=environment,
-            log_retention=logs.RetentionDays.ONE_WEEK,
-        )
-
-        for perm in permissions:
-            lambda_function.add_to_role_policy(perm)
-
-        api = apigw.HttpApi(
-            self,
-            f"{id}-endpoint",
-            default_integration=HttpLambdaIntegration(
-                f"{id}-integration", handler=lambda_function
-            ),
-        )
-        CfnOutput(self, "Endpoint", value=api.url)
-
-
-class titilerECSStack(Stack):
-    """Titiler ECS Fargate Stack."""
-
-    def __init__(
-        self,
-        scope: Construct,
-        id: str,
-        cpu: Union[int, float] = 256,
-        memory: Union[int, float] = 512,
-        mincount: int = 1,
-        maxcount: int = 50,
-        permissions: Optional[List[iam.PolicyStatement]] = None,
-        environment: Optional[Dict] = None,
-        code_dir: str = "./",
-        **kwargs: Any,
-    ) -> None:
-        """Define stack."""
-        super().__init__(scope, id, *kwargs)
-
-        permissions = permissions or []
-        environment = environment or {}
-
-        vpc = ec2.Vpc(self, f"{id}-vpc", max_azs=2)
-
-        cluster = ecs.Cluster(self, f"{id}-cluster", vpc=vpc)
-
-        task_env = environment.copy()
-        task_env.update({"LOG_LEVEL": "error"})
-
-        # GUNICORN configuration
-        if settings.workers_per_core:
-            task_env.update({"WORKERS_PER_CORE": str(settings.workers_per_core)})
-        if settings.max_workers:
-            task_env.update({"MAX_WORKERS": str(settings.max_workers)})
-        if settings.web_concurrency:
-            task_env.update({"WEB_CONCURRENCY": str(settings.web_concurrency)})
-
-        fargate_service = ecs_patterns.ApplicationLoadBalancedFargateService(
-            self,
-            f"{id}-service",
-            cluster=cluster,
-            cpu=cpu,
-            memory_limit_mib=memory,
-            desired_count=mincount,
-            public_load_balancer=True,
-            listener_port=80,
-            task_image_options=ecs_patterns.ApplicationLoadBalancedTaskImageOptions(
-                image=ecs.ContainerImage.from_registry(
-                    f"ghcr.io/developmentseed/titiler:{settings.image_version}",
-                ),
-                container_port=80,
-                environment=task_env,
-            ),
-        )
-        fargate_service.target_group.configure_health_check(path="/healthz")
-
-        for perm in permissions:
-            fargate_service.task_definition.task_role.add_to_policy(perm)
-
-        scalable_target = fargate_service.service.auto_scale_task_count(
-            min_capacity=mincount, max_capacity=maxcount
-        )
-
-        # https://github.com/awslabs/aws-rails-provisioner/blob/263782a4250ca1820082bfb059b163a0f2130d02/lib/aws-rails-provisioner/scaling.rb#L343-L387
-        scalable_target.scale_on_request_count(
-            "RequestScaling",
-            requests_per_target=50,
-            scale_in_cooldown=Duration.seconds(240),
-            scale_out_cooldown=Duration.seconds(30),
-            target_group=fargate_service.target_group,
-        )
-
-        # scalable_target.scale_on_cpu_utilization(
-        #     "CpuScaling", target_utilization_percent=70,
-        # )
-
-        fargate_service.service.connections.allow_from_any_ipv4(
-            port_range=ec2.Port(
-                protocol=ec2.Protocol.ALL,
-                string_representation="All port 80",
-                from_port=80,
-            ),
-            description="Allows traffic on port 80 from ALB",
-        )
-
-
-app = App()
-
-perms = []
-if settings.buckets:
-    perms.append(
-        iam.PolicyStatement(
-            actions=["s3:GetObject"],
-            resources=[
-                f"arn:aws:s3:::{bucket}/{settings.key}" for bucket in settings.buckets
-            ],
-        )
-    )
-
-
-ecs_stack = titilerECSStack(
-    app,
-    f"{settings.name}-ecs-{settings.stage}",
-    cpu=settings.task_cpu,
-    memory=settings.task_memory,
-    mincount=settings.min_ecs_instances,
-    maxcount=settings.max_ecs_instances,
-    permissions=perms,
-    environment=settings.env,
-)
-
-lambda_stack = titilerLambdaStack(
-    app,
-    f"{settings.name}-lambda-{settings.stage}",
-    memory=settings.memory,
-    timeout=settings.timeout,
-    concurrent=settings.max_concurrent,
-    permissions=perms,
-    environment=settings.env,
-)
-
-# Tag infrastructure
-for key, value in {
-    "Project": settings.name,
-    "Stack": settings.stage,
-    "Owner": settings.owner,
-    "Client": settings.client,
-}.items():
-    if value:
-        Tags.of(ecs_stack).add(key, value)
-        Tags.of(lambda_stack).add(key, value)
-
-
-app.synth()
diff --git a/deployment/aws/cdk/config.py b/deployment/aws/cdk/config.py
deleted file mode 100644
index fa016b193..000000000
--- a/deployment/aws/cdk/config.py
+++ /dev/null
@@ -1,92 +0,0 @@
-"""TITILER_STACK Configs."""
-
-from typing import Dict, List, Optional
-
-import pydantic
-
-
-class StackSettings(pydantic.BaseSettings):
-    """Application settings"""
-
-    name: str = "titiler"
-    stage: str = "production"
-
-    owner: Optional[str]
-    client: Optional[str]
-
-    # Default options are optimized for CloudOptimized GeoTIFF
-    # For more information on GDAL env see: https://gdal.org/user/configoptions.html
-    # or https://developmentseed.org/titiler/advanced/performance_tuning/
-    env: Dict = {
-        "GDAL_CACHEMAX": "200",  # 200 mb
-        "GDAL_DISABLE_READDIR_ON_OPEN": "EMPTY_DIR",
-        "GDAL_INGESTED_BYTES_AT_OPEN": "32768",  # get more bytes when opening the files.
-        "GDAL_HTTP_MERGE_CONSECUTIVE_RANGES": "YES",
-        "GDAL_HTTP_MULTIPLEX": "YES",
-        "GDAL_HTTP_VERSION": "2",
-        "PYTHONWARNINGS": "ignore",
-        "VSI_CACHE": "TRUE",
-        "VSI_CACHE_SIZE": "5000000",  # 5 MB (per file-handle)
-    }
-
-    # S3 bucket names where TiTiler could do HEAD and GET Requests
-    # specific private and public buckets MUST be added if you want to use s3:// urls
-    # You can whitelist all bucket by setting `*`.
-    # ref: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-arn-format.html
-    buckets: List = []
-
-    # S3 key pattern to limit the access to specific items (e.g: "my_data/*.tif")
-    key: str = "*"
-
-    ###########################################################################
-    # AWS ECS
-    # The following settings only apply to AWS ECS deployment
-    min_ecs_instances: int = 5
-    max_ecs_instances: int = 50
-
-    # CPU value      |   Memory value
-    # 256 (.25 vCPU) | 0.5 GB, 1 GB, 2 GB
-    # 512 (.5 vCPU)  | 1 GB, 2 GB, 3 GB, 4 GB
-    # 1024 (1 vCPU)  | 2 GB, 3 GB, 4 GB, 5 GB, 6 GB, 7 GB, 8 GB
-    # 2048 (2 vCPU)  | Between 4 GB and 16 GB in 1-GB increments
-    # 4096 (4 vCPU)  | Between 8 GB and 30 GB in 1-GB increments
-    task_cpu: int = 256
-    task_memory: int = 512
-
-    # GUNICORN configuration
-    # Ref: https://github.com/developmentseed/titiler/issues/119
-
-    # WORKERS_PER_CORE
-    # This image will check how many CPU cores are available in the current server running your container.
-    # It will set the number of workers to the number of CPU cores multiplied by this value.
-    workers_per_core: int = 1
-
-    # MAX_WORKERS
-    # You can use it to let the image compute the number of workers automatically but making sure it's limited to a maximum.
-    # should depends on `task_cpu`
-    max_workers: int = 1
-
-    # WEB_CONCURRENCY
-    # Override the automatic definition of number of workers.
-    # Set to the number of CPU cores in the current server multiplied by the environment variable WORKERS_PER_CORE.
-    # So, in a server with 2 cores, by default it will be set to 2.
-    web_concurrency: Optional[int]
-
-    image_version: str = "latest"
-
-    ###########################################################################
-    # AWS LAMBDA
-    # The following settings only apply to AWS Lambda deployment
-    # more about lambda config: https://www.sentiatechblog.com/aws-re-invent-2020-day-3-optimizing-lambda-cost-with-multi-threading
-    timeout: int = 10
-    memory: int = 1536
-
-    # The maximum of concurrent executions you want to reserve for the function.
-    # Default: - No specific limit - account limit.
-    max_concurrent: Optional[int]
-
-    class Config:
-        """model config"""
-
-        env_file = ".env"
-        env_prefix = "TITILER_STACK_"
diff --git a/deployment/aws/lambda/Dockerfile b/deployment/aws/lambda/Dockerfile
index 9e8fc276e..2a297b5d0 100644
--- a/deployment/aws/lambda/Dockerfile
+++ b/deployment/aws/lambda/Dockerfile
@@ -1,19 +1,29 @@
-ARG PYTHON_VERSION=3.10
+ARG PYTHON_VERSION=3.14
 
 FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION}
 
 WORKDIR /tmp
 
-RUN pip install pip -U
-RUN pip install "titiler.application==0.11.7" "mangum>=0.10.0" -t /asset --no-binary pydantic
+# Install system dependencies to compile (numexpr)
+RUN dnf install -y gcc-c++ && dnf clean all
+
+RUN python -m pip install pip -U
+RUN python -m pip install "titiler-application==2.0.0b2" "mangum==0.19.0" "cftime" -t /asset --no-binary pydantic,xarray,numpy,pandas
 
 # Reduce package size and remove useless files
 RUN cd /asset && find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done;
 RUN cd /asset && find . -type d -a -name '__pycache__' -print0 | xargs -0 rm -rf
 RUN cd /asset && find . -type f -a -name '*.py' -print0 | xargs -0 rm -f
+RUN cd /asset && find . -type f -name '*.so*' -not -path "*/numpy.libs/*" -exec strip --strip-unneeded {} \; 2>/dev/null || true
 RUN find /asset -type d -a -name 'tests' -print0 | xargs -0 rm -rf
 RUN rm -rdf /asset/numpy/doc/ /asset/boto3* /asset/botocore* /asset/bin /asset/geos_license /asset/Misc
 
 COPY lambda/handler.py /asset/handler.py
 
+# Ref: https://github.com/developmentseed/titiler/discussions/1108#discussioncomment-13045681
+RUN cp /usr/lib64/libexpat.so.1 /asset/
+
+WORKDIR /asset
+RUN python -c "from handler import handler; print('All Good')"
+
 CMD ["echo", "hello world"]
diff --git a/deployment/aws/lambda/handler.py b/deployment/aws/lambda/handler.py
index 4e66c7b2e..16ae56978 100644
--- a/deployment/aws/lambda/handler.py
+++ b/deployment/aws/lambda/handler.py
@@ -1,12 +1,21 @@
 """AWS Lambda handler."""
 
+import asyncio
 import logging
 
 from mangum import Mangum
 
 from titiler.application.main import app
+from titiler.application.settings import ApiSettings
+
+try:
+    asyncio.get_event_loop()
+except RuntimeError:
+    asyncio.set_event_loop(asyncio.new_event_loop())
 
 logging.getLogger("mangum.lifespan").setLevel(logging.ERROR)
 logging.getLogger("mangum.http").setLevel(logging.ERROR)
 
-handler = Mangum(app, lifespan="auto")
+api_settings = ApiSettings()
+
+handler = Mangum(app, api_gateway_base_path=api_settings.root_path, lifespan="auto")
diff --git a/deployment/aws/package-lock.json b/deployment/aws/package-lock.json
deleted file mode 100644
index ad5226462..000000000
--- a/deployment/aws/package-lock.json
+++ /dev/null
@@ -1,81 +0,0 @@
-{
-  "name": "cdk-deploy",
-  "version": "0.1.0",
-  "lockfileVersion": 2,
-  "requires": true,
-  "packages": {
-    "": {
-      "name": "cdk-deploy",
-      "version": "0.1.0",
-      "license": "MIT",
-      "dependencies": {
-        "cdk": "2.76.0-alpha.0"
-      }
-    },
-    "node_modules/aws-cdk": {
-      "version": "2.76.0",
-      "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.76.0.tgz",
-      "integrity": "sha512-y6VHtqUpYenn6mGIBFbcGGXIoXfKA3o0eGL/eeD/gUJ9TcPrgMLQM1NxSMb5JVsOk5BPPXzGmvB0gBu40utGqg==",
-      "bin": {
-        "cdk": "bin/cdk"
-      },
-      "engines": {
-        "node": ">= 14.15.0"
-      },
-      "optionalDependencies": {
-        "fsevents": "2.3.2"
-      }
-    },
-    "node_modules/cdk": {
-      "version": "2.76.0-alpha.0",
-      "resolved": "https://registry.npmjs.org/cdk/-/cdk-2.76.0-alpha.0.tgz",
-      "integrity": "sha512-HNfX5c7MU18LxthZRcapqEhG0IFgQeNOhtsTR1QiL/7dhy2TjvK26dYcJ67KIHfzMfm5EUjvOXdP1SPdW+eOOA==",
-      "dependencies": {
-        "aws-cdk": "2.76.0"
-      },
-      "bin": {
-        "cdk": "bin/cdk"
-      },
-      "engines": {
-        "node": ">= 8.10.0"
-      }
-    },
-    "node_modules/fsevents": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
-      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-      "hasInstallScript": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-      }
-    }
-  },
-  "dependencies": {
-    "aws-cdk": {
-      "version": "2.76.0",
-      "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.76.0.tgz",
-      "integrity": "sha512-y6VHtqUpYenn6mGIBFbcGGXIoXfKA3o0eGL/eeD/gUJ9TcPrgMLQM1NxSMb5JVsOk5BPPXzGmvB0gBu40utGqg==",
-      "requires": {
-        "fsevents": "2.3.2"
-      }
-    },
-    "cdk": {
-      "version": "2.76.0-alpha.0",
-      "resolved": "https://registry.npmjs.org/cdk/-/cdk-2.76.0-alpha.0.tgz",
-      "integrity": "sha512-HNfX5c7MU18LxthZRcapqEhG0IFgQeNOhtsTR1QiL/7dhy2TjvK26dYcJ67KIHfzMfm5EUjvOXdP1SPdW+eOOA==",
-      "requires": {
-        "aws-cdk": "2.76.0"
-      }
-    },
-    "fsevents": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
-      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-      "optional": true
-    }
-  }
-}
diff --git a/deployment/aws/package.json b/deployment/aws/package.json
deleted file mode 100644
index 040bfa6b4..000000000
--- a/deployment/aws/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "name": "cdk-deploy",
-  "version": "0.1.0",
-  "description": "Dependencies for CDK deployment",
-  "license": "MIT",
-  "private": true,
-  "dependencies": {
-    "cdk": "2.76.0-alpha.0"
-  },
-  "scripts": {
-    "cdk": "cdk"
-  }
-}
diff --git a/deployment/aws/pyproject.toml b/deployment/aws/pyproject.toml
new file mode 100644
index 000000000..9f1ff2d09
--- /dev/null
+++ b/deployment/aws/pyproject.toml
@@ -0,0 +1,12 @@
+[project]
+name = "titiler-cdk"
+version = "0.0.0"
+
+dependencies = [
+    # aws cdk
+    "aws-cdk-lib~=2.232.2",
+    "constructs>=10.4.2",
+    # pydantic settings
+    "pydantic~=2.0",
+    "pydantic-settings~=2.0",
+]
diff --git a/deployment/aws/requirements-cdk.txt b/deployment/aws/requirements-cdk.txt
deleted file mode 100644
index 973fc3433..000000000
--- a/deployment/aws/requirements-cdk.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# aws cdk
-aws-cdk-lib==2.76.0
-aws_cdk-aws_apigatewayv2_alpha==2.76.0a0
-aws_cdk-aws_apigatewayv2_integrations_alpha==2.76.0a0
-constructs>=10.0.0
-
-# pydantic settings
-pydantic
-python-dotenv
diff --git a/deployment/aws/uv.lock b/deployment/aws/uv.lock
new file mode 100644
index 000000000..23b5c46e7
--- /dev/null
+++ b/deployment/aws/uv.lock
@@ -0,0 +1,291 @@
+version = 1
+revision = 3
+requires-python = ">=3.14"
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+]
+
+[[package]]
+name = "aws-cdk-asset-awscli-v1"
+version = "2.2.242"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "jsii" },
+    { name = "publication" },
+    { name = "typeguard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/84/66/095e92652c175a9c18c98bc358db2c5957897245053fb5d0988c908be355/aws_cdk_asset_awscli_v1-2.2.242.tar.gz", hash = "sha256:a957d679a118f4375307ed90b9aed7127c5c1402989438060eae4ab29ab0d13f", size = 19284036, upload-time = "2025-06-23T17:42:03.275Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7a/ca/0415b7387c776c0a82a153fe75573e78cbbf1a71d4475636393f5ecfc649/aws_cdk_asset_awscli_v1-2.2.242-py3-none-any.whl", hash = "sha256:d1001bf56a12f7d1162d4211003d1e8f72a213159465e2d0e1c598cc0ea44aad", size = 19282441, upload-time = "2025-06-23T17:42:00.381Z" },
+]
+
+[[package]]
+name = "aws-cdk-asset-node-proxy-agent-v6"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "jsii" },
+    { name = "publication" },
+    { name = "typeguard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d5/ab/09ac3ecc0067988d02398328e088d66cbe8555c991563c8ddfa1db5296ae/aws_cdk_asset_node_proxy_agent_v6-2.1.0.tar.gz", hash = "sha256:1f292c0631f86708ba4ee328b3a2b229f7e46ea1c79fbde567ee9eb119c2b0e2", size = 1540231, upload-time = "2024-09-03T09:36:51.634Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8d/86/1817a6da223aa80aeb94a504f07f930170284694b18f6053729e9930cc6a/aws_cdk.asset_node_proxy_agent_v6-2.1.0-py3-none-any.whl", hash = "sha256:24a388b69a44d03bae6dbf864c4e25ba650d4b61c008b4568b94ffbb9a69e40e", size = 1538724, upload-time = "2024-09-03T09:36:49.8Z" },
+]
+
+[[package]]
+name = "aws-cdk-cloud-assembly-schema"
+version = "48.20.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "jsii" },
+    { name = "publication" },
+    { name = "typeguard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a7/b5/1ce2f6bff913ca8c94a001b84290ec4ce3729f54a3af0e3ff0edb303ac20/aws_cdk_cloud_assembly_schema-48.20.0.tar.gz", hash = "sha256:229aa136c26b71b0a82b5a32658eabcd30e344f7e136315fdb6e3de8ef523bfa", size = 208109, upload-time = "2025-11-19T12:19:48.206Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/13/08/17a35f0b668451484f2254f5e50a0105958bffe90da11c41b7629972e6a9/aws_cdk_cloud_assembly_schema-48.20.0-py3-none-any.whl", hash = "sha256:f5b6cf661cac8690add9461de13aeae3f3742eec71c066032bd045b08d0b7c3e", size = 207669, upload-time = "2025-11-19T12:19:46.614Z" },
+]
+
+[[package]]
+name = "aws-cdk-lib"
+version = "2.232.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "aws-cdk-asset-awscli-v1" },
+    { name = "aws-cdk-asset-node-proxy-agent-v6" },
+    { name = "aws-cdk-cloud-assembly-schema" },
+    { name = "constructs" },
+    { name = "jsii" },
+    { name = "publication" },
+    { name = "typeguard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0c/34/24af9f7bdca0143b09be8d9228901c2b82f4866aab7544eabe2e386600b8/aws_cdk_lib-2.232.2.tar.gz", hash = "sha256:329fd448ba3cc9a83bcd83eee372ed13b5efdd3a5b28a4e6dc513c43d9a746f7", size = 46570425, upload-time = "2025-12-12T20:50:14.095Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/00/81/94f9a35f3f703ca8f8df9fc8e70b587d3f9fb1e12c8fa54528e145732280/aws_cdk_lib-2.232.2-py3-none-any.whl", hash = "sha256:d1b3a5dbe518fbcbe3a9a06960396d8debdef9472c049fbf42adb3f01d613825", size = 47220640, upload-time = "2025-12-12T20:49:34.941Z" },
+]
+
+[[package]]
+name = "cattrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "attrs" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d8/2b/a40e1488fdfa02d3f9a653a61a5935ea08b3c2225ee818db6a76c7ba9695/cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff", size = 70738, upload-time = "2025-10-07T12:26:06.603Z" },
+]
+
+[[package]]
+name = "constructs"
+version = "10.4.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "jsii" },
+    { name = "publication" },
+    { name = "typeguard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7a/e1/48a63ea310ac999005368aa72f52be520210526002f3f0cfcb7aaa9477ee/constructs-10.4.3.tar.gz", hash = "sha256:bfe3657b0acb62af7aa1fda9a7e338ae5cae84ddf8f84f71125a2a3800d52ea0", size = 64486, upload-time = "2025-11-06T15:48:25.039Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/97/94/a5ce72d63276d3baa47592d3d12afc33f9e5d8f4ddd7bb2ec0012255037f/constructs-10.4.3-py3-none-any.whl", hash = "sha256:43bbefa1ac1c044577d0b1a30648fe5b49557b8b95de2648186f6b909febf7f9", size = 62659, upload-time = "2025-11-06T15:48:21.966Z" },
+]
+
+[[package]]
+name = "importlib-resources"
+version = "6.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
+]
+
+[[package]]
+name = "jsii"
+version = "1.121.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "attrs" },
+    { name = "cattrs" },
+    { name = "importlib-resources" },
+    { name = "publication" },
+    { name = "python-dateutil" },
+    { name = "typeguard" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b0/e2/2950c7891f4ccf301be5e4fad8781169aa7dde2d267d4990debb6aeadac3/jsii-1.121.0.tar.gz", hash = "sha256:6c003ae10916bedce0cdf4cf86389435265ee151581e57429d0dd8aecb495be1", size = 625665, upload-time = "2025-12-09T17:25:08.544Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e1/08/986dfcdb0e7cba3edca74219e3d86b00623ccf0c8d7bdd62dea3947766ff/jsii-1.121.0-py3-none-any.whl", hash = "sha256:e7e10f020cfce01951956750fea50a863955e21aae202ee7f129b873f9d4988b", size = 601786, upload-time = "2025-12-09T17:25:07.194Z" },
+]
+
+[[package]]
+name = "publication"
+version = "0.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/8e/8c9fe7e32fdf9c386f83d59610cc819a25dadb874b5920f2d0ef7d35f46d/publication-0.0.3.tar.gz", hash = "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4", size = 5484, upload-time = "2019-01-15T07:52:23.914Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f8/d3/6308debad7afcdb3ea5f50b4b3d852f41eb566a311fbcb4da23755a28155/publication-0.0.3-py2.py3-none-any.whl", hash = "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6", size = 7687, upload-time = "2019-01-15T07:52:22.151Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "annotated-types" },
+    { name = "pydantic-core" },
+    { name = "typing-extensions" },
+    { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+    { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+    { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+    { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+    { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+    { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+    { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+    { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+    { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+    { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+    { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+    { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "pydantic" },
+    { name = "python-dotenv" },
+    { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/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, upload-time = "2024-03-01T18:36:20.211Z" }
+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, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "titiler-cdk"
+version = "0.0.0"
+source = { virtual = "." }
+dependencies = [
+    { name = "aws-cdk-lib" },
+    { name = "constructs" },
+    { name = "pydantic" },
+    { name = "pydantic-settings" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "aws-cdk-lib", specifier = "~=2.232.2" },
+    { name = "constructs", specifier = ">=10.4.2" },
+    { name = "pydantic", specifier = "~=2.0" },
+    { name = "pydantic-settings", specifier = "~=2.0" },
+]
+
+[[package]]
+name = "typeguard"
+version = "2.13.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/38/c61bfcf62a7b572b5e9363a802ff92559cb427ee963048e1442e3aef7490/typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4", size = 40604, upload-time = "2021-12-10T21:09:39.158Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9a/bb/d43e5c75054e53efce310e79d63df0ac3f25e34c926be5dffb7d283fb2a8/typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1", size = 17605, upload-time = "2021-12-10T21:09:37.844Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
diff --git a/deployment/azure/README.md b/deployment/azure/README.md
index 7516cde29..1fb58b8d2 100644
--- a/deployment/azure/README.md
+++ b/deployment/azure/README.md
@@ -1,13 +1,13 @@
 ### Function
 
-TiTiler is built on top of [FastAPI](https://github.com/tiangolo/fastapi), a modern, fast, Python web framework for building APIs. As for AWS Lambda we can make our FastAPI application work on Azure Function by wrapping it within the [Azure Function Python worker](https://github.com/Azure/azure-functions-python-worker).
+TiTiler is built on top of [FastAPI](https://github.com/tiangolo/fastapi), a modern, fast, Python web framework for building APIs. We can make our FastAPI application work as an Azure Function by wrapping it within the [Azure Function Python worker](https://github.com/Azure/azure-functions-python-worker).
 
-If you are not familiar with **Azure functions** we recommend checking https://docs.microsoft.com/en-us/azure/azure-functions/ first.
+If you are not familiar with **Azure functions**, we recommend checking https://docs.microsoft.com/en-us/azure/azure-functions/ first.
 
 Minimal TiTiler Azure function code:
 ```python
 import azure.functions as func
-from titiler.application.routers import cog, mosaic, stac, tms
+from titiler.application.main import cog, mosaic, stac, tms
 from fastapi import FastAPI
 
 
@@ -20,14 +20,12 @@ app.include_router(mosaic.router, prefix="/mosaicjson", tags=["MosaicJSON"])
 app.include_router(tms.router, tags=["TileMatrixSets"])
 
 
-def main(
+async def main(
     req: func.HttpRequest, context: func.Context,
 ) -> func.HttpResponse:
-    return func.AsgiMiddleware(app).handle(req, context)
+    return await func.AsgiMiddleware(app).handle_async(req, context)
 ```
 
-Note: there is a `bug` in `azure.functions.AsgiMiddleware` which prevent using `starlette.BaseHTTPMiddleware` middlewares (see: https://github.com/Azure/azure-functions-python-worker/issues/903).
-
 #### Requirements
 - Azure CLI: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli
 - Azure Function Tool: https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local
@@ -42,9 +40,9 @@ $ cd titiler/deployment/azure
 
 $ az login
 $ az group create --name AzureFunctionsTiTiler-rg --location eastus
-$ az storage account create --name TiTilerStorage --sku Standard_LRS
-$ az functionapp create --consumption-plan-location eastus --runtime python --runtime-version 3.8 --functions-version 3 --name titiler --os-type linux
-$ func azure functionapp publish titiler
+$ az storage account create --name {your-new-storage-name} --sku Standard_LRS -g AzureFunctionsTiTiler-rg
+$ az functionapp create --consumption-plan-location eastus --runtime python --runtime-version 3.9 --functions-version 4 --name {your-new-function-name} --os-type linux -g AzureFunctionsTiTiler-rg -s {your-new-storage-name}
+$ func azure functionapp publish titiler --python
 ```
 
 or
diff --git a/deployment/azure/app/__init__.py b/deployment/azure/app/__init__.py
index 5aab2727d..46bda7298 100644
--- a/deployment/azure/app/__init__.py
+++ b/deployment/azure/app/__init__.py
@@ -1,107 +1,13 @@
 """Microsoft Azure Function."""
 
 import azure.functions as func
-from fastapi import FastAPI
-from starlette.middleware.cors import CORSMiddleware
-from starlette.requests import Request
-from starlette.responses import HTMLResponse
-from starlette_cramjam.middleware import CompressionMiddleware
 
-from titiler.application import __version__ as titiler_version
-from titiler.application.custom import templates
-from titiler.application.routers import cog, mosaic, stac, tms
-from titiler.application.settings import ApiSettings
-from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers
+from titiler.application.main import app
 
-# from titiler.core.middleware import (
-#     CacheControlMiddleware,
-#     LoggerMiddleware,
-#     LowerCaseQueryStringMiddleware,
-#     TotalTimeMiddleware,
-# )
-from titiler.mosaic.errors import MOSAIC_STATUS_CODES
 
-api_settings = ApiSettings()
-
-app = FastAPI(
-    title=api_settings.name,
-    description="A lightweight Cloud Optimized GeoTIFF tile server",
-    version=titiler_version,
-    root_path=api_settings.root_path,
-)
-
-if not api_settings.disable_cog:
-    app.include_router(cog.router, prefix="/cog", tags=["Cloud Optimized GeoTIFF"])
-
-if not api_settings.disable_stac:
-    app.include_router(
-        stac.router, prefix="/stac", tags=["SpatioTemporal Asset Catalog"]
-    )
-
-if not api_settings.disable_mosaic:
-    app.include_router(mosaic.router, prefix="/mosaicjson", tags=["MosaicJSON"])
-
-app.include_router(tms.router, tags=["TileMatrixSets"])
-add_exception_handlers(app, DEFAULT_STATUS_CODES)
-add_exception_handlers(app, MOSAIC_STATUS_CODES)
-
-
-# Set all CORS enabled origins
-if api_settings.cors_origins:
-    app.add_middleware(
-        CORSMiddleware,
-        allow_origins=api_settings.cors_origins,
-        allow_credentials=True,
-        allow_methods=["GET"],
-        allow_headers=["*"],
-    )
-
-app.add_middleware(
-    CompressionMiddleware,
-    minimum_size=0,
-    exclude_mediatype={
-        "image/jpeg",
-        "image/jpg",
-        "image/png",
-        "image/jp2",
-        "image/webp",
-    },
-)
-
-# see https://github.com/encode/starlette/issues/1320
-# app.add_middleware(
-#     CacheControlMiddleware,
-#     cachecontrol=api_settings.cachecontrol,
-#     exclude_path={r"/healthz"},
-# )
-
-# if api_settings.debug:
-#     app.add_middleware(LoggerMiddleware, headers=True, querystrings=True)
-#     app.add_middleware(TotalTimeMiddleware)
-
-# if api_settings.lower_case_query_parameters:
-#     app.add_middleware(LowerCaseQueryStringMiddleware)
-
-
-@app.get("/healthz", description="Health Check", tags=["Health Check"])
-def ping():
-    """Health check."""
-    return {"ping": "pong!"}
-
-
-@app.get("/", response_class=HTMLResponse, include_in_schema=False)
-def landing(request: Request):
-    """TiTiler Landing page"""
-    return templates.TemplateResponse(
-        name="index.html",
-        context={"request": request},
-        media_type="text/html",
-    )
-
-
-def main(
+async def main(
     req: func.HttpRequest,
     context: func.Context,
 ) -> func.HttpResponse:
     """Run App in AsgiMiddleware."""
-    return func.AsgiMiddleware(app).handle(req, context)
+    return await func.AsgiMiddleware(app).handle_async(req, context)
diff --git a/deployment/azure/host.json b/deployment/azure/host.json
index 8e588272b..6e86c559b 100644
--- a/deployment/azure/host.json
+++ b/deployment/azure/host.json
@@ -10,7 +10,7 @@
   },
   "extensionBundle": {
     "id": "Microsoft.Azure.Functions.ExtensionBundle",
-    "version": "[2.*, 3.0.0)"
+    "version": "[3.*, 4.0.0)"
   },
   "extensions": {
     "http": {
diff --git a/deployment/k8s/README.md b/deployment/k8s/README.md
index 1b2f03f04..ddb08334d 100644
--- a/deployment/k8s/README.md
+++ b/deployment/k8s/README.md
@@ -1,6 +1,6 @@
 ## k8s / Helm Deployment
 
-Try locally
+Try locally:
 
 ```
 minikube start
@@ -11,4 +11,4 @@ helm init --wait
 helm install -f titiler/Chart.yaml titiler
 ```
 
-For more info about K8S cluster and node configuration please see: https://github.com/developmentseed/titiler/issues/212
+For more info about K8S cluster and node configuration, please see: https://github.com/developmentseed/titiler/issues/212
diff --git a/deployment/k8s/charts/Chart.yaml b/deployment/k8s/charts/Chart.yaml
index febe06e3e..ce262de65 100644
--- a/deployment/k8s/charts/Chart.yaml
+++ b/deployment/k8s/charts/Chart.yaml
@@ -1,9 +1,11 @@
 apiVersion: v1
-appVersion: 0.11.7
+appVersion: 2.0.0b2
 description: A dynamic Web Map tile server
 name: titiler
-version: 1.1.0
+version: 2.0.3
 icon: https://raw.githubusercontent.com/developmentseed/titiler/main/docs/logos/TiTiler_logo_small.png
 maintainers:
   - name: emmanuelmathot  # Emmanuel Mathot
     url: https://github.com/emmanuelmathot
+  - name: ciaransweet     # Ciaran Sweet
+    url: https://github.com/ciaransweet
diff --git a/deployment/k8s/charts/templates/deployment.yaml b/deployment/k8s/charts/templates/deployment.yaml
index 9858c13a6..ed4582249 100644
--- a/deployment/k8s/charts/templates/deployment.yaml
+++ b/deployment/k8s/charts/templates/deployment.yaml
@@ -14,10 +14,19 @@ spec:
       labels:
         {{- include "titiler.selectorLabels" . | nindent 8 }}
     spec:
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
       containers:
         - name: {{ .Chart.Name }}
           image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
           imagePullPolicy: {{ .Values.image.pullPolicy }}
+          command: [ {{ .Values.image.command }} ]
+          args: [ {{- range $arg := .Values.image.args }}
+            {{- $arg | quote }},
+            {{- end }}
+          ]
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
           env:
           {{- range $key, $val := .Values.env }}
             - name: {{ $key }}
@@ -33,7 +42,7 @@ spec:
           {{- end }}
           ports:
             - name: http
-              containerPort: {{ .Values.env.PORT }}
+              containerPort: 80
               protocol: TCP
           livenessProbe:
             httpGet:
@@ -49,10 +58,31 @@ spec:
             - mountPath: /config
               name: config
               readOnly: true
+          {{- range .Values.extraHostPathMounts }}
+            - name: {{ .name }}
+              mountPath: {{ .mountPath }}
+              readOnly: {{ .readOnly }}
+            {{- if .mountPropagation }}
+              mountPropagation: {{ .mountPropagation }}
+            {{- end }}
+          {{- end }}
+      terminationGracePeriodSeconds: {{ .Values.env.terminationGracePeriodSeconds }}
       volumes:
         - name: config
           configMap:
             name: {{ include "titiler.fullname" . }}-configmap
+      {{- range .Values.extraHostPathMounts }}
+        - name: {{ .name }}
+          hostPath:
+            path: {{ .hostPath }}
+            type: Directory
+      {{- end }}
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- range . }}
+      - name: {{ .name }}
+        {{- end }}
+      {{- end }}
       {{- with .Values.serviceAccountName }}
       serviceAccountName: {{ . | quote }}
       {{- end }}
diff --git a/deployment/k8s/charts/values-test.yaml b/deployment/k8s/charts/values-test.yaml
deleted file mode 100644
index 47bd601e4..000000000
--- a/deployment/k8s/charts/values-test.yaml
+++ /dev/null
@@ -1,44 +0,0 @@
-# Default values for titiler.
-replicaCount: 4
-
-ingress:
-  enabled: true
-  annotations: {}
-    # kubernetes.io/ingress.class: nginx
-    # kubernetes.io/tls-acme: "true"
-  hosts:
-    - host: titiler.charter.uat.esaportal.eu
-      paths: ["/"]
-  tls:
-   - secretName: domain-tls
-     hosts:
-       - titiler.charter.uat.esaportal.eu
-
-env:
-  PORT: 80
-  CPL_TMPDIR: /tmp
-  GDAL_CACHEMAX: 75%
-  VSI_CACHE: TRUE
-  VSI_CACHE_SIZE: 1073741824
-  GDAL_DISABLE_READDIR_ON_OPEN: EMPTY_DIR
-  GDAL_HTTP_MERGE_CONSECUTIVE_RANGES: YES
-  GDAL_HTTP_MULTIPLEX: YES
-  GDAL_HTTP_VERSION: 2
-  PYTHONWARNINGS: ignore
-  WEB_CONCURRENCY: 2
-
-resources:
-   limits:
-    cpu: 256m
-    memory: 1Gi
-    # ephemeral-storage: 10Gi
-   requests:
-    cpu: 256m
-    memory: 1Gi
-    # ephemeral-storage: 10Gi
-
-nodeSelector: {}
-
-tolerations: []
-
-affinity: {}
diff --git a/deployment/k8s/charts/values.yaml b/deployment/k8s/charts/values.yaml
index b10995183..bb7b3a32e 100644
--- a/deployment/k8s/charts/values.yaml
+++ b/deployment/k8s/charts/values.yaml
@@ -2,13 +2,24 @@
 replicaCount: 1
 
 image:
-  repository: ghcr.io/developmentseed/titiler-uvicorn
-  tag: latest
+  repository: ghcr.io/developmentseed/titiler
+  tag: ""
   pullPolicy: IfNotPresent
+  command: "uvicorn"
+  args:
+    - "titiler.application.main:app"
+    - "--host"
+    - "0.0.0.0"
+    - "--port"
+    - "80"
+    - "--workers"
+    - "1"
 
 nameOverride: ""
 fullnameOverride: ""
 
+terminationGracePeriodSeconds: 30
+
 service:
   type: ClusterIP
   port: 80
@@ -26,8 +37,16 @@ ingress:
   #    hosts:
   #      - titiler.local
 
+extraHostPathMounts: []
+  # - name: map-sources
+  #   mountPath: /map-sources/
+  #   hostPath: /home/ubuntu/map-sources
+  #   readOnly: false
+  #   mountPropagation: HostToContainer # OPTIONAL
+
+imagePullSecrets: []
+
 env:
-  PORT: 80
   CPL_TMPDIR: /tmp
   GDAL_CACHEMAX: 200  # 200 mb
   VSI_CACHE: "TRUE"
@@ -37,7 +56,6 @@ env:
   GDAL_HTTP_MULTIPLEX: "YES"
   GDAL_HTTP_VERSION: 2
   PYTHONWARNINGS: "ignore"
-  WEB_CONCURRENCY: 2
 
 resources:
   limits:
@@ -54,3 +72,17 @@ nodeSelector: {}
 tolerations: []
 
 affinity: {}
+
+securityContext: {}
+  # capabilities:
+  #   drop:
+  #     - ALL
+  # readOnlyRootFilesystem: true
+  # allowPrivilegeEscalation: false
+  # runAsNonRoot: true
+  # runAsUser: 1001
+
+podSecurityContext: {}
+  # fsGroup: 1001
+  # runAsNonRoot: true
+  # runAsUser: 1001
diff --git a/dev_notebooks/rendering.ipynb b/dev_notebooks/rendering.ipynb
new file mode 100644
index 000000000..a38b3d472
--- /dev/null
+++ b/dev_notebooks/rendering.ipynb
@@ -0,0 +1,236 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "78d17219",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "/Users/vincentsarago/Dev/Devseed/titiler/.venv/lib/python3.13/site-packages/rasterio/io.py:140: NotGeoreferencedWarning: Dataset has no geotransform, gcps, or rpcs. The identity matrix will be returned.\n",
+      "  rd = DatasetReader(mempath, driver=driver, sharing=sharing, **kwargs)\n"
+     ]
+    }
+   ],
+   "source": [
+    "import numpy\n",
+    "\n",
+    "from titiler.core.resources.enums import ImageType\n",
+    "from titiler.core.utils import render_image\n",
+    "from matplotlib.pyplot import imshow\n",
+    "\n",
+    "from rasterio.io import MemoryFile\n",
+    "\n",
+    "from rio_tiler.models import ImageData\n",
+    "\n",
+    "# Partial alpha values\n",
+    "cm = {\n",
+    "    1: (0, 0, 0, 0),\n",
+    "    500: (100, 100, 100, 50),\n",
+    "    1000: (255, 255, 255, 255),\n",
+    "}\n",
+    "data = numpy.zeros((1, 256, 256), dtype=\"float32\") + 1\n",
+    "data[0, 0, 0] = 0\n",
+    "d = numpy.ma.masked_equal(data, 0)\n",
+    "d[0, 1:, 1:] = 1\n",
+    "d[0, 2:, 2:] = 500\n",
+    "d[0, 3:, 3:] = 1000\n",
+    "\n",
+    "img = ImageData(d)\n",
+    "content, media = render_image(\n",
+    "    img,\n",
+    "    output_format=ImageType.png,\n",
+    "    colormap=cm,\n",
+    ")\n",
+    "assert media == \"image/png\"\n",
+    "\n",
+    "with MemoryFile(content) as mem:\n",
+    "    with mem.open() as dst:\n",
+    "        data_converted = dst.read()\n",
+    "        assert dst.count == 4\n",
+    "        assert dst.dtypes == (\"uint8\", \"uint8\", \"uint8\", \"uint8\")\n",
+    "        assert data_converted[:, 0, 0].tolist() == [\n",
+    "            0,\n",
+    "            0,\n",
+    "            0,\n",
+    "            0,\n",
+    "        ]  # Masked from Original Mask | set to UINT8 (0)\n",
+    "        assert data_converted[:, 1, 1].tolist() == [0, 0, 0, 0]  # Masked from CMAP\n",
+    "        assert data_converted[:, 2, 2].tolist() == [\n",
+    "            100,\n",
+    "            100,\n",
+    "            100,\n",
+    "            50,\n",
+    "        ]  # Partially masked from CMAP\n",
+    "        assert data_converted[:, 3, 3].tolist() == [255, 255, 255, 255]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "f853aedb",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "masked_array(\n",
+       "  data=[[--, 1.0, 1.0, 1.0, 1.0],\n",
+       "        [1.0, 1.0, 1.0, 1.0, 1.0],\n",
+       "        [1.0, 1.0, 500.0, 500.0, 500.0],\n",
+       "        [1.0, 1.0, 500.0, 1000.0, 1000.0],\n",
+       "        [1.0, 1.0, 500.0, 1000.0, 1000.0]],\n",
+       "  mask=[[ True, False, False, False, False],\n",
+       "        [False, False, False, False, False],\n",
+       "        [False, False, False, False, False],\n",
+       "        [False, False, False, False, False],\n",
+       "        [False, False, False, False, False]],\n",
+       "  fill_value=0.0,\n",
+       "  dtype=float32)"
+      ]
+     },
+     "execution_count": 2,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "d[0, 0:5, 0:5]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "308282ee",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       ""
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "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",
+      "text/plain": [
+       "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "imshow(d[0, 0:4, 0:4])" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "441b642e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "imshow(data_converted[3, 0:4, 0:4])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "20906022", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[ 0, 0, 0, 0],\n", + " [ 0, 0, 0, 0],\n", + " [ 0, 0, 50, 50],\n", + " [ 0, 0, 50, 255]], dtype=uint8)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data_converted[3, 0:4, 0:4]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4274d194", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b34d1bb2", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11c3c53a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "titiler (3.13.9)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docker-compose.yml b/docker-compose.yml index 9e3207e76..97cdd79e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,12 @@ -version: '3' - services: titiler: - # See https://github.com/developmentseed/titiler/discussions/387 - platform: linux/amd64 build: context: . - dockerfile: dockerfiles/Dockerfile.gunicorn + dockerfile: dockerfiles/Dockerfile ports: - "8000:8000" + command: ["uvicorn", "titiler.application.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] environment: - # Application - - HOST=0.0.0.0 - - PORT=8000 - # Gunicorn / Uvicorn - # https://github.com/tiangolo/uvicorn-gunicorn-docker#web_concurrency - - WEB_CONCURRENCY=1 - # https://github.com/tiangolo/uvicorn-gunicorn-docker#workers_per_core - - WORKERS_PER_CORE=1 # GDAL config - CPL_TMPDIR=/tmp - GDAL_CACHEMAX=75% @@ -39,29 +28,32 @@ services: # - TITILER_API_DISABLE_STAC=TRUE/FALSE # - TITILER_API_DISABLE_MOSAIC=TRUE/FALSE # - TITILER_API_DISABLE_COG=TRUE/FALSE + # - TITILER_API_DISABLE_ZARR=TRUE/FALSE # - TITILER_API_CORS_ORIGIN=url.io,url.xyz # - TITILER_API_CACHECONTROL=public, max-age=3600 # - TITILER_API_DEBUG=TRUE/FALSE # - MOSAIC_CONCURRENCY= # will default to `RIO_TILER_MAX_THREADS` # rio-tiler config # - RIO_TILER_MAX_THREADS= + # telemetry config + - TITILER_API_TELEMETRY_ENABLED=True + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 + depends_on: + - otel-collector + volumes: + - ./:/data - titiler-uvicorn: - # See https://github.com/developmentseed/titiler/discussions/387 - platform: linux/amd64 - build: - context: . - dockerfile: dockerfiles/Dockerfile.uvicorn + titiler-gunicorn: + extends: + service: titiler ports: - "8000:8000" + command: ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "titiler.application.main:app", "--bind", "0.0.0.0:8000", "--workers", "1"] + + benchmark: + extends: + service: titiler environment: - # Application - - HOST=0.0.0.0 - - PORT=8000 - # Uvicorn - # http://www.uvicorn.org/settings/#production - - WEB_CONCURRENCY=1 - # GDAL config - CPL_TMPDIR=/tmp - GDAL_CACHEMAX=75% - GDAL_INGESTED_BYTES_AT_OPEN=32768 @@ -72,44 +64,41 @@ services: - PYTHONWARNINGS=ignore - VSI_CACHE=TRUE - VSI_CACHE_SIZE=536870912 - # GDAL VSI Config - # https://gdal.org/user/virtual_file_systems.html#vsis3-aws-s3-files - # https://gdal.org/user/virtual_file_systems.html#vsigs-google-cloud-storage-files - # https://gdal.org/user/virtual_file_systems.html#vsiaz-microsoft-azure-blob-files - # - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - # - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - # TiTiler config - # - TITILER_API_DISABLE_STAC=TRUE/FALSE - # - TITILER_API_DISABLE_MOSAIC=TRUE/FALSE - # - TITILER_API_DISABLE_COG=TRUE/FALSE - # - TITILER_API_CORS_ORIGIN=url.io,url.xyz - # - TITILER_API_CACHECONTROL=public, max-age=3600 - # - TITILER_API_DEBUG=TRUE/FALSE - # - MOSAIC_CONCURRENCY= # will default to `RIO_TILER_MAX_THREADS` - # rio-tiler config - # - RIO_TILER_MAX_THREADS= - - benchmark: - extends: - service: titiler-uvicorn + - TITILER_API_TELEMETRY_ENABLED=False volumes: - ./.github/data:/data nginx-titiler: extends: service: titiler - ports: - - 8081:8081 - environment: - - PORT=8081 - - TITILER_API_ROOT_PATH=/api/v1/titiler + command: ["uvicorn", "titiler.application.main:app", "--host", "0.0.0.0", "--port", "8081", "--workers", "1", "--proxy-headers", "--forwarded-allow-ips='*'", "--root-path=/api/v1/titiler"] nginx: image: nginx ports: - - 80:80 + - 8080:80 volumes: - ./dockerfiles/nginx.conf:/etc/nginx/nginx.conf depends_on: - nginx-titiler command: ["nginx-debug", "-g", "daemon off;"] + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./dockerfiles/otel-collector-config.yaml:/etc/otel-collector-config.yaml + ports: + - "4318:4318" # OTLP HTTP receiver + - "13133:13133" # Health check extension + depends_on: + - jaeger + + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" # UI + - "14250:14250" # OTLP gRPC (Jaeger receiver) + environment: + - COLLECTOR_OTLP_ENABLED=true + diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile new file mode 100644 index 000000000..1ebef5cb6 --- /dev/null +++ b/dockerfiles/Dockerfile @@ -0,0 +1,30 @@ +ARG PYTHON_VERSION=3.14 + +FROM python:${PYTHON_VERSION} +RUN apt update && apt upgrade -y \ + && apt install curl -y \ + && rm -rf /var/lib/apt/lists/* + +# Ensure root certificates are always updated at evey container build +# and curl is using the latest version of them +# RUN mkdir /usr/local/share/ca-certificates/cacert.org +# RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/root.crt +# RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/class3.crt +# ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +RUN update-ca-certificates + +RUN python -m pip install -U pip +RUN python -m pip install uvicorn uvicorn-worker gunicorn + +COPY src/titiler/ /tmp/titiler/ +RUN python -m pip install /tmp/titiler/core /tmp/titiler/extensions["cogeo,stac"] /tmp/titiler/mosaic["mosaicjson"] /tmp/titiler/xarray /tmp/titiler/application --no-cache-dir --upgrade +RUN rm -rf /tmp/titiler + +################################################### +# For compatibility (might be removed at one point) +ENV MODULE_NAME=titiler.application.main +ENV VARIABLE_NAME=app +ENV HOST=0.0.0.0 +ENV PORT=80 +ENV WEB_CONCURRENCY=1 +CMD gunicorn -k uvicorn.workers.UvicornWorker ${MODULE_NAME}:${VARIABLE_NAME} --bind ${HOST}:${PORT} --workers ${WEB_CONCURRENCY} diff --git a/dockerfiles/Dockerfile.gunicorn b/dockerfiles/Dockerfile.gunicorn deleted file mode 100644 index 6df319f04..000000000 --- a/dockerfiles/Dockerfile.gunicorn +++ /dev/null @@ -1,21 +0,0 @@ -# Dockerfile for running titiler application with gunicorn server -# Size ~1.4GB -ARG PYTHON_VERSION=3.11 - -FROM ghcr.io/vincentsarago/uvicorn-gunicorn:${PYTHON_VERSION} - -# Ensure root certificates are always updated at evey container build -# and curl is using the latest version of them -RUN mkdir /usr/local/share/ca-certificates/cacert.org -RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/root.crt -RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/class3.crt -RUN update-ca-certificates -ENV CURL_CA_BUNDLE /etc/ssl/certs/ca-certificates.crt - -COPY src/titiler/ /tmp/titiler/ -RUN python -m pip install -U pip -RUN python -m pip install /tmp/titiler/core /tmp/titiler/extensions["cogeo,stac"] /tmp/titiler/mosaic /tmp/titiler/application --no-cache-dir --upgrade -RUN rm -rf /tmp/titiler - -ENV MODULE_NAME titiler.application.main -ENV VARIABLE_NAME app diff --git a/dockerfiles/Dockerfile.uvicorn b/dockerfiles/Dockerfile.uvicorn deleted file mode 100644 index e0a861133..000000000 --- a/dockerfiles/Dockerfile.uvicorn +++ /dev/null @@ -1,24 +0,0 @@ -# Dockerfile for running titiler application with uvicorn server -ARG PYTHON_VERSION=3.11 - -FROM bitnami/python:${PYTHON_VERSION} -RUN apt update && apt upgrade -y \ - && apt install curl -y \ - && rm -rf /var/lib/apt/lists/* - -# Ensure root certificates are always updated at evey container build -# and curl is using the latest version of them -RUN mkdir /usr/local/share/ca-certificates/cacert.org -RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/root.crt -RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/class3.crt -RUN update-ca-certificates -ENV CURL_CA_BUNDLE /etc/ssl/certs/ca-certificates.crt - -COPY src/titiler/ /tmp/titiler/ -RUN python -m pip install -U pip -RUN python -m pip install /tmp/titiler/core /tmp/titiler/extensions["cogeo,stac"] /tmp/titiler/mosaic /tmp/titiler/application["server"] --no-cache-dir --upgrade -RUN rm -rf /tmp/titiler - -ENV HOST 0.0.0.0 -ENV PORT 80 -CMD uvicorn titiler.application.main:app --host ${HOST} --port ${PORT} diff --git a/dockerfiles/Dockerfile.xarray b/dockerfiles/Dockerfile.xarray new file mode 100644 index 000000000..9a27e28dc --- /dev/null +++ b/dockerfiles/Dockerfile.xarray @@ -0,0 +1,30 @@ +ARG PYTHON_VERSION=3.14 + +FROM python:${PYTHON_VERSION} +RUN apt update && apt upgrade -y \ + && apt install curl -y \ + && rm -rf /var/lib/apt/lists/* + +# Ensure root certificates are always updated at evey container build +# and curl is using the latest version of them +# RUN mkdir /usr/local/share/ca-certificates/cacert.org +# RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/root.crt +# RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/class3.crt +# ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +RUN update-ca-certificates + +RUN python -m pip install -U pip +RUN python -m pip install uvicorn uvicorn-worker gunicorn + +COPY src/titiler/ /tmp/titiler/ +RUN python -m pip install /tmp/titiler/core["telemetry"] /tmp/titiler/xarray starlette-cramjam pydantic-settings --no-cache-dir --upgrade +RUN rm -rf /tmp/titiler + +################################################### +# For compatibility (might be removed at one point) +ENV MODULE_NAME=titiler.xarray.main +ENV VARIABLE_NAME=app +ENV HOST=0.0.0.0 +ENV PORT=80 +ENV WEB_CONCURRENCY=1 +CMD gunicorn -k uvicorn.workers.UvicornWorker ${MODULE_NAME}:${VARIABLE_NAME} --bind ${HOST}:${PORT} --workers ${WEB_CONCURRENCY} diff --git a/dockerfiles/nginx.conf b/dockerfiles/nginx.conf index ccf17b175..12ad20609 100644 --- a/dockerfiles/nginx.conf +++ b/dockerfiles/nginx.conf @@ -6,8 +6,8 @@ http { location /api/v1/titiler { rewrite ^/api/v1/titiler(.*)$ $1 break; - proxy_pass http://app:8081; - proxy_set_header HOST $host; + proxy_pass http://nginx-titiler:8081; + proxy_set_header HOST $http_host; proxy_set_header Referer $http_referer; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; diff --git a/dockerfiles/otel-collector-config.yaml b/dockerfiles/otel-collector-config.yaml new file mode 100644 index 000000000..fed63867f --- /dev/null +++ b/dockerfiles/otel-collector-config.yaml @@ -0,0 +1,28 @@ +receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + +exporters: + otlp: + endpoint: jaeger:4317 + tls: + insecure: true + debug: + verbosity: detailed + +extensions: + health_check: + endpoint: 0.0.0.0:13133 + +service: + extensions: [health_check] + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [otlp, debug] \ No newline at end of file diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 1d3aa53f7..0691900e1 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -10,6 +10,22 @@ edit_uri: "blob/main/docs/src/" site_url: "https://developmentseed.org/titiler/" extra: + analytics: + provider: plausible + domain: developmentseed.org/titiler + + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: good + note: Thanks for your feedback! + + - icon: material/emoticon-sad-outline + name: This page could be improved + data: bad + note: Thanks for your feedback! social: - icon: "fontawesome/brands/github" link: "https://github.com/developmentseed" @@ -21,21 +37,36 @@ extra: nav: - TiTiler: "index.md" - User Guide: - - Intro: "intro.md" - - Dynamic Tiling: "dynamic_tiling.md" - - Mosaics: "mosaics.md" - - TileMatrixSets: "tile_matrix_sets.md" - - Output data format: "output_format.md" + - Getting Started: "user_guide/getting_started.md" + - Titiler with STAC: "user_guide/titiler_with_stac.md" + - Dynamic Tiling: "user_guide/dynamic_tiling.md" + - TileMatrixSets: "user_guide/tile_matrix_sets.md" + - Output data format: "user_guide/output_format.md" + - Algorithm: "user_guide/algorithms.md" + - Rendering: "user_guide/rendering.md" - Advanced User Guide: - - Tiler Factories: "advanced/tiler_factories.md" + - Endpoints Factories: "advanced/endpoints_factories.md" - Dependencies: "advanced/dependencies.md" - Customization: "advanced/customization.md" - Performance Tuning: "advanced/performance_tuning.md" - - Custom Algorithm: "advanced/Algorithms.md" - Extensions: "advanced/Extensions.md" - - Rendering: "advanced/rendering.md" - # - APIRoute and environment variables: "advanced/APIRoute_and_environment_variables.md" + - Observability with OpenTelemetry: "advanced/telemetry.md" + + - Packages: + - titiler.core: "packages/core.md" + - titiler.xarray: "packages/xarray.md" + - titiler.extensions: "packages/extensions.md" + - titiler.mosaic: "packages/mosaic.md" + - titiler.application: "packages/application.md" + + - Endpoints documentation: + - /cog: "endpoints/cog.md" + - /stac: "endpoints/stac.md" + - /mosaicjson: "endpoints/mosaic.md" + - /tileMatrixSets: "endpoints/tms.md" + - /algorithms: "endpoints/algorithms.md" + - /colormaps: "endpoints/colormaps.md" - Examples: - Create dynamic tilers with TiTiler: @@ -47,9 +78,10 @@ nav: - Tiler with custom Colormap dependency: "examples/code/tiler_with_custom_colormap.md" - Loading data with signed URLs: "examples/code/working_with_signed_urls.md" - STAC endpoints with custom `/validate`: "examples/code/tiler_with_custom_stac_validation.md" - - Custom Sentinel 2 Tiler: "examples/code/tiler_for_sentinel2.md" - Add custom algorithms: "examples/code/tiler_with_custom_algorithm.md" - GDAL WMTS Extension: "examples/code/create_gdal_wmts_extension.md" + - STAC + Xarray: "examples/code/tiler_with_custom_stac+xarray.md" + - Custom Layers: "examples/code/tiler_with_layers.md" - Use TiTiler endpoints: - COG: "examples/notebooks/Working_with_CloudOptimizedGeoTIFF_simple.ipynb" @@ -61,6 +93,7 @@ nav: - NumpyTile: "examples/notebooks/Working_with_NumpyTile.ipynb" - Algorithm: "examples/notebooks/Working_with_Algorithm.ipynb" - Statistics: "examples/notebooks/Working_with_Statistics.ipynb" + - Xarray: "examples/notebooks/Working_with_Zarr.ipynb" - API: - titiler.core: @@ -68,49 +101,125 @@ nav: - factory: api/titiler/core/factory.md - routing: api/titiler/core/routing.md - errors: api/titiler/core/errors.md - - enums: api/titiler/core/resources/enums.md - middleware: api/titiler/core/middleware.md + - resources: + - enums: api/titiler/core/resources/enums.md + - responses: api/titiler/core/resources/responses.md + - models: + - OGC: api/titiler/core/models/OGC.md + - Mapbox/MapLibre: api/titiler/core/models/mapbox.md + - responses: api/titiler/core/models/responses.md - titiler.extensions: - cogeo: api/titiler/extensions/cogeo.md - stac: api/titiler/extensions/stac.md - viewer: api/titiler/extensions/viewer.md + - wms: api/titiler/extensions/wms.md + - wmts: api/titiler/extensions/wmts.md - titiler.mosaic: - factory: api/titiler/mosaic/factory.md - - enums: api/titiler/mosaic/resources/enums.md - errors: api/titiler/mosaic/errors.md - - - titiler.application: - - /cog: "endpoints/cog.md" - - /stac: "endpoints/stac.md" - - /mosaicjson: "endpoints/mosaic.md" - - /tileMatrixSets: "endpoints/tms.md" + - extensions: + - wmts: api/titiler/mosaic/wmts.md + - mosaicjson: api/titiler/mosaic/mosaicjson.md + - models: + - responses: api/titiler/mosaic/models/responses.md + - titiler.xarray: + - io: api/titiler/xarray/io.md + - dependencies: api/titiler/xarray/dependencies.md + - extensions: api/titiler/xarray/extensions.md + - factory: api/titiler/xarray/factory.md + - main: api/titiler/xarray/main.md - Deployment: - Amazon Web Services: - Intro: "deployment/aws/intro.md" - - ECS: "deployment/aws/ecs.md" - Lambda: "deployment/aws/lambda.md" - SAM: "deployment/aws/sam.md" - k8s / Helm Deployment: "deployment/k8s.md" - Azure: "deployment/azure.md" + - Migrations: + - From titiler 0.26.x to 1.x: "migrations/v1_migration.md" + - From titiler 1.x to 2.x: "migrations/v2_migration.md" + - External links: "external_links.md" - Development - Contributing: "contributing.md" - Release Notes: "release-notes.md" + - Security: "security.md" + - Performance Benchmarks: benchmark.html plugins: - search + - social - mkdocs-jupyter: - include_source: True + include_source: true + ignore: ["**/.ipynb_checkpoints/*.ipynb"] + - mkdocstrings: + enable_inventory: true + handlers: + python: + paths: [src] + options: + filters: + - "!^__post_init__" + docstring_section_style: list + docstring_style: google + line_length: 100 + separate_signature: true + show_root_heading: true + show_signature_annotations: true + show_source: false + show_symbol_type_toc: true + signature_crossrefs: true + extensions: + - griffe_inherited_docstrings + inventories: + - https://docs.python.org/3/objects.inv + - https://numpy.org/doc/stable/objects.inv + - https://rasterio.readthedocs.io/en/stable/objects.inv + - https://docs.pydantic.dev/latest/objects.inv + - https://fastapi.tiangolo.com/objects.inv + - https://cogeotiff.github.io/rio-tiler/objects.inv theme: name: material palette: - primary: indigo - scheme: default + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + custom_dir: 'src/overrides' favicon: img/favicon.png + features: + - content.code.annotate + - content.code.copy + - navigation.indexes + - navigation.instant + - navigation.tracking + - search.suggest + - search.share + # https://github.com/kylebarron/cogeo-mosaic/blob/mkdocs/mkdocs.yml#L50-L75 markdown_extensions: - admonition @@ -124,7 +233,9 @@ markdown_extensions: - pymdownx.caret: insert: false - pymdownx.details - - pymdownx.emoji + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.escapeall: hardbreak: true nbsp: true diff --git a/docs/src/advanced/APIRoute_and_environment_variables.md b/docs/src/advanced/APIRoute_and_environment_variables.md deleted file mode 100644 index 36e1183a5..000000000 --- a/docs/src/advanced/APIRoute_and_environment_variables.md +++ /dev/null @@ -1,40 +0,0 @@ -!!! important - This has been deprecated. You can now pass `environment_dependency=lambda: {"GDAL_DISABLE_READDIR_ON_OPEN":"FALSE"}` to the Tiler Factory. This will be passed to a `rasterio.Env()` context manager on top of all gdal related blocks. - - ```python - from titiler.core.factory import TilerFactory - cog = TilerFactory( - reader=COGReader, - router_prefix="cog", - environment_dependency=lambda: {"GDAL_DISABLE_READDIR_ON_OPEN":"FALSE"}, - ) - ``` - -Sometimes, specifically when using GDAL, it can be useful to have environment variables set for certain endpoints -(e.g. when using Landsat data on AWS you need `GDAL_DISABLE_READDIR_ON_OPEN=FALSE` but you don't want this environment variable set for other endpoints). To be able to do this -we created a *custom* APIRoute class which wraps classic fastapi APIRoute with a `rasterio.Env()` block: https://github.com/developmentseed/titiler/blob/8a7127ca56631c2c327713d99e80285048c3aa6c/titiler/custom/routing.py#L13-L41 - -Example: - ```python - from fastapi import FastAPI, APIRouter - from rasterio._env import get_gdal_config - from titiler.core.routing import apiroute_factory - from titiler.core.factory import TilerFactory - - app = FastAPI() - route_class = apiroute_factory({"GDAL_DISABLE_READDIR_ON_OPEN": "FALSE"}) - router = APIRouter(route_class=route_class) - - tiler = TilerFactory(router=router) - - @router.get("/simple") - def simple(): - """should return FALSE.""" - res = get_gdal_config("GDAL_DISABLE_READDIR_ON_OPEN") - return {"env": res} - - app.include_router(router) - ``` - -!!! important - This has only be tested for python 3.6 and 3.7. diff --git a/docs/src/advanced/Extensions.md b/docs/src/advanced/Extensions.md index 9af9a7c2f..16bb665ad 100644 --- a/docs/src/advanced/Extensions.md +++ b/docs/src/advanced/Extensions.md @@ -33,28 +33,48 @@ class FactoryExtension(metaclass=abc.ABCMeta): ## Available extensions -#### cogValidateExtension +#### titiler.extensions.cogValidateExtension - Goal: adds a `/validate` endpoint which return the content of rio-cogeo `info` method - Additional requirements: `titiler.extensions["cogeo"]` (installs `rio-cogeo`) -#### cogViewerExtension +#### titiler.extensions.cogViewerExtension - Goal: adds a `/viewer` endpoint which return an HTML viewer for simple COGs -#### stacViewerExtension +#### titiler.extensions.stacViewerExtension - Goal: adds a `/viewer` endpoint which return an HTML viewer for STAC item -#### stacExtension +#### titiler.extensions.stacExtension - Goal: adds a `/stac` endpoint which return an HTML viewer for STAC item - Additional requirements: `titiler.extensions["stac"]` (installs `rio-stac`) -#### wmsExtension +#### titiler.extensions.wmsExtension - Goal: adds a `/wms` endpoint to support OGC WMS specification (`GetCapabilities` and `GetMap`) +#### titiler.extensions.wmtsExtension + +- Goal: adds a `/WMTSCapabilities.xml` endpoint to support OGC WMTS RESTFULL specification (`GetCapabilities` and `GetTile`) + +#### titiler.extensions.stacRenderExtenstion + +- Goal: adds `/render` and `/render/{render_id}` endpoints which return the contents of [STAC render extension](https://github.com/stac-extensions/render) and links to tileset.json and WMTS service + +#### titiler.xarray.DatasetMetadataExtension + +- Goal: adds `/dataset/`, `/dataset/keys` and `/datasets/dict` endpoints which return metadata about a multidimensional Dataset (not a DataArray) + +#### titiler.mosaic.extensions.wmts.wmtsExtension + +- Goal: adds `/WMTSCapabilities.xml` to support OGC WMTS RESTFULL specification (`GetCapabilities` and `GetTile`) + +#### titiler.mosaic.extensions.mosaicjson.MosaicJSONExtension + +- Goal: adds `/` and `/validate` endpoints to return MosaicJSON content and validate external mosaics. + ## How To ### Use extensions @@ -81,18 +101,16 @@ tiler = TilerFactory( app.include_router(tiler.router, prefix="/cog") ``` -See [titiler.application](../application) for a full example. - ### Create your own ```python from dataclasses import dataclass, field from typing import Tuple, List, Optional - +import rasterio from starlette.responses import Response from fastapi import Depends, FastAPI, Query -from titiler.core.factory import BaseTilerFactory, FactoryExtension, TilerFactory -from titiler.core.dependencies import RescalingParams +from titiler.core.factory import TilerFactory, FactoryExtension +from titiler.core.dependencies import ImageRenderingParams from titiler.core.factory import TilerFactory from titiler.core.resources.enums import ImageType @@ -104,8 +122,8 @@ class thumbnailExtension(FactoryExtension): # Set some options max_size: int = field(default=128) - # Register method is mandatory and must take a BaseTilerFactory object as input - def register(self, factory: BaseTilerFactory): + # Register method is mandatory and must take a TilerFactory object as input + def register(self, factory: TilerFactory): """Register endpoint to the tiler factory.""" # register an endpoint to the factory's router @@ -125,47 +143,37 @@ class thumbnailExtension(FactoryExtension): def thumbnail( # we can reuse the factory dependency src_path: str = Depends(factory.path_dependency), + reader_params=Depends(factory.reader_dependency), layer_params=Depends(factory.layer_dependency), dataset_params=Depends(factory.dataset_dependency), post_process=Depends(factory.process_dependency), - rescale: Optional[List[Tuple[float, ...]]] = Depends(RescalingParams), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(factory.colormap_dependency), render_params=Depends(factory.render_dependency), - reader_params=Depends(factory.reader_dependency), env=Depends(factory.environment_dependency), ): with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - im = src.preview( + with factory.reader(src_path, **reader_params.as_dict()) as src: + image = src.preview( max_size=self.max_size, - **layer_params, - **dataset_params, + **layer_params.as_dict(), + **dataset_params.as_dict(), ) if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) - - if color_formula: - image.apply_color_formula(color_formula) - format = ImageType.jpeg if image.mask.all() else ImageType.png - content = image.render( - img_format=format.driver, - colormap=colormap or dst_colormap, - **format.profile, - **render_params, + if post_process: + image = post_process(image) + + content, media_type = factory.render_func( + image, + colormap=colormap, + **render_params.as_dict(), ) - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) # Use it app = FastAPI() diff --git a/docs/src/advanced/customization.md b/docs/src/advanced/customization.md index 13bcbfaa6..330d9cb45 100644 --- a/docs/src/advanced/customization.md +++ b/docs/src/advanced/customization.md @@ -1,5 +1,32 @@ -`TiTiler` is designed to help user customize input/output for each endpoint. This section goes over some simple customization examples. +`TiTiler` is designed to help users customize input/output for each endpoint. This section goes over some simple customization examples. + +### Custom Colormap + +Add user defined colormap to the default colormaps provided by rio-tiler + +```python +from fastapi import FastAPI + +from rio_tiler.colormap import cmap as default_cmap + +from titiler.core.dependencies import create_colormap_dependency +from titiler.core.factory import TilerFactory + + +app = FastAPI(title="My simple app with custom TMS") + +cmap_values = { + "cmap1": {6: (4, 5, 6, 255)}, +} +# add custom colormap `cmap1` to the default colormaps +cmap = default_cmap.register(cmap_values) +ColorMapParams = create_colormap_dependency(cmap) + + +cog = TilerFactory(colormap_dependency=ColorMapParams) +app.include_router(cog.router) +``` ### Custom DatasetPathParams for `reader_dependency` @@ -13,7 +40,6 @@ import re from fastapi import FastAPI, HTTPException, Query -from titiler.core.dependencies import DefaultDependency from titiler.mosaic.factory import MosaicTilerFactory @@ -69,9 +95,11 @@ COGTilerWithCustomTMS = TilerFactory(supported_tms=tms) ### Add a MosaicJSON creation endpoint ```python -from dataclasses import dataclass from typing import List, Optional +from attrs import define + +from cogeo_mosaic.backends import MosaicBackend as MosaicJSONBackend from titiler.mosaic.factory import MosaicTilerFactory from titiler.core.errors import BadRequestError from cogeo_mosaic.mosaic import MosaicJSON @@ -102,9 +130,11 @@ class UpdateMosaicJSON(BaseModel): add_first: bool = True -@dataclass +@define(kw_only=True) class CustomMosaicFactory(MosaicTilerFactory): + backend: Type[MosaicJSONBackend] = MosaicJSONBackend + def register_routes(self): """Update the class method to add create/update""" super().register_routes() @@ -136,8 +166,10 @@ class CustomMosaicFactory(MosaicTilerFactory): # Write the MosaicJSON using a cogeo-mosaic backend with rasterio.Env(**env): - with self.reader( - body.url, mosaic_def=mosaic, reader=self.dataset_reader + with self.backend( + body.url, + mosaic_def=mosaic, + reader=self.dataset_reader ) as mosaic: try: mosaic.write(overwrite=body.overwrite) @@ -159,7 +191,7 @@ class CustomMosaicFactory(MosaicTilerFactory): ): """Update an existing MosaicJSON""" with rasterio.Env(**env): - with self.reader(body.url, reader=self.dataset_reader) as mosaic: + with self.backend(body.url, reader=self.dataset_reader) as mosaic: features = get_footprints(body.files, max_threads=body.max_threads) try: mosaic.update(features, add_first=body.add_first, quiet=True) diff --git a/docs/src/advanced/dependencies.md b/docs/src/advanced/dependencies.md index 937395e1f..4298a9ca4 100644 --- a/docs/src/advanced/dependencies.md +++ b/docs/src/advanced/dependencies.md @@ -5,31 +5,18 @@ In titiler `Factories`, we use the dependencies to define the inputs for each en Example: ```python -# Custom Dependency +from typing import Annotated from dataclasses import dataclass -from typing import Optional - from fastapi import Depends, FastAPI, Query from titiler.core.dependencies import DefaultDependency - -from rio_tiler.io import COGReader +from rio_tiler.io import Reader @dataclass class ImageParams(DefaultDependency): - """Common Preview/Crop parameters.""" - - max_size: Optional[int] = Query( - 1024, description="Maximum image size to read onto." - ) - height: Optional[int] = Query(None, description="Force output image height.") - width: Optional[int] = Query(None, description="Force output image width.") - - def __post_init__(self): - """Post Init.""" - if self.width and self.height: - self.max_size = None - + max_size: Annotated[ + int, Query(description="Maximum image size to read onto.") + ] = 1024 app = FastAPI() @@ -39,15 +26,10 @@ def preview( url: str = Query(..., description="data set URL"), params: ImageParams = Depends(), ): - - with COGReader(url) as cog: - img = cog.preview(**params) # classes built with `DefaultDependency` can be unpacked + with Reader(url) as cog: + img = cog.preview(**params.as_dict()) # we use `DefaultDependency().as_dict()` to pass only non-None parameters # or - img = cog.preview( - max_size=params.max_size, - height=params.height, - width=params.width, - ) + img = cog.preview(max_size=params.max_size) ... ``` @@ -55,61 +37,161 @@ def preview( In the example above, we create a custom `ImageParams` dependency which will then be injected to the `preview` endpoint to add **max_size**, **height** and **width** query string parameters. - Using `titiler.core.dependencies.DefaultDependency`, we can `unpack` the class as if it was a dictionary, which helps with customization. + Using `titiler.core.dependencies.DefaultDependency`, we can use `.as_dict(exclude_none=True/False)` method to `unpack` the object parameters. This can be useful if method or reader do not take the same parameters. +## titiler.core -## Factories Dependencies +#### AssetsParams -The `factories` allow users to set multiple default dependencies. Here is the list of common dependencies and their **default** values. +Define `assets`. -### BaseTilerFactory +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **assets** | Query (str) | No | None -#### path_dependency +
-Set dataset path (url). +```python +@dataclass +class AssetsParams(DefaultDependency): + """Assets parameters.""" + + assets: Annotated[ + list[str], + Query( + title="Asset names", + description="Asset's names.", + openapi_examples={ + "user-provided": {"value": None}, + "one-asset": { + "description": "Return results for asset `data`.", + "value": ["data"], + }, + "multi-assets": { + "description": "Return results for assets `data` and `cog`.", + "value": ["data", "cog"], + }, + }, + ), + ] +``` + +
+ +#### AssetsExprParams + +Define `assets`. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **assets** | Query (str) | Yes | +| **expression** | Query (str) | No | None +| **asset_as_band** | Query (bool) | No | False + +\* `assets` is required. + +
```python -def DatasetPathParams( - url: str = Query(..., description="Dataset URL") -) -> str: - """Create dataset path from args""" - return url +@dataclass +class AssetsExprParams(AssetsParams, ExpressionParams): + """Assets and Expression parameters.""" + + asset_as_band: Annotated[ + bool | None, + Query( + title="Consider asset as a 1 band dataset", + description="Asset as Band", + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if not self.assets: + raise MissingAssets("assets must be defined") ``` -#### layer_dependency +
+ +#### `BidxParams` + +Define band indexes. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **bidx** | Query (int) | No | None -Define band indexes or expression +
```python @dataclass class BidxParams(DefaultDependency): """Band Indexes parameters.""" - indexes: Optional[List[int]] = Query( - None, - title="Band indexes", - alias="bidx", - description="Dataset band indexes", - examples={"one-band": {"value": [1]}, "multi-bands": {"value": [1, 2, 3]}}, - ) + indexes: Annotated[ + Optional[List[int]], + Query( + title="Band indexes", + alias="bidx", + description="Dataset band indexes", + openapi_examples={ + "user-provided": {"value": None}, + "one-band": {"value": [1]}, + "multi-bands": {"value": [1, 2, 3]}, + }, + ), + ] = None +``` + +
+#### `ExpressionParams` + +Define band expression. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **expression** | Query (str) | No | None + + +
+ +```python @dataclass class ExpressionParams(DefaultDependency): """Expression parameters.""" - expression: Optional[str] = Query( - None, - title="Band Math expression", - description="rio-tiler's band math expression", - examples={ - "simple": {"description": "Simple band math.", "value": "b1/b2"}, - "multi-bands": { - "description": "Semicolon (;) delimited expressions (band1: b1/b2, band2: b2+b3).", - "value": "b1/b2;b2+b3", + expression: Annotated[ + Optional[str], + Query( + title="Band Math expression", + description="rio-tiler's band math expression", + openapi_examples={ + "user-provided": {"value": None}, + "simple": {"description": "Simple band math.", "value": "b1/b2"}, + "multi-bands": { + "description": "Semicolon (;) delimited expressions (band1: b1/b2, band2: b2+b3).", + "value": "b1/b2;b2+b3", + }, }, - }, - ) + ), + ] = None +``` +
+ +#### `BidxExprParams` + +Define band indexes or expression. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **bidx** | Query (int) | No | None +| **expression** | Query (str) | No | None + +
+ +```python @dataclass class BidxExprParams(ExpressionParams, BidxParams): """Band Indexes and Expression parameters.""" @@ -117,176 +199,422 @@ class BidxExprParams(ExpressionParams, BidxParams): pass ``` -#### dataset_dependency +
+ +#### `ColorMapParams` + +Colormap options. See [titiler.core.dependencies](https://github.com/developmentseed/titiler/blob/e46c35c8927b207f08443a274544901eb9ef3914/src/titiler/core/titiler/core/dependencies.py#L18-L54). -Overwrite nodata value, apply rescaling or change default resampling. +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **colormap_name** | Query (str) | No | None +| **colormap** | Query (encoded json) | No | None + +
+ +```python +cmap = {} + +def ColorMapParams( + colormap_name: Annotated[ # type: ignore + Literal[tuple(cmap.list())], + Query(description="Colormap name"), + ] = None, + colormap: Annotated[ + Optional[str], Query(description="JSON encoded custom Colormap") + ] = None, +): + if colormap_name: + return cmap.get(colormap_name) + + if colormap: + try: + c = json.loads( + colormap, + object_hook=lambda x: { + int(k): parse_color(v) for k, v in x.items() + }, + ) + + # Make sure to match colormap type + if isinstance(c, Sequence): + c = [(tuple(inter), parse_color(v)) for (inter, v) in c] + + return c + except json.JSONDecodeError as e: + raise HTTPException( + status_code=400, detail="Could not parse the colormap value." + ) from e + + return None +``` + +
+ +#### CoordCRSParams + +Define input Coordinate Reference System. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **crs** | Query (str) | No | None + + +
+ +```python +def CoordCRSParams( + crs: Annotated[ + Optional[str], + Query( + alias="coord_crs", + description="Coordinate Reference System of the input coords. Default to `epsg:4326`.", + ), + ] = None, +) -> Optional[CRS]: + """Coordinate Reference System Coordinates Param.""" + if crs: + return CRS.from_user_input(crs) + + return None +``` + +
+ +#### `DatasetParams` + +Overwrite `nodata` value, apply `rescaling` and change the `I/O` or `Warp` resamplings. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **nodata** | Query (str, int, float) | No | None +| **unscale** | Query (bool) | No | False +| **resampling** | Query (str) | No | 'nearest' +| **reproject** | Query (str) | No | 'nearest' + +
```python @dataclass class DatasetParams(DefaultDependency): """Low level WarpedVRT Optional parameters.""" - nodata: Optional[Union[str, int, float]] = Query( - None, title="Nodata value", description="Overwrite internal Nodata value" - ) - unscale: Optional[bool] = Query( - False, - title="Apply internal Scale/Offset", - description="Apply internal Scale/Offset", - ) - resampling_method: ResamplingName = Query( - ResamplingName.nearest, # type: ignore - alias="resampling", - description="Resampling method.", - ) + nodata: Annotated[ + Optional[Union[str, int, float]], + Query( + title="Nodata value", + description="Overwrite internal Nodata value", + ), + ] = None + unscale: Annotated[ + bool, + Query( + title="Apply internal Scale/Offset", + description="Apply internal Scale/Offset. Defaults to `False` in rio-tiler.", + ), + ] = False + resampling_method: Annotated[ + Optional[RIOResampling], + Query( + alias="resampling", + description="RasterIO resampling algorithm. Defaults to `nearest` in rio-tiler.", + ), + ] = None + reproject_method: Annotated[ + Optional[WarpResampling], + Query( + alias="reproject", + description="WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest` in rio-tiler.", + ), + ] = None def __post_init__(self): """Post Init.""" if self.nodata is not None: self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata) - self.resampling_method = self.resampling_method.value # type: ignore + + if self.unscale is not None: + self.unscale = bool(self.unscale) ``` -#### render_dependency +
-Image rendering options. +#### `DatasetPathParams` -```python -@dataclass -class ImageRenderingParams(DefaultDependency): - """Image Rendering options.""" +Set dataset path. - add_mask: bool = Query( - True, alias="return_mask", description="Add mask to the output data." - ) -``` +| Name | Type | Required | Default +| ------ | ----------|--------------------- |-------------- +| **url** | Query (str) | :warning: **Yes** :warning: | - -#### colormap_dependency -Colormap options. +
```python -def ColorMapParams( - colormap_name: ColorMapName = Query(None, description="Colormap name"), - colormap: str = Query(None, description="JSON encoded custom Colormap"), -) -> Optional[Union[Dict, Sequence]]: - """Colormap Dependency.""" - if colormap_name: - return cmap.get(colormap_name.value) +def DatasetPathParams( + url: Annotated[str, Query(description="Dataset URL")] +) -> str: + """Create dataset path from args""" + return url +``` - if colormap: - try: - return json.loads( - colormap, - object_hook=lambda x: {int(k): parse_color(v) for k, v in x.items()}, - ) - except json.JSONDecodeError: - raise HTTPException( - status_code=400, detail="Could not parse the colormap value." - ) +
+ + +#### DstCRSParams + +Define output Coordinate Reference System. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **crs** | Query (str) | No | None + + +
+ +```python +def DstCRSParams( + crs: Annotated[ + Optional[str], + Query( + alias="dst_crs", + description="Output Coordinate Reference System.", + ), + ] = None, +) -> Optional[CRS]: + """Coordinate Reference System Coordinates Param.""" + if crs: + return CRS.from_user_input(crs) return None ``` -#### reader_dependency +
-Additional reader options. Defaults to `DefaultDependency` (empty). +#### HistogramParams +Define *numpy*'s histogram options. -#### Other Attributes +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **histogram_bins** | Query (encoded list of Number) | No | 10 +| **histogram_range** | Query (encoded list of Number) | No | None -##### Supported TMS - -The TMS dependency sets the available TMS for a tile endpoint. +
```python -# Allow all morecantile TMS -from morecantile import tms as default_tms +@dataclass +class HistogramParams(DefaultDependency): + """Numpy Histogram options.""" + + bins: Annotated[ + Optional[str], + Query( + alias="histogram_bins", + title="Histogram bins.", + description=""" +Defines the number of equal-width bins in the given range (10, by default). + +If bins is a sequence (comma `,` delimited values), it defines a monotonically increasing array of bin edges, including the rightmost edge, allowing for non-uniform bin widths. + +link: https://numpy.org/doc/stable/reference/generated/numpy.histogram.html + """, + openapi_examples={ + "user-provided": {"value": None}, + "simple": { + "description": "Defines the number of equal-width bins", + "value": 8, + }, + "array": { + "description": "Defines custom bin edges (comma `,` delimited values)", + "value": "0,100,200,300", + }, + }, + ), + ] = None + + range: Annotated[ + Optional[str], + Query( + alias="histogram_range", + title="Histogram range", + description=""" +Comma `,` delimited range of the bins. -tiler = TilerFactory(supported_tms=default_tms) +The lower and upper range of the bins. If not provided, range is simply (a.min(), a.max()). +Values outside the range are ignored. The first element of the range must be less than or equal to the second. +range affects the automatic bin computation as well. -# Restrict the TMS to `WebMercatorQuad` only -from morecantile import tms -from morecantile.defaults import TileMatrixSets +link: https://numpy.org/doc/stable/reference/generated/numpy.histogram.html + """, + examples="0,1000", + ), + ] = None -# Construct a TileMatrixSets object with only the `WebMercatorQuad` tms -default_tms = TileMatrixSets({"WebMercatorQuad": tms.get("WebMercatorQuad")}) -tiler = TilerFactory(supported_tms=default_tms) + def __post_init__(self): + """Post Init.""" + if self.bins: + bins = self.bins.split(",") + if len(bins) == 1: + self.bins = int(bins[0]) # type: ignore + else: + self.bins = list(map(float, bins)) # type: ignore + else: + self.bins = 10 + + if self.range: + self.range = list(map(float, self.range.split(","))) # type: ignore ``` -##### Default TMS +
-Set the default's TMS Identifier (default to `WebMercatorQuad`). +#### `ImageRenderingParams` + +Control output image rendering options. + +| Name | Type | Required | Default +| ------ | ---------- |----------|-------------- +| **rescale** | Query (str, comma delimited Numer) | No | None +| **color_formula** | Query (str) | No | None +| **return_mask** | Query (bool) | No | False + +
```python -# Create a Tile with it's default TMS being `WGS1984Quad` -tiler = TilerFactory(default_tms="WGS1984Quad") +@dataclass +class ImageRenderingParams(DefaultDependency): + """Image Rendering options.""" + + rescale: Annotated[ + Optional[List[str]], + Query( + title="Min/Max data Rescaling", + description="comma (',') delimited Min,Max range. Can set multiple time for multiple bands.", + examples=["0,2000", "0,1000", "0,10000"], # band 1 # band 2 # band 3 + ), + ] = None + + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None + + add_mask: Annotated[ + Optional[bool], + Query( + alias="return_mask", + description="Add mask to the output data. Defaults to `True`", + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if self.rescale: + rescale_array = [] + for r in self.rescale: + parsed = tuple( + map( + float, + r.replace(" ", "").replace("[", "").replace("]", "").split(","), + ) + ) + assert ( + len(parsed) == 2 + ), f"Invalid rescale values: {self.rescale}, should be of form ['min,max', 'min,max'] or [[min,max], [min, max]]" + rescale_array.append(parsed) + + self.rescale: RescaleType = rescale_array # Noqa + ``` -### TilerFactory +
-The `TilerFactory` inherits dependency from `BaseTilerFactory`. +#### PartFeatureParams -#### metadata_dependency +Same as `PreviewParams` but without default `max_size`. -`rio_tiler.io.BaseReader.metadata()` methods options. +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **max_size** | Query (int) | No | None +| **height** | Query (int) | No | None +| **width** | Query (int) | No | None + +
```python @dataclass -class MetadataParams(DefaultDependency): - """Common Metadada parameters.""" - - # Required params - pmin: float = Query(2.0, description="Minimum percentile") - pmax: float = Query(98.0, description="Maximum percentile") +class PreviewParams(DefaultDependency): + """Common Preview parameters.""" - # Optional params - max_size: Optional[int] = Query( - None, description="Maximum image size to read onto." + # NOTE: sizes dependency can either be a Query or a Path Parameter + max_size: Annotated[int, Field(description="Maximum image size to read onto.")] = ( + 1024 ) - histogram_bins: Optional[int] = Query(None, description="Histogram bins.") - histogram_range: Optional[str] = Query( - None, description="comma (',') delimited Min,Max histogram bounds" - ) - bounds: Optional[str] = Query( - None, - descriptions="comma (',') delimited Bounding box coordinates from which to calculate image statistics.", + height: Annotated[ + Optional[int], Field(description="Force output image height.") + ] = None + width: Annotated[Optional[int], Field(description="Force output image width.")] = ( + None ) def __post_init__(self): """Post Init.""" - if self.max_size is not None: - self.kwargs["max_size"] = self.max_size - - if self.bounds: - self.kwargs["bounds"] = tuple(map(float, self.bounds.split(","))) - - hist_options = {} - if self.histogram_bins: - hist_options.update(dict(bins=self.histogram_bins)) - if self.histogram_range: - hist_options.update( - dict(range=list(map(float, self.histogram_range.split(",")))) - ) - if hist_options: - self.kwargs["hist_options"] = hist_options + if self.width or self.height: + self.max_size = None ``` -#### img_dependency +
+ +#### PixelSelectionParams -Used in Crop/Preview to define size of the output image. +In `titiler.mosaic`, define pixel-selection method to apply. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **pixel_selection** | Query (str) | No | 'first' + + +
+ +```python +def PixelSelectionParams( + pixel_selection: Annotated[ # type: ignore + Literal[tuple([e.name for e in PixelSelectionMethod])], + Query(description="Pixel selection method."), + ] = "first", +) -> MosaicMethodBase: + """ + Returns the mosaic method used to combine datasets together. + """ + return PixelSelectionMethod[pixel_selection].value() +``` + +
+ +#### PreviewParams + +Define image output size. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **max_size** | Query (int) | No | 1024 +| **height** | Query (int) | No | None +| **width** | Query (int) | No | None + +
```python @dataclass -class ImageParams(DefaultDependency): - """Common Preview/Crop parameters.""" +class PreviewParams(DefaultDependency): + """Common Preview parameters.""" - max_size: Optional[int] = Query( - 1024, description="Maximum image size to read onto." - ) - height: Optional[int] = Query(None, description="Force output image height.") - width: Optional[int] = Query(None, description="Force output image width.") + max_size: Annotated[int, "Maximum image size to read onto."] = 1024 + height: Annotated[Optional[int], "Force output image height."] = None + width: Annotated[Optional[int], "Force output image width."] = None def __post_init__(self): """Post Init.""" @@ -294,70 +622,327 @@ class ImageParams(DefaultDependency): self.max_size = None ``` -### MultiBaseTilerFactory +
-The `MultiBaseTilerFactory` inherits dependency from `TilerFactory` and `BaseTilerFactory`. +#### StatisticsParams -#### assets_dependency +Define options for *rio-tiler*'s statistics method. -Define `assets`. +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **categorical** | Query (bool) | No | False +| **categories** | Query (list of Number) | No | None +| **p** | Query (list of Number) | No | [2, 98] + +
```python @dataclass -class AssetsParams(DefaultDependency): - """Assets parameters.""" +class StatisticsParams(DefaultDependency): + """Statistics options.""" + + categorical: Annotated[ + Optional[bool], + Query(description="Return statistics for categorical dataset. Defaults to `False` in rio-tiler"), + ] = None + categories: Annotated[ + Optional[List[Union[float, int]]], + Query( + alias="c", + title="Pixels values for categories.", + description="List of values for which to report counts.", + examples=[1, 2, 3], + ), + ] = None + percentiles: Annotated[ + Optional[List[int]], + Query( + alias="p", + title="Percentile values", + description="List of percentile values (default to [2, 98]).", + examples=[2, 5, 95, 98], + ), + ] = None - assets: List[str] = Query( - None, - title="Asset names", - description="Asset's names.", - examples={ - "one-asset": { - "description": "Return results for asset `data`.", - "value": ["data"], - }, - "multi-assets": { - "description": "Return results for assets `data` and `cog`.", - "value": ["data", "cog"], - }, - }, - ) + def __post_init__(self): + """Set percentiles default.""" + if not self.percentiles: + self.percentiles = [2, 98] ``` -### MultiBandTilerFactory +
-The `MultiBaseTilerFactory` inherits dependency from `TilerFactory` and `BaseTilerFactory`. +#### TileParams -#### bands_dependency +Define `buffer` and `padding` to apply at tile creation. -Define `bands`. +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **buffer** | Query (float) | No | None +| **padding** | Query (int) | No | None + +
```python @dataclass -class BandsParams(DefaultDependency): - """Band names parameters.""" - - bands: List[str] = Query( - None, - title="Band names", - description="Band's names.", - examples={ - "one-band": { - "description": "Return results for band `B01`.", - "value": ["B01"], - }, - "multi-bands": { - "description": "Return results for bands `B01` and `B02`.", - "value": ["B01", "B02"], - }, - }, - ) +class TileParams(DefaultDependency): + """Tile options.""" + + buffer: Annotated[ + Optional[float], + Query( + gt=0, + title="Tile buffer.", + description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", + ), + ] = None + + padding: Annotated[ + Optional[int], + Query( + gt=0, + title="Tile padding.", + description="Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`.", + ), + ] = None +``` + +
+ +#### `algorithm.dependency` + +Control which `algorithm` to apply to the data. + +See [titiler.core.algorithm](https://github.com/developmentseed/titiler/blob/e46c35c8927b207f08443a274544901eb9ef3914/src/titiler/core/titiler/core/algorithm/__init__.py#L54-L79). + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **algorithm** | Query (str) | No | None +| **algorithm_params** | Query (encoded json) | No | None + +
+ +```python +algorithms = {} + +def post_process( + algorithm: Annotated[ + Literal[tuple(algorithms.keys())], + Query(description="Algorithm name"), + ] = None, + algorithm_params: Annotated[ + Optional[str], + Query(description="Algorithm parameter"), + ] = None, +) -> Optional[BaseAlgorithm]: + """Data Post-Processing options.""" + kwargs = json.loads(algorithm_params) if algorithm_params else {} + if algorithm: + try: + return algorithms.get(algorithm)(**kwargs) + + except ValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + return None +``` + +
+ +## titiler.xarray + + +#### XarrayIOParams + +Define Xarray's `open_args` to `xarray.open_dataset`. + +| Name | Type | Required | Default +| ------ | ---------- |----------|-------------- +| **group** | Query (str) | No | None +| **decode_times** | Query (bool)| No | None + +
+ +```python +@dataclass +class XarrayIOParams(DefaultDependency): + """Dataset IO Options.""" + + group: Annotated[ + Optional[str], + Query( + description="Select a specific zarr group from a zarr hierarchy. Could be associated with a zoom level or dataset." + ), + ] = None + + decode_times: Annotated[ + Optional[bool], + Query( + title="decode_times", + description="Whether to decode times", + ), + ] = None ``` -### MosaicTilerFactory +
-The `MultiBaseTilerFactory` inherits dependency from `BaseTilerFactory`. +#### XarrayDsParams -#### backend_dependency +Define options to select a **variable** within a Xarray Dataset. + +| Name | Type | Required | Default +| ------ | ---------- |----------|-------------- +| **variable** | Query (str) | Yes | None +| **sel** | Query (list of str) | No | None + +
+ +```python +@dataclass +class XarrayDsParams(DefaultDependency): + """Xarray Dataset Options.""" + + variable: Annotated[str, Query(description="Xarray Variable name.")] + + sel: Annotated[ + Optional[List[SelDimStr]], + Query( + description="Xarray Indexing using dimension names `{dimension}={value}` or `{dimension}={method}::{value}`.", + ), + ] = None +``` + +
+ + +#### XarrayParams + +Combination of `XarrayIOParams` and `XarrayDsParams` + +| Name | Type | Required | Default +| ------ | ---------- |----------|-------------- +| **group** | Query (str) | No | None +| **decode_times** | Query (bool)| No | None +| **variable** | Query (str) | Yes | None +| **sel** | Query (list of str) | No | None + +
+ +```python +@dataclass +class XarrayParams(XarrayIOParams, XarrayDsParams): + """Xarray Reader dependency.""" + + pass +``` + +
+ +#### CompatXarrayParams + +same as `XarrayParams` but with optional `variable` option. + +| Name | Type | Required | Default +| ------ | ---------- |----------|-------------- +| **group** | Query (str) | No | None +| **decode_times** | Query (bool)| No | None +| **variable** | Query (str) | No | None +| **sel** | Query (list of str) | No | None + +
+ +```python +@dataclass +class XarrayParams(XarrayIOParams, XarrayDsParams): + """Xarray Reader dependency.""" + + pass +``` + +
+ + +#### DatasetParams + +Same as `titiler.core.dependencies.DatasetParams` but with only `nodata` and `reproject` + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **nodata** | Query (str, int, float) | No | None +| **reproject** | Query (str) | No | 'nearest' + +
+ +```python +@dataclass +class DatasetParams(DefaultDependency): + """Low level WarpedVRT Optional parameters.""" + + nodata: Annotated[ + Optional[Union[str, int, float]], + Query( + title="Nodata value", + description="Overwrite internal Nodata value", + ), + ] = None + reproject_method: Annotated[ + Optional[WarpResampling], + Query( + alias="reproject", + description="WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`.", + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if self.nodata is not None: + self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata) +``` + +
+ + +#### PartFeatureParams + +Same as `titiler.core.dependencies.PartFeatureParams` but with `resampling` option + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **max_size** | Query (int) | No | None +| **height** | Query (int) | No | None +| **width** | Query (int) | No | None +| **resampling** | Query (str) | No | 'nearest' + + +
+ +```python +@dataclass +class PartFeatureParams(DefaultDependency): + """Common parameters for bbox and feature.""" + + # NOTE: the part sizes dependency can either be a Query or a Path Parameter + max_size: Annotated[ + Optional[int], Field(description="Maximum image size to read onto.") + ] = None + height: Annotated[ + Optional[int], Field(description="Force output image height.") + ] = None + width: Annotated[Optional[int], Field(description="Force output image width.")] = ( + None + ) + resampling_method: Annotated[ + Optional[RIOResampling], + Query( + alias="resampling", + description="RasterIO resampling algorithm. Defaults to `nearest`.", + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if self.width or self.height: + self.max_size = None +``` -Additional backend options. Defaults to `DefaultDependency` (empty). +
diff --git a/docs/src/advanced/endpoints_factories.md b/docs/src/advanced/endpoints_factories.md new file mode 100644 index 000000000..20e74f27a --- /dev/null +++ b/docs/src/advanced/endpoints_factories.md @@ -0,0 +1,406 @@ + +TiTiler's endpoints factories are helper functions that let users create a FastAPI *router* (`fastapi.APIRouter`) with a minimal set of endpoints. + +!!! Important + + Most of `tiler` **Factories** are built around [`rio_tiler.io.BaseReader`](https://cogeotiff.github.io/rio-tiler/advanced/custom_readers/), which defines basic methods to access datasets (e.g COG or STAC). The default reader is `Reader` for `TilerFactory` and `MosaicBackend` for `MosaicTilerFactory`. + + Factories classes use [dependencies injection](dependencies.md) to define most of the endpoint options. + +## titiler.core + +### BaseFactory + +class: `titiler.core.factory.BaseFactory` + +Most **Factories** are built from this [abstract based class](https://docs.python.org/3/library/abc.html) which is used to define commons attributes and utility functions shared between all factories. + +#### Attributes + +- **router**: FastAPI router. Defaults to `fastapi.APIRouter`. +- **router_prefix**: Set prefix to all factory's endpoint. Defaults to `""`. +- **route_dependencies**: Additional routes dependencies to add after routes creations. Defaults to `[]`. +- **extension**: TiTiler extensions to register after endpoints creations. Defaults to `[]`. +- **name**: Name of the Endpoints group. Defaults to `None`. +- **operation_prefix** (*private*): Endpoint's `operationId` prefix. Defined by `self.name` or `self.router_prefix.replace("/", ".")`. +- **conforms_to**: Set of conformance classes the Factory implement + +#### Methods + +- **register_routes**: Abstract method which needs to be define by each factories. +- **url_for**: Method to construct endpoint URL +- **add_route_dependencies**: Add dependencies to routes. + +### TilerFactory + +class: `titiler.core.factory.TilerFactory` + +Factory meant to create endpoints for single dataset using [*rio-tiler*'s `Reader`](https://cogeotiff.github.io/rio-tiler/readers/#rio_tileriorasterioreader). + +#### Attributes + +- **reader**: Dataset Reader **required**. +- **reader_dependency**: Dependency to control options passed to the reader instance init. Defaults to `titiler.core.dependencies.DefaultDependency` +- **path_dependency**: Dependency to use to define the dataset url. Defaults to `titiler.core.dependencies.DatasetPathParams`. +- **layer_dependency**: Dependency to define band indexes or expression. Defaults to `titiler.core.dependencies.BidxExprParams`. +- **dataset_dependency**: Dependency to overwrite `nodata` value, apply `rescaling` and change the `I/O` or `Warp` resamplings. Defaults to `titiler.core.dependencies.DatasetParams`. +- **tile_dependency**: Dependency to define `buffer` and `padding` to apply at tile creation. Defaults to `titiler.core.dependencies.TileParams`. +- **stats_dependency**: Dependency to define options for *rio-tiler*'s statistics method used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.StatisticsParams`. +- **histogram_dependency**: Dependency to define *numpy*'s histogram options used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.HistogramParams`. +- **img_preview_dependency**: Dependency to define image size for `/preview` and `/statistics` endpoints. Defaults to `titiler.core.dependencies.PreviewParams`. +- **img_part_dependency**: Dependency to define image size for `/bbox` and `/feature` endpoints. Defaults to `titiler.core.dependencies.PartFeatureParams`. +- **process_dependency**: Dependency to control which `algorithm` to apply to the data. Defaults to `titiler.core.algorithm.algorithms.dependency`. +- **colormap_dependency**: Dependency to define the Colormap options. Defaults to `titiler.core.dependencies.ColorMapParams` +- **render_dependency**: Dependency to control output image rendering options. Defaults to `titiler.core.dependencies.ImageRenderingParams` +- **environment_dependency**: Dependency to define GDAL environment at runtime. Default to `lambda: {}`. +- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. +- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`. +- **render_func**: Image rendering method. Defaults to `titiler.core.utils.render_image`. +- **add_preview**: Add `/preview` endpoint to the router. Defaults to `True`. +- **add_part**: Add `/bbox` and `/feature` endpoints to the router. Defaults to `True`. +- **add_viewer**: Add `/{TileMatrixSetId}/map.html` endpoints to the router. Defaults to `True`. +- **add_ogc_maps**: Add `/map` endoint (OGC Maps API) to the router. Defaults to `False`. + +#### Endpoints + +```python +from fastapi import FastAPI + +from titiler.core.factory import TilerFactory + +# Create FastAPI application +app = FastAPI() + +# Create router and register set of endpoints +cog = TilerFactory( + add_preview=True, + add_part=True, + add_viewer=True, + add_ogc_maps=True, +) + +# add router endpoint to the main application +app.include_router(cog.router) +``` + +| Method | URL | Output | Description +| ------ | --------------------------------------------------------------- |-------------------------------------------- |-------------- +| `GET` | `/info` | JSON ([Info][info_model]) | return dataset's basic info +| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return dataset's basic info as a GeoJSON feature +| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return dataset's statistics +| `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return dataset's statistics for a GeoJSON +| `GET` | `/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/{tileMatrixSetId}/map.html` | HTML | return a simple map viewer **Optional** +| `GET` | `/{tileMatrixSetId}/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel values from a dataset +| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset **Optional** +| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature **Optional** +| `GET` | `/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from a dataset **Optional** +| `GET` | `/maps` | image/bin | create maps from a dataset **Optional** + + +### MultiBaseTilerFactory + +class: `titiler.core.factory.MultiBaseTilerFactory` + +Custom `TilerFactory` to be used with [`rio_tiler.io.MultiBaseReader`](https://cogeotiff.github.io/rio-tiler/advanced/custom_readers/#multibasereader) type readers (e.g [`rio_tiler.io.STACReader`](https://cogeotiff.github.io/rio-tiler/readers/#rio_tileriostacstacreader)). + +#### Attributes + +- **reader**: `rio_tiler.io.base.MultiBaseReader` Dataset Reader **required**. +- **layer_dependency**: Dependency to define assets or expression. Defaults to `titiler.core.dependencies.AssetsExprParams`. +- **assets_dependency**: Dependency to define assets to be used. Defaults to `titiler.core.dependencies.AssetsParams`. + +#### Endpoints + +```python +from fastapi import FastAPI + +from rio_tiler.io import STACReader # STACReader is a MultiBaseReader + +from titiler.core.factory import MultiBaseTilerFactory + +app = FastAPI() +stac = MultiBaseTilerFactory( + reader=STACReader, + add_preview=True, + add_part=True, + add_viewer=True, + add_ogc_maps=True, +) +app.include_router(stac.router) +``` + +| Method | URL | Output | Description +| ------ | --------------------------------------------------------------- |------------------------------------------------- |-------------- +| `GET` | `/assets` | JSON | return the list of available assets +| `GET` | `/info` | JSON ([Info][multiinfo_model]) | return assets basic info +| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][multiinfo_geojson_model]) | return assets basic info as a GeoJSON feature +| `GET` | `/asset_statistics` | JSON ([Statistics][multistats_model]) | return per asset statistics +| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return assets statistics (merged) +| `POST` | `/statistics` | GeoJSON ([Statistics][multistats_geojson_model]) | return assets statistics for a GeoJSON (merged) +| `GET` | `/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[.{format}]` | image/bin | create a web map tile image from assets +| `GET` | `/{tileMatrixSetId}/map.html` | HTML | return a simple map viewer **Optional** +| `GET` | `/{tileMatrixSetId}/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `/point/{lon},{lat}` | JSON ([Point][multipoint_model]) | return pixel values from assets +| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets **Optional** +| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature intersecting assets **Optional** +| `GET` | `/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from assets **Optional** +| `GET` | `/map` | image/bin | create maps from a dataset **Optional** + +### TMSFactory + +class: `titiler.core.factory.TMSFactory` + +Endpoints factory for OGC `TileMatrixSets`. + +#### Attributes + +- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. + +```python +from fastapi import FastAPI + +from titiler.core.factory import TMSFactory + +app = FastAPI() +tms = TMSFactory() +app.include_router(tms.router) +``` + +#### Endpoints + +| Method | URL | Output | Description +| ------ | ------------------------------------- |----------------------------------------------- |-------------- +| `GET` | `/tileMatrixSets` | JSON ([TileMatrixSetList][tilematrixset_list]) | retrieve the list of available tiling schemes (tile matrix sets) +| `GET` | `/tileMatrixSets/{tileMatrixSetId}` | JSON ([TileMatrixSet][tilematrixset]) | retrieve the definition of the specified tiling scheme (tile matrix set) + + +### AlgorithmFactory + +class: `titiler.core.factory.AlgorithmFactory` + +Endpoints factory for custom algorithms. + +#### Attributes + +- **supported_algorithm**: List of available `Algorithm`. Defaults to `titiler.core.algorithm.algorithms`. + +```python +from fastapi import FastAPI + +from titiler.core.factory import AlgorithmFactory + +app = FastAPI() +algo = AlgorithmFactory() +app.include_router(algo.router) +``` + +#### Endpoints + +| Method | URL | Output | Description +| ------ | ---------------------------- |--------------------------------------------------------- |-------------- +| `GET` | `/algorithms` | JSON (Dict of [Algorithm Metadata][algorithm_metadata]) | retrieve the list of available Algorithms +| `GET` | `/algorithms/{algorithmId}` | JSON ([Algorithm Metadata][algorithm_metadata]) | retrieve the metadata of the specified algorithm. + + +### ColorMapFactory + +class: `titiler.core.factory.ColorMapFactory` + +Endpoints factory for colorMaps metadata. + +#### Attributes + +- **supported_colormaps**: List of available `ColorMaps`. Defaults to `rio_tiler.colormap.cmap`. + +```python +from fastapi import FastAPI + +from titiler.core.factory import ColorMapFactory + +app = FastAPI() +colormap = ColorMapFactory() +app.include_router(colormap.router) +``` + +#### Endpoints + +| Method | URL | Output | Description +| ------ | ---------------------------- |-------------------------------------- |-------------- +| `GET` | `/colorMaps` | JSON ([colorMapList][colormap_list]) | retrieve the list of available colorMaps +| `GET` | `/colorMaps/{colorMapId}` | JSON ([colorMap][colormap]) | retrieve the metadata or image of the specified colorMap. + + +## titiler.mosaic + +### MosaicTilerFactory + +class: `titiler.mosaic.factory.MosaicTilerFactory` + +Endpoints factory for mosaics. + +#### Attributes + +- **backend**: `rio_tiler.mosaic.backends.BaseBackend` Mosaic backend. +- **backend_dependency**: Dependency to control options passed to the backend instance init. Defaults to `titiler.core.dependencies.DefaultDependency` +- **dataset_reader**: Dataset Reader. Defaults to `rio_tiler.io.Reader` +- **reader_dependency**: Dependency to control options passed to the reader instance init. Defaults to `titiler.core.dependencies.DefaultDependency` +- **path_dependency**: Dependency to use to define the dataset. Defaults to `titiler.mosaic.factory.DatasetPathParams`. +- **assets_accessor_dependency**: Dependency to define options to be forwarded to the backend `get_assets` method. Defaults to `titiler.core.dependencies.DefaultDependency`. +- **layer_dependency**: Dependency to define band indexes or expression. Defaults to `titiler.core.dependencies.BidxExprParams`. +- **dataset_dependency**: Dependency to overwrite `nodata` value, apply `rescaling` and change the `I/O` or `Warp` resamplings. Defaults to `titiler.core.dependencies.DatasetParams`. +- **tile_dependency**: Dependency to define `buffer` and `padding` to apply at tile creation. Defaults to `titiler.core.dependencies.TileParams`. +- **process_dependency**: Dependency to control which `algorithm` to apply to the data. Defaults to `titiler.core.algorithm.algorithms.dependency`. +- **stats_dependency**: Dependency to define options for *rio-tiler*'s statistics method used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.StatisticsParams`. +- **histogram_dependency**: Dependency to define *numpy*'s histogram options used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.HistogramParams`. +- **colormap_dependency**: Dependency to define the Colormap options. Defaults to `titiler.core.dependencies.ColorMapParams` +- **render_dependency**: Dependency to control output image rendering options. Defaults to `titiler.core.dependencies.ImageRenderingParams` +- **pixel_selection_dependency**: Dependency to select the `pixel_selection` method. Defaults to `titiler.mosaic.factory.PixelSelectionParams`. +- **environment_dependency**: Dependency to define GDAL environment at runtime. Default to `lambda: {}`. +- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. +- **render_func**: Image rendering method. Defaults to `titiler.core.utils.render_image`. +- **optional_headers**: List of OptionalHeader which endpoints could add (if implemented). Defaults to `[]`. +- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`. +- **add_viewer**: Add `/{TileMatrixSetId}/map.html` endpoints to the router. Defaults to `True`. +- **add_statistics**: Add `POST - /statistics` endpoints to the router. Defaults to `False`. +- **add_part**: Add `/bbox` and `/feature` endpoints to the router. Defaults to `False`. +- **add_ogc_maps**: Add `/map` endpoints to the router. Default to `False`. +- **conforms_to**: Set of conformance classes the Factory implement + +#### Endpoints + +| Method | URL | Output | Description +| ------ | --------------------------------------------------------------- |--------------------------------------------------- |-------------- +| `GET` | `/info` | JSON ([Info][mosaic_info_model]) | return mosaic's basic info +| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][mosaic_geojson_info_model]) | return mosaic's basic info as a GeoJSON feature +| `GET` | `/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[.{format}]` | image/bin | create a web map tile image from a MosaicJSON +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}/assets` | JSON | return list of assets intersecting a XYZ tile +| `GET` | `/{tileMatrixSetId}/map.html` | HTML | return a simple map viewer **Optional** +| `GET` | `/{tileMatrixSetId}/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `/point/{lon},{lat}` | JSON ([Point][mosaic_point]) | return pixel value from a MosaicJSON dataset +| `GET` | `/point/{lon},{lat}/assets` | JSON | return list of assets intersecting a point +| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}/assets` | JSON | return list of assets intersecting a bounding box +| `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return info and statistics for a dataset **Optional** +| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset **Optional** +| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature **Optional** +| `GET` | `/map` | image/bin | create maps from a dataset **Optional** + +```python +from fastapi import FastAPI + +from titiler.mosaic.factory import MosaicTilerFactory +from cogeo_mosaic.backends import MosaicBackend as MosaicJSONBackend + +# Create FastAPI application +app = FastAPI() + +# Create router and register set of endpoints +mosaic = TilerFactory( + backend=MosaicJSONBackend, + add_part=True, # default to False + add_statistics=True, # default to False + add_ogc_maps=True, # default to False +) + +# add router endpoint to the main application +app.include_router(mosaic.router) +``` + +## titiler.xarray + +### TilerFactory + +class: `titiler.xarray.factory.TilerFactory` + +#### Attributes + +- **reader**: Dataset Reader **required**. +- **path_dependency**: Dependency to use to define the dataset url. Defaults to `titiler.core.dependencies.DatasetPathParams`. +- **reader_dependency**: Dependency to control options passed to the reader instance init. Defaults to `titiler.xarray.dependencies.XarrayParams` +- **layer_dependency**: Dependency to define band indexes or expression. Defaults to `titiler.core.dependencies.BidxParams`. +- **dataset_dependency**: Dependency to overwrite `nodata` value and change the `Warp` resamplings. Defaults to `titiler.xarray.dependencies.DatasetParams`. +- **tile_dependency**: Dependency for tile creation options. Defaults to `titiler.core.dependencies.DefaultDependency`. +- **stats_dependency**: Dependency to define options for *rio-tiler*'s statistics method used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.StatisticsParams`. +- **histogram_dependency**: Dependency to define *numpy*'s histogram options used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.HistogramParams`. +- **img_part_dependency**: Dependency to define image size for `/bbox` and `/feature` endpoints. Defaults to `titiler.xarray.dependencies.PartFeatureParams`. +- **process_dependency**: Dependency to control which `algorithm` to apply to the data. Defaults to `titiler.core.algorithm.algorithms.dependency`. +- **colormap_dependency**: Dependency to define the Colormap options. Defaults to `titiler.core.dependencies.ColorMapParams` +- **render_dependency**: Dependency to control output image rendering options. Defaults to `titiler.core.dependencies.ImageRenderingParams` +- **environment_dependency**: Dependency to define GDAL environment at runtime. Default to `lambda: {}`. +- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. +- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`. +- **add_part**: Add `/bbox` and `/feature` endpoints to the router. Defaults to `True`. +- **add_viewer**: Add `/{TileMatrixSetId}/map.html` endpoints to the router. Defaults to `True`. +- **add_ogc_maps**: Add `/map` endpoints to the router. Default to `False`. +- **add_preview**: Add `/preview` endpoints to the router. Default to `False`. + +```python +from fastapi import FastAPI + +from titiler.xarray.factory import TilerFactory + +# Create FastAPI application +app = FastAPI() + +# Create router and register set of endpoints +md = TilerFactory( + add_part=True, # default to True + add_viewer=True, # default to True + add_preview=True, # default to False +) + +# add router endpoint to the main application +app.include_router(md.router) +``` + +#### Endpoints + +| Method | URL | Output | Description +| ------ | --------------------------------------------------------------- |-------------------------------------------- |-------------- +| `GET` | `/info` | JSON ([Info][info_model]) | return dataset's basic info +| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return dataset's basic info as a GeoJSON feature +| `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return dataset's statistics for a GeoJSON +| `GET` | `/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/{tileMatrixSetId}/map.html` | HTML | return a simple map viewer **Optional** +| `GET` | `/{tileMatrixSetId}/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel values from a dataset +| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset **Optional** +| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature **Optional** +| `GET` | `/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from a dataset **Optional** + + +[bounds_model]: https://github.com/cogeotiff/rio-tiler/blob/9aaa88000399ee8d36e71d176f67b6ea3ec53f2d/rio_tiler/models.py#L43-L46 +[info_model]: https://github.com/cogeotiff/rio-tiler/blob/9aaa88000399ee8d36e71d176f67b6ea3ec53f2d/rio_tiler/models.py#L56-L72 +[info_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L30 +[tilejson_model]: https://github.com/developmentseed/titiler/blob/2335048a407f17127099cbbc6c14e1328852d619/src/titiler/core/titiler/core/models/mapbox.py#L16-L38 +[point_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L11-L20 +[stats_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L32 +[stats_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L46-L49 + +[multiinfo_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L52 +[multiinfo_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L53 +[multipoint_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L23-L27 +[multistats_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L55 +[multistats_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L56-L59 + +[mosaic_info_model]: https://github.com/developmentseed/cogeo-mosaic/blob/1dc3c873472c8cf7634ad893b9cdc40105ca3874/cogeo_mosaic/models.py#L9-L17 +[mosaic_geojson_info_model]: https://github.com/developmentseed/titiler/blob/2bd1b159a9cf0932ad14e9eabf1e4e66498adbdc/src/titiler/mosaic/titiler/mosaic/factory.py#L130 +[mosaic_model]: https://github.com/developmentseed/cogeo-mosaic/blob/1dc3c873472c8cf7634ad893b9cdc40105ca3874/cogeo_mosaic/mosaic.py#L55-L72 +[mosaic_point]: https://github.com/developmentseed/titiler/blob/2bd1b159a9cf0932ad14e9eabf1e4e66498adbdc/src/titiler/mosaic/titiler/mosaic/models/responses.py#L8-L17 + +[tilematrixset_list]: https://github.com/developmentseed/titiler/blob/ffd67af34c2807a6e1447817f943446a58441ed8/src/titiler/core/titiler/core/models/OGC.py#L33-L40 +[tilematrixset]: https://github.com/developmentseed/morecantile/blob/eec54326ce2b134cfbc03dd69a3e2938e4109101/morecantile/models.py#L399-L490 + +[algorithm_metadata]: https://github.com/developmentseed/titiler/blob/ffd67af34c2807a6e1447817f943446a58441ed8/src/titiler/core/titiler/core/algorithm/base.py#L32-L40 + +[colormap_list]: https://github.com/developmentseed/titiler/blob/535304fd7e1b0bfbb791bdec8cbfb6e78b4a6eb5/src/titiler/core/titiler/core/models/responses.py#L51-L55 +[colormap]: https://github.com/cogeotiff/rio-tiler/blob/6343b571a367ef63a10d6807e3d907c3283ebb20/rio_tiler/types.py#L24-L27 diff --git a/docs/src/advanced/performance_tuning.md b/docs/src/advanced/performance_tuning.md index 94201cdff..5d9c1044a 100644 --- a/docs/src/advanced/performance_tuning.md +++ b/docs/src/advanced/performance_tuning.md @@ -1,6 +1,6 @@ ## Overview -Titiler makes use of several great underlying libraries, including [GDAL][gdal] +TiTiler makes use of several great underlying libraries, including [GDAL][gdal] and [Python bindings to GDAL][rasterio]. An effective deployment of titiler generally requires tweaking GDAL configuration settings. This document provides an overview of relevant settings. Full documentation from GDAL is available @@ -15,7 +15,7 @@ an overview of relevant settings. Full documentation from GDAL is available ### Setting a config variable GDAL configuration is modified using environment variables. Thus in order to -change a setting you'll need to set environment variables through your +change a setting, you'll need to set environment variables through your deployment mechanism. For example, in order to test locally you'd set an environment variable in bash: @@ -66,10 +66,10 @@ files, so if you wished to read this data, you'd want #### `GDAL_INGESTED_BYTES_AT_OPEN` -Gives the number of initial bytes GDAL should read when opening a file and +Defines the number of initial bytes GDAL should read when opening a file and inspecting its metadata. -Titiler works best with Cloud-Optimized GeoTIFFs (COGs) because they have a +TiTiler works best with Cloud-Optimized GeoTIFFs (COGs) because they have a tiled internal structure that supports efficient random reads. These files have an initial metadata section that describes the location (byte range) within the file of each internal tile. The more internal tiles the COG has, the more data diff --git a/docs/src/advanced/rendering.md b/docs/src/advanced/rendering.md deleted file mode 100644 index ea31bfd3a..000000000 --- a/docs/src/advanced/rendering.md +++ /dev/null @@ -1,115 +0,0 @@ -# Rendering Options - -When using Titiler to visualize imagery, there are some helper options that change how the data appears on the screen. You can: - -1. Adjust band values using basic color-oriented image operations -2. Apply color maps to create heat maps, colorful terrain based on band value -3. Rescale images on a per-band basis - -## Color Map - -Color maps are arrays of colors, used to map pixel values to specific colors. For example, it is possible to map a single band DEM, where pixel values denote height, to a color map which shows higher values as white: - -![color map example](../img/colormap.png) - -Titiler supports both default colormaps (each with a name) and custom color maps. - -### Default Colormaps - -Default colormaps pre-made, each with a given name. These maps come from the `rio-tiler` library, which has taken colormaps packaged with Matplotlib and has added others that are commonly used with raster data. - -A list of available color maps can be found in Titiler's Swagger docs, or in the [rio-tiler documentation](https://cogeotiff.github.io/rio-tiler/colormap/#default-rio-tilers-colormaps). - -To use a default colormap, simply use the parameter `colormap_name`: - -```python3 -import requests - -resp = requests.get("titiler.xyz/cog/preview", params={ - "url": "", - "colormap_name": "" # e.g. autumn_r -}) -``` - -You can take any of the colormaps listed on `rio-tiler`, and add `_r` to reverse it. - -### Custom Colormaps - -If you'd like to specify your own colormap, you can specify your own using an encoded JSON: - -```python3 -import requests - -response = requests.get( - f"titiler.xyz/cog/preview", - params={ - "url": "", - "bidx": "1", - "colormap": { - "0": "#e5f5f9", - "10": "#99d8c9", - "255": "#2ca25f", - } - } -) -``` - -Titiler supports colormaps that are both discrete (where pixels will be one of the colors that you specify) and linear (where pixel colors will blend between the given colors). - -For more information, please check out [rio-tiler's docs](https://cogeotiff.github.io/rio-tiler/colormap/). - -It is also possible to add a [colormap dependency](../../examples/code/tiler_with_custom_colormap) to automatically apply -a default colormap. - -## Color Formula - -Color formulae are simple commands that apply color corrections to images. This is useful for reducing artefacts like atmospheric haze, dark shadows, or muted colors. - -Titiler supports color formulae as defined in [Mapbox's `rio-color` plugin](https://github.com/mapbox/rio-color). These include the operations ([taken from the `rio-color` docs](https://github.com/mapbox/rio-color#operations)): - -- **Gamma** adjustment adjusts RGB values according to a power law, effectively brightening or darkening the midtones. It can be very effective in satellite imagery for reducing atmospheric haze in the blue and green bands. - -- **Sigmoidal** contrast adjustment can alter the contrast and brightness of an image in a way that matches human's non-linear visual perception. It works well to increase contrast without blowing out the very dark shadows or already-bright parts of the image. - -- **Saturation** can be thought of as the "colorfulness" of a pixel. Highly saturated colors are intense and almost cartoon-like, low saturation is more muted, closer to black and white. You can adjust saturation independently of brightness and hue but the data must be transformed into a different color space. - -In Titiler, color_formulae are applied through the `color_formula` parameter as a string. An example of this option in action: - -```python3 -import requests - -response = requests.get( - f"titiler.xyz/cog/preview", - params={ - "url": "", - "color_formula": "gamma rg 1.3, sigmoidal rgb 22 0.1, saturation 1.5" - } -) -``` - -## Rescaling - -Rescaling is the act of adjusting the minimum and maximum values when rendering an image. In an image with a single band, the rescaled minimum value will be set to black, and the rescaled maximum value will be set to white. This is useful if you want to accentuate features that only appear at a certain pixel value (e.g. you have a DEM, but you want to highlight how the terrain changes between sea level and 100m). - -Titiler supports rescaling on a per-band basis, using the `rescaling` parameter. The input is a list of comma-delimited min-max ranges (e.g. ["0,100", "100,200", "0,1000]). - -```python3 -import requests - -response = requests.get( - f"titiler.xyz/cog/preview", - params={ - "url": "", - "rescaling": ["0,100", "0,1000", "0,10000"] - } -) -``` - -By default, Titiler will rescale the bands using the min/max values of the input datatype. For example, PNG images 8- or 16-bit unsigned pixels, -giving a possible range of 0 to 255 or 0 to 65,536, so Titiler will use these ranges to rescale to the output format. - -For certain datasets (e.g. DEMs) this default behaviour can make the image seem washed out (or even entirely one color), -so if you see this happen look into rescaling your images to something that makes sense for your data. - -It is also possible to add a [rescaling dependency](../../api/titiler/core/dependencies/#rescalingparams) to automatically apply -a default rescale. \ No newline at end of file diff --git a/docs/src/advanced/telemetry.md b/docs/src/advanced/telemetry.md new file mode 100644 index 000000000..a7de597d0 --- /dev/null +++ b/docs/src/advanced/telemetry.md @@ -0,0 +1,87 @@ + +## Observability with OpenTelemetry + +`TiTiler` provides built-in observability through OpenTelemetry, automatically creating traces for all API endpoints. These traces include detailed spans for key internal operations like data access and image processing, enabling fine-grained performance analysis and debugging. + +This instrumentation works seamlessly with other OpenTelemetry libraries, such as FastAPIInstrumentor, to provide a complete, end-to-end view of your application's performance, from incoming request to final response. + +### Installation + +To enable telemetry, you must install titiler.core with the [telemetry] extra. This ensures all necessary OpenTelemetry packages are installed. + +```bash +python -m pip install -U pip + +# From Pypi +python -m pip install titiler.core[telemetry] + +# Or from sources +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core[telemetry] +``` + +### Configuration + +To export traces, you need to configure your application to send them to an observability platform (like Jaeger or Datadog) using an OTLP Exporter. + +The following example demonstrates how to set up a tracer provider that exports data via the OTLP protocol over HTTP. This setup is typically done once when your application starts. + +```python +# In your main application file, e.g., main.py + +import os +from fastapi import FastAPI +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor +from opentelemetry.instrumentation.logging import LoggingInstrumentor +from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION, Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +from titiler.core.factory import TilerFactory + +# --- OpenTelemetry Configuration --- + +# Define a "Resource" for your application. +# This adds metadata to your traces, like the service name and version. +resource = Resource.create( + { + SERVICE_NAME: os.getenv("OTEL_SERVICE_NAME", "titiler"), + SERVICE_VERSION: "0.1", + } +) + +# Create a "TracerProvider" with the defined resource. +# The provider manages the creation of tracers. +provider = TracerProvider(resource=resource) + +# Configure an "Exporter" to send telemetry data. +# The OTLPSpanExporter sends data to an OTLP-compatible endpoint. +# By default, it reads the endpoint from the OTEL_EXPORTER_OTLP_ENDPOINT +# environment variable. The default for HTTP is http://localhost:4318. +exporter = OTLPSpanExporter() + +# Use a "BatchSpanProcessor" to send spans in the background. +# This is the recommended processor for production. +processor = BatchSpanProcessor(exporter) +provider.add_span_processor(processor) + +# Set the configured provider as the global tracer provider. +trace.set_tracer_provider(provider) + +# --- FastAPI Application Setup --- +app = FastAPI(title="My TiTiler App") + +# Instrument the FastAPI application. +# This adds middleware to trace requests, responses, and exceptions, +# complementing TiTiler's internal endpoint tracing. +FastAPIInstrumentor.instrument_app(app) + +# Add trace/span info to logging messages for trace correlation +LoggingInstrumentor().instrument(set_logging_format=True) + +# Add your TiTiler endpoints with the enable_telemetry flag set to True +cog = TilerFactory(enable_telemetry=True) +app.include_router(cog.router) +``` diff --git a/docs/src/advanced/tiler_factories.md b/docs/src/advanced/tiler_factories.md deleted file mode 100644 index c97dadd15..000000000 --- a/docs/src/advanced/tiler_factories.md +++ /dev/null @@ -1,147 +0,0 @@ - -Tiler factories are helper functions that let users create a FastAPI router (`fastapi.APIRouter`) with a minimal set of endpoints. - -### `titiler.core.factory.TilerFactory` - -```python -from fastapi import FastAPI - -from titiler.core.factory import TilerFactory - -app = FastAPI(description="A lightweight Cloud Optimized GeoTIFF tile server") -cog = TilerFactory() -app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) -``` - -| Method | URL | Output | Description -| ------ | --------------------------------------------------------------- |-------------------------------------------- |-------------- -| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return dataset's bounds -| `GET` | `/info` | JSON ([Info][info_model]) | return dataset's basic info -| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return dataset's basic info as a GeoJSON feature -| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return dataset's statistics -| `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return dataset's statistics for a GeoJSON -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset -| `GET` | `[/{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities -| `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel values from a dataset -| `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset (**Optional**) -| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset (**Optional**) -| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature (**Optional**) -| `GET` | `/map` | HTML | return a simple map viewer -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer - -### `titiler.core.factory.MultiBaseTilerFactory` - -Custom `TilerFactory` to be used with `rio_tiler.io.MultiBaseReader` type readers. - -```python -from fastapi import FastAPI -from rio_tiler.io import STACReader # rio_tiler.io.STACReader is a MultiBaseReader - -from titiler.core.factory import MultiBaseTilerFactory - -app = FastAPI(description="A lightweight STAC tile server") -cog = MultiBaseTilerFactory(reader=STACReader) -app.include_router(cog.router, tags=["STAC"]) -``` - -| Method | URL | Output | Description -| ------ | --------------------------------------------------------------- |------------------------------------------------- |-------------- -| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return dataset's bounds -| `GET` | `/assets` | JSON | return the list of available assets -| `GET` | `/info` | JSON ([Info][multiinfo_model]) | return assets basic info -| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][multiinfo_geojson_model]) | return assets basic info as a GeoJSON feature -| `GET` | `/asset_statistics` | JSON ([Statistics][multistats_model]) | return per asset statistics -| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return assets statistics (merged) -| `POST` | `/statistics` | GeoJSON ([Statistics][multistats_geojson_model]) | return assets statistics for a GeoJSON (merged) -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets -| `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities -| `GET` | `/point/{lon},{lat}` | JSON ([Point][multipoint_model]) | return pixel values from assets -| `GET` | `/preview[.{format}]` | image/bin | create a preview image from assets (**Optional**) -| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets (**Optional**) -| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature intersecting assets (**Optional**) -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer - -### `titiler.core.factory.MultiBandTilerFactory` - -Custom `TilerFactory` to be used with `rio_tiler.io.MultiBandReader` type readers. - -```python -from fastapi import FastAPI, Query -from rio_tiler_pds.landsat.aws import LandsatC2Reader # rio_tiler_pds.landsat.aws.LandsatC2Reader is a MultiBandReader - -from titiler.core.factory import MultiBandTilerFactory - - -def SceneIDParams(sceneid: str = Query(..., description="Landsat Scene ID")) -> str: - """Use `sceneid` in query instead of url.""" - return sceneid - - -app = FastAPI(description="A lightweight Landsat Collection 2 tile server") -cog = MultiBandTilerFactory(reader=LandsatC2Reader, path_dependency=SceneIDParams) -app.include_router(cog.router, tags=["Landsat"]) -``` - -| Method | URL | Output | Description -| ------ | --------------------------------------------------------------- |--------------------------------------------- |-------------- -| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return dataset's bounds -| `GET` | `/bands` | JSON | return the list of available bands -| `GET` | `/info` | JSON ([Info][info_model]) | return basic info for a dataset -| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return basic info for a dataset as a GeoJSON feature -| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return info and statistics for a dataset -| `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return info and statistics for a dataset -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset -| `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities -| `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel value from a dataset -| `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset -| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset -| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer - -### `titiler.mosaic.factory.MosaicTilerFactory` - - -| Method | URL | Output | Description -| ------ | --------------------------------------------------------------- |--------------------------------------------------- |-------------- -| `GET` | `/` | JSON [MosaicJSON][mosaic_model] | return a MosaicJSON document -| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return mosaic's bounds -| `GET` | `/info` | JSON ([Info][mosaic_info_model]) | return mosaic's basic info -| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][mosaic_geojson_info_model]) | return mosaic's basic info as a GeoJSON feature -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a MosaicJSON -| `GET` | `[/{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities -| `GET` | `/point/{lon},{lat}` | JSON ([Point][mosaic_point]) | return pixel value from a MosaicJSON dataset -| `GET` | `/{z}/{x}/{y}/assets` | JSON | return list of assets intersecting a XYZ tile -| `GET` | `/{lon},{lat}/assets` | JSON | return list of assets intersecting a point -| `GET` | `/{minx},{miny},{maxx},{maxy}/assets` | JSON | return list of assets intersecting a bounding box -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer - - -!!! Important - - **Factories** are built around [`rio_tiler.io.BaseReader`](https://cogeotiff.github.io/rio-tiler/advanced/custom_readers/), which defines basic methods to access datasets (e.g COG or STAC). The default reader is `COGReader` for `TilerFactory` and `MosaicBackend` for `MosaicTilerFactory`. - - Factories classes use [dependencies injection](dependencies.md) to define most of the endpoint options. - - -[bounds_model]: https://github.com/cogeotiff/rio-tiler/blob/9aaa88000399ee8d36e71d176f67b6ea3ec53f2d/rio_tiler/models.py#L43-L46 -[info_model]: https://github.com/cogeotiff/rio-tiler/blob/9aaa88000399ee8d36e71d176f67b6ea3ec53f2d/rio_tiler/models.py#L56-L72 -[info_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L30 -[tilejson_model]: https://github.com/developmentseed/titiler/blob/2335048a407f17127099cbbc6c14e1328852d619/src/titiler/core/titiler/core/models/mapbox.py#L16-L38 -[point_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L11-L20 -[stats_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L32 -[stats_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L46-L49 - -[multiinfo_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L52 -[multiinfo_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L53 -[multipoint_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L23-L27 -[multistats_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L55 -[multistats_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L56-L59 - -[mosaic_info_model]: https://github.com/developmentseed/cogeo-mosaic/blob/1dc3c873472c8cf7634ad893b9cdc40105ca3874/cogeo_mosaic/models.py#L9-L17 -[mosaic_geojson_info_model]: https://github.com/developmentseed/titiler/blob/2bd1b159a9cf0932ad14e9eabf1e4e66498adbdc/src/titiler/mosaic/titiler/mosaic/factory.py#L130 -[mosaic_model]: https://github.com/developmentseed/cogeo-mosaic/blob/1dc3c873472c8cf7634ad893b9cdc40105ca3874/cogeo_mosaic/mosaic.py#L55-L72 -[mosaic_point]: https://github.com/developmentseed/titiler/blob/2bd1b159a9cf0932ad14e9eabf1e4e66498adbdc/src/titiler/mosaic/titiler/mosaic/models/responses.py#L8-L17 diff --git a/docs/src/api/titiler/core/dependencies.md b/docs/src/api/titiler/core/dependencies.md index 51ae7afb2..3e4e260f6 100644 --- a/docs/src/api/titiler/core/dependencies.md +++ b/docs/src/api/titiler/core/dependencies.md @@ -1,1471 +1,5 @@ -# Module titiler.core.dependencies -Common dependency. +::: titiler.core.dependencies + options: + show_source: true -None - -## Functions - - -### ColorMapParams - -```python3 -def ColorMapParams( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict, Sequence, NoneType] -``` - - -Colormap Dependency. - - -### DatasetPathParams - -```python3 -def DatasetPathParams( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -### TMSParams - -```python3 -def TMSParams( - TileMatrixSetId: titiler.core.dependencies.TileMatrixSetName = Query(TileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -### WebMercatorTMSParams - -```python3 -def WebMercatorTMSParams( - TileMatrixSetId: titiler.core.dependencies.WebMercatorTileMatrixSetName = Query(WebMercatorTileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - -## Classes - -### AssetsBidxExprParams - -```python3 -class AssetsBidxExprParams( - assets: Union[List[str], NoneType] = Query(None), - expression: Union[str, NoneType] = Query(None), - asset_indexes: Union[Sequence[str], NoneType] = Query(None), - asset_expression: Union[Sequence[str], NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -asset_expression -``` - -```python3 -asset_indexes -``` - -```python3 -assets -``` - -```python3 -expression -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### AssetsBidxParams - -```python3 -class AssetsBidxParams( - assets: List[str] = Query(None), - asset_indexes: Union[Sequence[str], NoneType] = Query(None), - asset_expression: Union[Sequence[str], NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.AssetsParams -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -asset_expression -``` - -```python3 -asset_indexes -``` - -```python3 -assets -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### AssetsParams - -```python3 -class AssetsParams( - assets: List[str] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Descendants - -* titiler.core.dependencies.AssetsBidxParams - -#### Class variables - -```python3 -assets -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### BandsExprParams - -```python3 -class BandsExprParams( - bands: List[str] = Query(None), - expression: Union[str, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.ExpressionParams -* titiler.core.dependencies.BandsParams -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -bands -``` - -```python3 -expression -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### BandsExprParamsOptional - -```python3 -class BandsExprParamsOptional( - bands: List[str] = Query(None), - expression: Union[str, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.ExpressionParams -* titiler.core.dependencies.BandsParams -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -bands -``` - -```python3 -expression -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### BandsParams - -```python3 -class BandsParams( - bands: List[str] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Descendants - -* titiler.core.dependencies.BandsExprParamsOptional -* titiler.core.dependencies.BandsExprParams - -#### Class variables - -```python3 -bands -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### BidxExprParams - -```python3 -class BidxExprParams( - indexes: Union[List[int], NoneType] = Query(None), - expression: Union[str, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.ExpressionParams -* titiler.core.dependencies.BidxParams -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -expression -``` - -```python3 -indexes -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### BidxParams - -```python3 -class BidxParams( - indexes: Union[List[int], NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Descendants - -* titiler.core.dependencies.BidxExprParams - -#### Class variables - -```python3 -indexes -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### ColorMapName - -```python3 -class ColorMapName( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* enum.Enum - -#### Class variables - -```python3 -accent -``` - -```python3 -accent_r -``` - -```python3 -afmhot -``` - -```python3 -afmhot_r -``` - -```python3 -autumn -``` - -```python3 -autumn_r -``` - -```python3 -binary -``` - -```python3 -binary_r -``` - -```python3 -blues -``` - -```python3 -blues_r -``` - -```python3 -bone -``` - -```python3 -bone_r -``` - -```python3 -brbg -``` - -```python3 -brbg_r -``` - -```python3 -brg -``` - -```python3 -brg_r -``` - -```python3 -bugn -``` - -```python3 -bugn_r -``` - -```python3 -bupu -``` - -```python3 -bupu_r -``` - -```python3 -bwr -``` - -```python3 -bwr_r -``` - -```python3 -cfastie -``` - -```python3 -cividis -``` - -```python3 -cividis_r -``` - -```python3 -cmrmap -``` - -```python3 -cmrmap_r -``` - -```python3 -cool -``` - -```python3 -cool_r -``` - -```python3 -coolwarm -``` - -```python3 -coolwarm_r -``` - -```python3 -copper -``` - -```python3 -copper_r -``` - -```python3 -cubehelix -``` - -```python3 -cubehelix_r -``` - -```python3 -dark2 -``` - -```python3 -dark2_r -``` - -```python3 -flag -``` - -```python3 -flag_r -``` - -```python3 -gist_earth -``` - -```python3 -gist_earth_r -``` - -```python3 -gist_gray -``` - -```python3 -gist_gray_r -``` - -```python3 -gist_heat -``` - -```python3 -gist_heat_r -``` - -```python3 -gist_ncar -``` - -```python3 -gist_ncar_r -``` - -```python3 -gist_rainbow -``` - -```python3 -gist_rainbow_r -``` - -```python3 -gist_stern -``` - -```python3 -gist_stern_r -``` - -```python3 -gist_yarg -``` - -```python3 -gist_yarg_r -``` - -```python3 -gnbu -``` - -```python3 -gnbu_r -``` - -```python3 -gnuplot -``` - -```python3 -gnuplot2 -``` - -```python3 -gnuplot2_r -``` - -```python3 -gnuplot_r -``` - -```python3 -gray -``` - -```python3 -gray_r -``` - -```python3 -greens -``` - -```python3 -greens_r -``` - -```python3 -greys -``` - -```python3 -greys_r -``` - -```python3 -hot -``` - -```python3 -hot_r -``` - -```python3 -hsv -``` - -```python3 -hsv_r -``` - -```python3 -inferno -``` - -```python3 -inferno_r -``` - -```python3 -jet -``` - -```python3 -jet_r -``` - -```python3 -magma -``` - -```python3 -magma_r -``` - -```python3 -name -``` - -```python3 -nipy_spectral -``` - -```python3 -nipy_spectral_r -``` - -```python3 -ocean -``` - -```python3 -ocean_r -``` - -```python3 -oranges -``` - -```python3 -oranges_r -``` - -```python3 -orrd -``` - -```python3 -orrd_r -``` - -```python3 -paired -``` - -```python3 -paired_r -``` - -```python3 -pastel1 -``` - -```python3 -pastel1_r -``` - -```python3 -pastel2 -``` - -```python3 -pastel2_r -``` - -```python3 -pink -``` - -```python3 -pink_r -``` - -```python3 -piyg -``` - -```python3 -piyg_r -``` - -```python3 -plasma -``` - -```python3 -plasma_r -``` - -```python3 -prgn -``` - -```python3 -prgn_r -``` - -```python3 -prism -``` - -```python3 -prism_r -``` - -```python3 -pubu -``` - -```python3 -pubu_r -``` - -```python3 -pubugn -``` - -```python3 -pubugn_r -``` - -```python3 -puor -``` - -```python3 -puor_r -``` - -```python3 -purd -``` - -```python3 -purd_r -``` - -```python3 -purples -``` - -```python3 -purples_r -``` - -```python3 -rainbow -``` - -```python3 -rainbow_r -``` - -```python3 -rdbu -``` - -```python3 -rdbu_r -``` - -```python3 -rdgy -``` - -```python3 -rdgy_r -``` - -```python3 -rdpu -``` - -```python3 -rdpu_r -``` - -```python3 -rdylbu -``` - -```python3 -rdylbu_r -``` - -```python3 -rdylgn -``` - -```python3 -rdylgn_r -``` - -```python3 -reds -``` - -```python3 -reds_r -``` - -```python3 -rplumbo -``` - -```python3 -schwarzwald -``` - -```python3 -seismic -``` - -```python3 -seismic_r -``` - -```python3 -set1 -``` - -```python3 -set1_r -``` - -```python3 -set2 -``` - -```python3 -set2_r -``` - -```python3 -set3 -``` - -```python3 -set3_r -``` - -```python3 -spectral -``` - -```python3 -spectral_r -``` - -```python3 -spring -``` - -```python3 -spring_r -``` - -```python3 -summer -``` - -```python3 -summer_r -``` - -```python3 -tab10 -``` - -```python3 -tab10_r -``` - -```python3 -tab20 -``` - -```python3 -tab20_r -``` - -```python3 -tab20b -``` - -```python3 -tab20b_r -``` - -```python3 -tab20c -``` - -```python3 -tab20c_r -``` - -```python3 -terrain -``` - -```python3 -terrain_r -``` - -```python3 -twilight -``` - -```python3 -twilight_r -``` - -```python3 -twilight_shifted -``` - -```python3 -twilight_shifted_r -``` - -```python3 -value -``` - -```python3 -viridis -``` - -```python3 -viridis_r -``` - -```python3 -winter -``` - -```python3 -winter_r -``` - -```python3 -wistia -``` - -```python3 -wistia_r -``` - -```python3 -ylgn -``` - -```python3 -ylgn_r -``` - -```python3 -ylgnbu -``` - -```python3 -ylgnbu_r -``` - -```python3 -ylorbr -``` - -```python3 -ylorbr_r -``` - -```python3 -ylorrd -``` - -```python3 -ylorrd_r -``` - -### DatasetParams - -```python3 -class DatasetParams( - nodata: Union[str, int, float, NoneType] = Query(None), - unscale: Union[bool, NoneType] = Query(False), - resampling_method: titiler.core.dependencies.ResamplingName = Query(ResamplingName.nearest) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -nodata -``` - -```python3 -resampling_method -``` - -```python3 -unscale -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### DefaultDependency - -```python3 -class DefaultDependency( - -) -``` - -#### Descendants - -* titiler.core.dependencies.BidxParams -* titiler.core.dependencies.ExpressionParams -* titiler.core.dependencies.AssetsParams -* titiler.core.dependencies.AssetsBidxExprParams -* titiler.core.dependencies.BandsParams -* titiler.core.dependencies.ImageParams -* titiler.core.dependencies.DatasetParams -* titiler.core.dependencies.ImageRenderingParams -* titiler.core.dependencies.PostProcessParams - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### ExpressionParams - -```python3 -class ExpressionParams( - expression: Union[str, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Descendants - -* titiler.core.dependencies.BidxExprParams -* titiler.core.dependencies.BandsExprParamsOptional -* titiler.core.dependencies.BandsExprParams - -#### Class variables - -```python3 -expression -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### ImageParams - -```python3 -class ImageParams( - max_size: Union[int, NoneType] = Query(1024), - height: Union[int, NoneType] = Query(None), - width: Union[int, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -height -``` - -```python3 -max_size -``` - -```python3 -width -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### ImageRenderingParams - -```python3 -class ImageRenderingParams( - add_mask: bool = Query(True) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -add_mask -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### PostProcessParams - -```python3 -class PostProcessParams( - in_range: Union[List[str], NoneType] = Query(None), - color_formula: Union[str, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -color_formula -``` - -```python3 -in_range -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### ResamplingName - -```python3 -class ResamplingName( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* enum.Enum - -#### Class variables - -```python3 -average -``` - -```python3 -bilinear -``` - -```python3 -cubic -``` - -```python3 -cubic_spline -``` - -```python3 -gauss -``` - -```python3 -lanczos -``` - -```python3 -max -``` - -```python3 -med -``` - -```python3 -min -``` - -```python3 -mode -``` - -```python3 -name -``` - -```python3 -nearest -``` - -```python3 -q1 -``` - -```python3 -q3 -``` - -```python3 -rms -``` - -```python3 -sum -``` - -```python3 -value -``` - -### TileMatrixSetName - -```python3 -class TileMatrixSetName( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* enum.Enum - -#### Class variables - -```python3 -CanadianNAD83_LCC -``` - -```python3 -EuropeanETRS89_LAEAQuad -``` - -```python3 -LINZAntarticaMapTilegrid -``` - -```python3 -NZTM2000 -``` - -```python3 -NZTM2000Quad -``` - -```python3 -UPSAntarcticWGS84Quad -``` - -```python3 -UPSArcticWGS84Quad -``` - -```python3 -UTM31WGS84Quad -``` - -```python3 -WebMercatorQuad -``` - -```python3 -WorldCRS84Quad -``` - -```python3 -WorldMercatorWGS84Quad -``` - -```python3 -name -``` - -```python3 -value -``` - -### WebMercatorTileMatrixSetName - -```python3 -class WebMercatorTileMatrixSetName( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* enum.Enum - -#### Class variables - -```python3 -WebMercatorQuad -``` - -```python3 -name -``` - -```python3 -value -``` \ No newline at end of file diff --git a/docs/src/api/titiler/core/errors.md b/docs/src/api/titiler/core/errors.md index 6b897df9c..16fe3296f 100644 --- a/docs/src/api/titiler/core/errors.md +++ b/docs/src/api/titiler/core/errors.md @@ -1,162 +1,2 @@ -# Module titiler.core.errors -Titiler error classes. - -None - -## Variables - -```python3 -DEFAULT_STATUS_CODES -``` - -```python3 -logger -``` - -## Functions - - -### add_exception_handlers - -```python3 -def add_exception_handlers( - app: fastapi.applications.FastAPI, - status_codes: Dict[Type[Exception], int] -) -> None -``` - - -Add exception handlers to the FastAPI app. - - -### exception_handler_factory - -```python3 -def exception_handler_factory( - status_code: int -) -> Callable -``` - - -Create a FastAPI exception handler from a status code. - -## Classes - -### BadRequestError - -```python3 -class BadRequestError( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* titiler.core.errors.TilerError -* builtins.Exception -* builtins.BaseException - -#### Class variables - -```python3 -args -``` - -#### Methods - - -#### with_traceback - -```python3 -def with_traceback( - ... -) -``` - - -Exception.with_traceback(tb) -- - -set self.__traceback__ to tb and return self. - -### TileNotFoundError - -```python3 -class TileNotFoundError( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* titiler.core.errors.TilerError -* builtins.Exception -* builtins.BaseException - -#### Class variables - -```python3 -args -``` - -#### Methods - - -#### with_traceback - -```python3 -def with_traceback( - ... -) -``` - - -Exception.with_traceback(tb) -- - -set self.__traceback__ to tb and return self. - -### TilerError - -```python3 -class TilerError( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.Exception -* builtins.BaseException - -#### Descendants - -* titiler.core.errors.TileNotFoundError -* titiler.core.errors.BadRequestError - -#### Class variables - -```python3 -args -``` - -#### Methods - - -#### with_traceback - -```python3 -def with_traceback( - ... -) -``` - - -Exception.with_traceback(tb) -- - -set self.__traceback__ to tb and return self. \ No newline at end of file +::: titiler.core.errors diff --git a/docs/src/api/titiler/core/factory.md b/docs/src/api/titiler/core/factory.md index 5f6737af5..80ff1f59d 100644 --- a/docs/src/api/titiler/core/factory.md +++ b/docs/src/api/titiler/core/factory.md @@ -1,1000 +1,2 @@ -# Module titiler.core.factory -TiTiler Router factories. - -None - -## Variables - -```python3 -img_endpoint_params -``` - -```python3 -templates -``` - -## Classes - -### BaseTilerFactory - -```python3 -class BaseTilerFactory( - reader: Type[rio_tiler.io.base.BaseReader], - reader_options: Dict = , - router: fastapi.routing.APIRouter = , - path_dependency: Callable[..., str] = , - dataset_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - layer_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - render_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - colormap_dependency: Callable[..., Union[Dict, NoneType]] = , - process_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - tms_dependency: Callable[..., morecantile.models.TileMatrixSet] = , - additional_dependency: Callable[..., Dict] = at 0x163f42310>, - router_prefix: str = '', - gdal_config: Dict = , - optional_headers: List[titiler.core.resources.enums.OptionalHeader] = -) -``` - -#### Descendants - -* titiler.core.factory.TilerFactory - -#### Class variables - -```python3 -dataset_dependency -``` - -```python3 -layer_dependency -``` - -```python3 -process_dependency -``` - -```python3 -render_dependency -``` - -```python3 -router_prefix -``` - -#### Methods - - -#### additional_dependency - -```python3 -def additional_dependency( - -) -``` - - - - -#### colormap_dependency - -```python3 -def colormap_dependency( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict, Sequence, NoneType] -``` - - -Colormap Dependency. - - -#### path_dependency - -```python3 -def path_dependency( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -Register Tiler Routes. - - -#### tms_dependency - -```python3 -def tms_dependency( - TileMatrixSetId: titiler.core.dependencies.WebMercatorTileMatrixSetName = Query(WebMercatorTileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - -### MultiBandTilerFactory - -```python3 -class MultiBandTilerFactory( - reader: Type[rio_tiler.io.base.MultiBandReader] = , - reader_options: Dict = , - router: fastapi.routing.APIRouter = , - path_dependency: Callable[..., str] = , - dataset_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - layer_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - render_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - colormap_dependency: Callable[..., Union[Dict, NoneType]] = , - process_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - tms_dependency: Callable[..., morecantile.models.TileMatrixSet] = , - additional_dependency: Callable[..., Dict] = at 0x163f42310>, - router_prefix: str = '', - gdal_config: Dict = , - optional_headers: List[titiler.core.resources.enums.OptionalHeader] = , - img_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - add_preview: bool = True, - add_part: bool = True, - add_statistics: bool = True, - bands_dependency: Type[titiler.core.dependencies.DefaultDependency] = -) -``` - -#### Ancestors (in MRO) - -* titiler.core.factory.TilerFactory -* titiler.core.factory.BaseTilerFactory - -#### Class variables - -```python3 -add_part -``` - -```python3 -add_preview -``` - -```python3 -add_statistics -``` - -```python3 -bands_dependency -``` - -```python3 -dataset_dependency -``` - -```python3 -img_dependency -``` - -```python3 -layer_dependency -``` - -```python3 -process_dependency -``` - -```python3 -reader -``` - -```python3 -render_dependency -``` - -```python3 -router_prefix -``` - -#### Methods - - -#### additional_dependency - -```python3 -def additional_dependency( - -) -``` - - - - -#### bounds - -```python3 -def bounds( - self -) -``` - - -Register /bounds endpoint. - - -#### colormap_dependency - -```python3 -def colormap_dependency( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict, Sequence, NoneType] -``` - - -Colormap Dependency. - - -#### info - -```python3 -def info( - self -) -``` - - -Register /info endpoint. - - -#### part - -```python3 -def part( - self -) -``` - - -Register /crop endpoint. - - -#### path_dependency - -```python3 -def path_dependency( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -#### point - -```python3 -def point( - self -) -``` - - -Register /point endpoints. - - -#### preview - -```python3 -def preview( - self -) -``` - - -Register /preview endpoint. - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -This Method register routes to the router. - -Because we wrap the endpoints in a class we cannot define the routes as -methods (because of the self argument). The HACK is to define routes inside -the class method and register them after the class initialisation. - - -#### statistics - -```python3 -def statistics( - self -) -``` - - -add statistics endpoints. - - -#### tile - -```python3 -def tile( - self -) -``` - - -Register /tiles endpoint. - - -#### tilejson - -```python3 -def tilejson( - self -) -``` - - -Register /tilejson.json endpoint. - - -#### tms_dependency - -```python3 -def tms_dependency( - TileMatrixSetId: titiler.core.dependencies.TileMatrixSetName = Query(TileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - - -#### wmts - -```python3 -def wmts( - self -) -``` - - -Register /wmts endpoint. - -### MultiBaseTilerFactory - -```python3 -class MultiBaseTilerFactory( - reader: Type[rio_tiler.io.base.MultiBaseReader] = , - reader_options: Dict = , - router: fastapi.routing.APIRouter = , - path_dependency: Callable[..., str] = , - dataset_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - layer_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - render_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - colormap_dependency: Callable[..., Union[Dict, NoneType]] = , - process_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - tms_dependency: Callable[..., morecantile.models.TileMatrixSet] = , - additional_dependency: Callable[..., Dict] = at 0x163f42310>, - router_prefix: str = '', - gdal_config: Dict = , - optional_headers: List[titiler.core.resources.enums.OptionalHeader] = , - img_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - add_preview: bool = True, - add_part: bool = True, - add_statistics: bool = True, - assets_dependency: Type[titiler.core.dependencies.DefaultDependency] = -) -``` - -#### Ancestors (in MRO) - -* titiler.core.factory.TilerFactory -* titiler.core.factory.BaseTilerFactory - -#### Class variables - -```python3 -add_part -``` - -```python3 -add_preview -``` - -```python3 -add_statistics -``` - -```python3 -assets_dependency -``` - -```python3 -dataset_dependency -``` - -```python3 -img_dependency -``` - -```python3 -layer_dependency -``` - -```python3 -process_dependency -``` - -```python3 -reader -``` - -```python3 -render_dependency -``` - -```python3 -router_prefix -``` - -#### Methods - - -#### additional_dependency - -```python3 -def additional_dependency( - -) -``` - - - - -#### bounds - -```python3 -def bounds( - self -) -``` - - -Register /bounds endpoint. - - -#### colormap_dependency - -```python3 -def colormap_dependency( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict, Sequence, NoneType] -``` - - -Colormap Dependency. - - -#### info - -```python3 -def info( - self -) -``` - - -Register /info endpoint. - - -#### part - -```python3 -def part( - self -) -``` - - -Register /crop endpoint. - - -#### path_dependency - -```python3 -def path_dependency( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -#### point - -```python3 -def point( - self -) -``` - - -Register /point endpoints. - - -#### preview - -```python3 -def preview( - self -) -``` - - -Register /preview endpoint. - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -This Method register routes to the router. - -Because we wrap the endpoints in a class we cannot define the routes as -methods (because of the self argument). The HACK is to define routes inside -the class method and register them after the class initialisation. - - -#### statistics - -```python3 -def statistics( - self -) -``` - - -Register /statistics endpoint. - - -#### tile - -```python3 -def tile( - self -) -``` - - -Register /tiles endpoint. - - -#### tilejson - -```python3 -def tilejson( - self -) -``` - - -Register /tilejson.json endpoint. - - -#### tms_dependency - -```python3 -def tms_dependency( - TileMatrixSetId: titiler.core.dependencies.TileMatrixSetName = Query(TileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - - -#### wmts - -```python3 -def wmts( - self -) -``` - - -Register /wmts endpoint. - -### TMSFactory - -```python3 -class TMSFactory( - supported_tms: Type[titiler.core.dependencies.TileMatrixSetName] = , - tms_dependency: Callable[..., morecantile.models.TileMatrixSet] = , - router: fastapi.routing.APIRouter = , - router_prefix: str = '' -) -``` - -#### Class variables - -```python3 -router_prefix -``` - -```python3 -supported_tms -``` - -#### Methods - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -Register TMS endpoint routes. - - -#### tms_dependency - -```python3 -def tms_dependency( - TileMatrixSetId: titiler.core.dependencies.TileMatrixSetName = Query(TileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - -### TilerFactory - -```python3 -class TilerFactory( - reader: Type[rio_tiler.io.base.BaseReader] = , - reader_options: Dict = , - router: fastapi.routing.APIRouter = , - path_dependency: Callable[..., str] = , - dataset_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - layer_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - render_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - colormap_dependency: Callable[..., Union[Dict, NoneType]] = , - process_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - tms_dependency: Callable[..., morecantile.models.TileMatrixSet] = , - additional_dependency: Callable[..., Dict] = at 0x163f42310>, - router_prefix: str = '', - gdal_config: Dict = , - optional_headers: List[titiler.core.resources.enums.OptionalHeader] = , - img_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - add_preview: bool = True, - add_part: bool = True, - add_statistics: bool = True -) -``` - -#### Ancestors (in MRO) - -* titiler.core.factory.BaseTilerFactory - -#### Descendants - -* titiler.core.factory.MultiBaseTilerFactory -* titiler.core.factory.MultiBandTilerFactory - -#### Class variables - -```python3 -add_part -``` - -```python3 -add_preview -``` - -```python3 -add_statistics -``` - -```python3 -dataset_dependency -``` - -```python3 -img_dependency -``` - -```python3 -layer_dependency -``` - -```python3 -process_dependency -``` - -```python3 -reader -``` - -```python3 -render_dependency -``` - -```python3 -router_prefix -``` - -#### Methods - - -#### additional_dependency - -```python3 -def additional_dependency( - -) -``` - - - - -#### bounds - -```python3 -def bounds( - self -) -``` - - -Register /bounds endpoint. - - -#### colormap_dependency - -```python3 -def colormap_dependency( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict, Sequence, NoneType] -``` - - -Colormap Dependency. - - -#### info - -```python3 -def info( - self -) -``` - - -Register /info endpoint. - - -#### part - -```python3 -def part( - self -) -``` - - -Register /crop endpoint. - - -#### path_dependency - -```python3 -def path_dependency( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -#### point - -```python3 -def point( - self -) -``` - - -Register /point endpoints. - - -#### preview - -```python3 -def preview( - self -) -``` - - -Register /preview endpoint. - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -This Method register routes to the router. - -Because we wrap the endpoints in a class we cannot define the routes as -methods (because of the self argument). The HACK is to define routes inside -the class method and register them after the class initialisation. - - -#### statistics - -```python3 -def statistics( - self -) -``` - - -add statistics endpoints. - - -#### tile - -```python3 -def tile( - self -) -``` - - -Register /tiles endpoint. - - -#### tilejson - -```python3 -def tilejson( - self -) -``` - - -Register /tilejson.json endpoint. - - -#### tms_dependency - -```python3 -def tms_dependency( - TileMatrixSetId: titiler.core.dependencies.TileMatrixSetName = Query(TileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - - -#### wmts - -```python3 -def wmts( - self -) -``` - - -Register /wmts endpoint. \ No newline at end of file +::: titiler.core.factory diff --git a/docs/src/api/titiler/core/middleware.md b/docs/src/api/titiler/core/middleware.md new file mode 100644 index 000000000..9c06108b0 --- /dev/null +++ b/docs/src/api/titiler/core/middleware.md @@ -0,0 +1,2 @@ + +::: titiler.core.middleware diff --git a/docs/src/api/titiler/core/models/OGC.md b/docs/src/api/titiler/core/models/OGC.md new file mode 100644 index 000000000..a0fcf3bdb --- /dev/null +++ b/docs/src/api/titiler/core/models/OGC.md @@ -0,0 +1,2 @@ + +::: titiler.core.models.OGC diff --git a/docs/src/api/titiler/core/models/mapbox.md b/docs/src/api/titiler/core/models/mapbox.md new file mode 100644 index 000000000..67b9ac6e3 --- /dev/null +++ b/docs/src/api/titiler/core/models/mapbox.md @@ -0,0 +1,2 @@ + +::: titiler.core.models.mapbox diff --git a/docs/src/api/titiler/core/models/responses.md b/docs/src/api/titiler/core/models/responses.md new file mode 100644 index 000000000..fc34ecb70 --- /dev/null +++ b/docs/src/api/titiler/core/models/responses.md @@ -0,0 +1,2 @@ + +::: titiler.core.models.responses diff --git a/docs/src/api/titiler/core/resources/enums.md b/docs/src/api/titiler/core/resources/enums.md index eb427e10f..1faab5f5d 100644 --- a/docs/src/api/titiler/core/resources/enums.md +++ b/docs/src/api/titiler/core/resources/enums.md @@ -1,239 +1,2 @@ -# Module titiler.core.resources.enums -Titiler.core Enums. - -None - -## Classes - -### ImageDriver - -```python3 -class ImageDriver( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.str -* enum.Enum - -#### Class variables - -```python3 -jp2 -``` - -```python3 -jpeg -``` - -```python3 -jpg -``` - -```python3 -name -``` - -```python3 -npy -``` - -```python3 -png -``` - -```python3 -pngraw -``` - -```python3 -tif -``` - -```python3 -value -``` - -```python3 -webp -``` - -### ImageType - -```python3 -class ImageType( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.str -* enum.Enum - -#### Class variables - -```python3 -jp2 -``` - -```python3 -jpeg -``` - -```python3 -jpg -``` - -```python3 -name -``` - -```python3 -npy -``` - -```python3 -png -``` - -```python3 -pngraw -``` - -```python3 -tif -``` - -```python3 -value -``` - -```python3 -webp -``` - -### MediaType - -```python3 -class MediaType( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.str -* enum.Enum - -#### Class variables - -```python3 -geojson -``` - -```python3 -html -``` - -```python3 -jp2 -``` - -```python3 -jpeg -``` - -```python3 -jpg -``` - -```python3 -json -``` - -```python3 -mvt -``` - -```python3 -name -``` - -```python3 -npy -``` - -```python3 -pbf -``` - -```python3 -png -``` - -```python3 -pngraw -``` - -```python3 -text -``` - -```python3 -tif -``` - -```python3 -value -``` - -```python3 -webp -``` - -```python3 -xml -``` - -### OptionalHeader - -```python3 -class OptionalHeader( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.str -* enum.Enum - -#### Class variables - -```python3 -name -``` - -```python3 -server_timing -``` - -```python3 -value -``` - -```python3 -x_assets -``` \ No newline at end of file +::: titiler.core.resources.enums diff --git a/docs/src/api/titiler/core/resources/responses.md b/docs/src/api/titiler/core/resources/responses.md new file mode 100644 index 000000000..e3ddaa410 --- /dev/null +++ b/docs/src/api/titiler/core/resources/responses.md @@ -0,0 +1,2 @@ + +::: titiler.core.resources.responses diff --git a/docs/src/api/titiler/core/routing.md b/docs/src/api/titiler/core/routing.md index 839b09283..89e89a435 100644 --- a/docs/src/api/titiler/core/routing.md +++ b/docs/src/api/titiler/core/routing.md @@ -1,26 +1,2 @@ -# Module titiler.core.routing -Custom routing classes. - -None - -## Functions - - -### apiroute_factory - -```python3 -def apiroute_factory( - env: Union[Dict, NoneType] = None -) -> Type[fastapi.routing.APIRoute] -``` - - -Create Custom API Route class with custom Env. - -Because we cannot create middleware for specific router we need to create -a custom APIRoute which add the `rasterio.Env(` block before the endpoint is -actually called. This way we set the env outside the threads and we make sure -that event multithreaded Reader will get the environment set. - -Note: This has been tested in python 3.6 and 3.7 only. \ No newline at end of file +::: titiler.core.routing diff --git a/docs/src/api/titiler/extensions/cogeo.md b/docs/src/api/titiler/extensions/cogeo.md new file mode 100644 index 000000000..7c24ed38a --- /dev/null +++ b/docs/src/api/titiler/extensions/cogeo.md @@ -0,0 +1 @@ +::: titiler.extensions.cogeo diff --git a/docs/src/api/titiler/extensions/stac.md b/docs/src/api/titiler/extensions/stac.md new file mode 100644 index 000000000..2f930fc6e --- /dev/null +++ b/docs/src/api/titiler/extensions/stac.md @@ -0,0 +1 @@ +::: titiler.extensions.stac diff --git a/docs/src/api/titiler/extensions/viewer.md b/docs/src/api/titiler/extensions/viewer.md new file mode 100644 index 000000000..e303253e3 --- /dev/null +++ b/docs/src/api/titiler/extensions/viewer.md @@ -0,0 +1 @@ +::: titiler.extensions.viewer diff --git a/docs/src/api/titiler/extensions/wms.md b/docs/src/api/titiler/extensions/wms.md new file mode 100644 index 000000000..301e0fbd8 --- /dev/null +++ b/docs/src/api/titiler/extensions/wms.md @@ -0,0 +1 @@ +::: titiler.extensions.wms diff --git a/docs/src/api/titiler/extensions/wmts.md b/docs/src/api/titiler/extensions/wmts.md new file mode 100644 index 000000000..f300bc2e4 --- /dev/null +++ b/docs/src/api/titiler/extensions/wmts.md @@ -0,0 +1 @@ +::: titiler.extensions.wmts diff --git a/docs/src/api/titiler/mosaic/errors.md b/docs/src/api/titiler/mosaic/errors.md index 2f3e10c48..e1ef9b87e 100644 --- a/docs/src/api/titiler/mosaic/errors.md +++ b/docs/src/api/titiler/mosaic/errors.md @@ -1,11 +1 @@ -# Module titiler.mosaic.errors - -Titiler mosaic errors. - -None - -## Variables - -```python3 -MOSAIC_STATUS_CODES -``` \ No newline at end of file +::: titiler.mosaic.errors diff --git a/docs/src/api/titiler/mosaic/factory.md b/docs/src/api/titiler/mosaic/factory.md index ad00ccce1..cf1ddcc8b 100644 --- a/docs/src/api/titiler/mosaic/factory.md +++ b/docs/src/api/titiler/mosaic/factory.md @@ -1,353 +1 @@ -# Module titiler.mosaic.factory - -TiTiler.mosaic Router factories. - -None - -## Variables - -```python3 -MAX_THREADS -``` - -```python3 -img_endpoint_params -``` - -```python3 -mosaic_tms -``` - -## Functions - - -### PixelSelectionParams - -```python3 -def PixelSelectionParams( - pixel_selection: titiler.mosaic.resources.enums.PixelSelectionMethod = Query(first) -) -> rio_tiler.mosaic.methods.base.MosaicMethodBase -``` - - -Returns the mosaic method used to combine datasets together. - -## Classes - -### MosaicTilerFactory - -```python3 -class MosaicTilerFactory( - reader: Type[cogeo_mosaic.backends.base.BaseBackend] = , - router: fastapi.routing.APIRouter = , - path_dependency: Callable[..., Any] = , - dataset_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - layer_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - render_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - colormap_dependency: Callable[..., Union[Dict[int, Tuple[int, int, int, int]], Sequence[Tuple[Tuple[Union[float, int], Union[float, int]], Tuple[int, int, int, int]]], NoneType]] = , - process_dependency: Callable[..., Optional[titiler.core.algorithm.base.BaseAlgorithm]] = .post_process at 0x146adc670>, - reader_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - environment_dependency: Callable[..., Dict] = at 0x146adc5e0>, - supported_tms: morecantile.defaults.TileMatrixSets = TileMatrixSets(tms={'WebMercatorQuad': }), - default_tms: str = 'WebMercatorQuad', - router_prefix: str = '', - optional_headers: List[titiler.core.resources.enums.OptionalHeader] = , - route_dependencies: List[Tuple[List[titiler.core.routing.EndpointScope], List[fastapi.params.Depends]]] = , - extensions: List[titiler.core.factory.FactoryExtension] = , - dataset_reader: Union[Type[rio_tiler.io.base.BaseReader], Type[rio_tiler.io.base.MultiBaseReader], Type[rio_tiler.io.base.MultiBandReader]] = , - backend_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - pixel_selection_dependency: Callable[..., rio_tiler.mosaic.methods.base.MosaicMethodBase] = , - add_viewer: bool = True -) -``` - -#### Ancestors (in MRO) - -* titiler.core.factory.BaseTilerFactory - -#### Class variables - -```python3 -add_viewer -``` - -```python3 -backend_dependency -``` - -```python3 -dataset_dependency -``` - -```python3 -dataset_reader -``` - -```python3 -default_tms -``` - -```python3 -layer_dependency -``` - -```python3 -reader_dependency -``` - -```python3 -render_dependency -``` - -```python3 -router_prefix -``` - -```python3 -supported_tms -``` - -#### Methods - - -#### add_route_dependencies - -```python3 -def add_route_dependencies( - self, - *, - scopes: List[titiler.core.routing.EndpointScope], - dependencies=typing.List[fastapi.params.Depends] -) -``` - - -Add dependencies to routes. - -Allows a developer to add dependencies to a route after the route has been defined. - - -#### assets - -```python3 -def assets( - self -) -``` - - -Register /assets endpoint. - - -#### bounds - -```python3 -def bounds( - self -) -``` - - -Register /bounds endpoint. - - -#### colormap_dependency - -```python3 -def colormap_dependency( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict[int, Tuple[int, int, int, int]], Sequence[Tuple[Tuple[Union[float, int], Union[float, int]], Tuple[int, int, int, int]]], NoneType] -``` - - -Colormap Dependency. - - -#### environment_dependency - -```python3 -def environment_dependency( - -) -``` - - - - -#### info - -```python3 -def info( - self -) -``` - - -Register /info endpoint - - -#### map_viewer - -```python3 -def map_viewer( - self -) -``` - - -Register /map endpoint. - - -#### path_dependency - -```python3 -def path_dependency( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -#### pixel_selection_dependency - -```python3 -def pixel_selection_dependency( - pixel_selection: titiler.mosaic.resources.enums.PixelSelectionMethod = Query(first) -) -> rio_tiler.mosaic.methods.base.MosaicMethodBase -``` - - -Returns the mosaic method used to combine datasets together. - - -#### point - -```python3 -def point( - self -) -``` - - -Register /point endpoint. - - -#### process_dependency - -```python3 -def process_dependency( - algorithm: Literal['hillshade', 'contours', 'normalizedIndex', 'terrarium', 'terrainrgb'] = Query(None), - algorithm_params: str = Query(None) -) -> Optional[titiler.core.algorithm.base.BaseAlgorithm] -``` - - -Data Post-Processing options. - - -#### read - -```python3 -def read( - self -) -``` - - -Register / (Get) Read endpoint. - - -#### reader - -```python3 -def reader( - input: str, - *args: Any, - **kwargs: Any -) -> cogeo_mosaic.backends.base.BaseBackend -``` - - -Select mosaic backend for input. - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -This Method register routes to the router. - -Because we wrap the endpoints in a class we cannot define the routes as -methods (because of the self argument). The HACK is to define routes inside -the class method and register them after the class initialization. - - -#### tile - -```python3 -def tile( - self -) -``` - - -Register /tiles endpoints. - - -#### tilejson - -```python3 -def tilejson( - self -) -``` - - -Add tilejson endpoint. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - - -#### validate - -```python3 -def validate( - self -) -``` - - -Register /validate endpoint. - - -#### wmts - -```python3 -def wmts( - self -) -``` - - -Add wmts endpoint. \ No newline at end of file +::: titiler.mosaic.factory diff --git a/docs/src/api/titiler/mosaic/models/responses.md b/docs/src/api/titiler/mosaic/models/responses.md new file mode 100644 index 000000000..2b32fc0c6 --- /dev/null +++ b/docs/src/api/titiler/mosaic/models/responses.md @@ -0,0 +1 @@ +::: titiler.mosaic.models.responses diff --git a/docs/src/api/titiler/mosaic/mosaicjson.md b/docs/src/api/titiler/mosaic/mosaicjson.md new file mode 100644 index 000000000..a5b2c611f --- /dev/null +++ b/docs/src/api/titiler/mosaic/mosaicjson.md @@ -0,0 +1 @@ +::: titiler.mosaic.extensions.mosaicjson diff --git a/docs/src/api/titiler/mosaic/resources/enums.md b/docs/src/api/titiler/mosaic/resources/enums.md deleted file mode 100644 index d25733aeb..000000000 --- a/docs/src/api/titiler/mosaic/resources/enums.md +++ /dev/null @@ -1,56 +0,0 @@ -# Module titiler.mosaic.resources.enums - -Titiler.mosaic Enums. - -None - -## Classes - -### PixelSelectionMethod - -```python3 -class PixelSelectionMethod( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.str -* enum.Enum - -#### Class variables - -```python3 -first -``` - -```python3 -highest -``` - -```python3 -lowest -``` - -```python3 -mean -``` - -```python3 -median -``` - -```python3 -name -``` - -```python3 -stdev -``` - -```python3 -value -``` \ No newline at end of file diff --git a/docs/src/api/titiler/mosaic/wmts.md b/docs/src/api/titiler/mosaic/wmts.md new file mode 100644 index 000000000..c82c97050 --- /dev/null +++ b/docs/src/api/titiler/mosaic/wmts.md @@ -0,0 +1 @@ +::: titiler.mosaic.extensions.wmts diff --git a/docs/src/api/titiler/xarray/dependencies.md b/docs/src/api/titiler/xarray/dependencies.md new file mode 100644 index 000000000..6bb4bf4a6 --- /dev/null +++ b/docs/src/api/titiler/xarray/dependencies.md @@ -0,0 +1 @@ +::: titiler.xarray.dependencies diff --git a/docs/src/api/titiler/xarray/extensions.md b/docs/src/api/titiler/xarray/extensions.md new file mode 100644 index 000000000..e6b41dc30 --- /dev/null +++ b/docs/src/api/titiler/xarray/extensions.md @@ -0,0 +1 @@ +::: titiler.xarray.extensions diff --git a/docs/src/api/titiler/xarray/factory.md b/docs/src/api/titiler/xarray/factory.md new file mode 100644 index 000000000..74f2363b4 --- /dev/null +++ b/docs/src/api/titiler/xarray/factory.md @@ -0,0 +1 @@ +::: titiler.xarray.factory diff --git a/docs/src/api/titiler/xarray/io.md b/docs/src/api/titiler/xarray/io.md new file mode 100644 index 000000000..37ccfec84 --- /dev/null +++ b/docs/src/api/titiler/xarray/io.md @@ -0,0 +1 @@ +::: titiler.xarray.io diff --git a/docs/src/api/titiler/xarray/main.md b/docs/src/api/titiler/xarray/main.md new file mode 100644 index 000000000..5cf1ac66b --- /dev/null +++ b/docs/src/api/titiler/xarray/main.md @@ -0,0 +1 @@ +::: titiler.xarray.main diff --git a/docs/src/benchmark.html b/docs/src/benchmark.html new file mode 100644 index 000000000..9c1d0cb5c --- /dev/null +++ b/docs/src/benchmark.html @@ -0,0 +1,292 @@ + + + + + + + Benchmarks + + + + +
+ + + + + diff --git a/docs/src/deployment/aws/ecs.md b/docs/src/deployment/aws/ecs.md deleted file mode 100644 index d89b98e1a..000000000 --- a/docs/src/deployment/aws/ecs.md +++ /dev/null @@ -1,89 +0,0 @@ -# AWS ECS (Fargate) + ALB (Application Load Balancer) - -!!! warning - When using Fargate or vanilla ECS, you should set the number of worker carefully. Setting too high a number of workers could lead to extra charges due to a bug in fastapi (https://github.com/developmentseed/titiler/issues/119, https://github.com/tiangolo/fastapi/issues/253). - - -## Deploy - -The example handles tasks such as generating a docker image and setting up an application load balancer (ALB) and ECS services. - - -1. Install CDK and connect to your AWS account. This step is only necessary once per AWS account. - - ```bash - # Download titiler repo - $ git clone https://github.com/developmentseed/titiler.git - - # Create a virtual environment - python -m pip install --upgrade virtualenv - virtualenv .venv - source .venv/bin/activate - - # Install CDK dependencies - python -m pip install -r requirements-cdk.txt - - # Install NodeJS dependencies - npm install - - $ npm run cdk -- bootstrap # Deploys the CDK toolkit stack into an AWS environment - - # or in specific region - $ npm run cdk -- bootstrap aws://${AWS_ACCOUNT_ID}/eu-central-1 - ``` - -2. Generate CloudFormation template - - ```bash - $ npm run cdk -- synth # Synthesizes and prints the CloudFormation template for this stack - ``` - -3. Update settings (see [intro.md](intro.md)) - - ```bash - export TITILER_STACK_NAME="mytiler" - export TITILER_STACK_STAGE="dev" - export TITILER_STACK_MIN_ECS_INSTANCES=10 - ``` - - Available settings for ECS: - - ```python - min_ecs_instances: int = 5 - max_ecs_instances: int = 50 - - # CPU value | Memory value - # 256 (.25 vCPU) | 0.5 GB, 1 GB, 2 GB - # 512 (.5 vCPU) | 1 GB, 2 GB, 3 GB, 4 GB - # 1024 (1 vCPU) | 2 GB, 3 GB, 4 GB, 5 GB, 6 GB, 7 GB, 8 GB - # 2048 (2 vCPU) | Between 4 GB and 16 GB in 1-GB increments - # 4096 (4 vCPU) | Between 8 GB and 30 GB in 1-GB increments - task_cpu: int = 256 - task_memory: int = 512 - - # GUNICORN configuration - # Ref: https://github.com/developmentseed/titiler/issues/119 - - # WORKERS_PER_CORE - # This image will check how many CPU cores are available in the current server running your container. - # It will set the number of workers to the number of CPU cores multiplied by this value. - workers_per_core: int = 1 - - # MAX_WORKERS - # You can use it to let the image compute the number of workers automatically but making sure it's limited to a maximum. - # should depends on `task_cpu` - max_workers: int = 1 - - # WEB_CONCURRENCY - # Override the automatic definition of number of workers. - # Set to the number of CPU cores in the current server multiplied by the environment variable WORKERS_PER_CORE. - # So, in a server with 2 cores, by default it will be set to 2. - web_concurrency: Optional[int] - ``` - -4. Deploy - - ```bash - # Deploys the stack(s) mytiler-ecs-dev in cdk/app.py - $ npm run cdk -- deploy mytiler-ecs-dev - ``` diff --git a/docs/src/deployment/aws/intro.md b/docs/src/deployment/aws/intro.md index 5130c9fd0..63a52c11d 100644 --- a/docs/src/deployment/aws/intro.md +++ b/docs/src/deployment/aws/intro.md @@ -49,42 +49,6 @@ env: Dict = { # add S3 bucket where TiTiler could do HEAD and GET Requests buckets: List = [] -########################################################################### -# AWS ECS -# The following settings only apply to AWS ECS deployment -min_ecs_instances: int = 5 -max_ecs_instances: int = 50 - -# CPU value | Memory value -# 256 (.25 vCPU) | 0.5 GB, 1 GB, 2 GB -# 512 (.5 vCPU) | 1 GB, 2 GB, 3 GB, 4 GB -# 1024 (1 vCPU) | 2 GB, 3 GB, 4 GB, 5 GB, 6 GB, 7 GB, 8 GB -# 2048 (2 vCPU) | Between 4 GB and 16 GB in 1-GB increments -# 4096 (4 vCPU) | Between 8 GB and 30 GB in 1-GB increments -task_cpu: int = 256 -task_memory: int = 512 - -# GUNICORN configuration -# Ref: https://github.com/developmentseed/titiler/issues/119 - -# WORKERS_PER_CORE -# This image will check how many CPU cores are available in the current server running your container. -# It will set the number of workers to the number of CPU cores multiplied by this value. -workers_per_core: int = 1 - -# MAX_WORKERS -# You can use it to let the image compute the number of workers automatically but making sure it's limited to a maximum. -# should depends on `task_cpu` -max_workers: int = 1 - -# WEB_CONCURRENCY -# Override the automatic definition of number of workers. -# Set to the number of CPU cores in the current server multiplied by the environment variable WORKERS_PER_CORE. -# So, in a server with 2 cores, by default it will be set to 2. -web_concurrency: Optional[int] - -image_version: str = "latest" - ########################################################################### # AWS LAMBDA # The following settings only apply to AWS Lambda deployment diff --git a/docs/src/deployment/aws/lambda.md b/docs/src/deployment/aws/lambda.md index b473784cf..0c5fd6e50 100644 --- a/docs/src/deployment/aws/lambda.md +++ b/docs/src/deployment/aws/lambda.md @@ -20,27 +20,19 @@ The Lambda stack is also deployed by the [AWS CDK](https://aws.amazon.com/cdk/) git clone https://github.com/developmentseed/titiler.git cd titiler/deployment/aws - # Create a virtual environment - python -m pip install --upgrade virtualenv - virtualenv .venv - source .venv/bin/activate - - # Install CDK dependencies - python -m pip install -r requirements-cdk.txt - # Install NodeJS dependencies - npm install + npm install -g aws-cdk@2.159.1 - $ npm run cdk -- bootstrap # Deploys the CDK toolkit stack into an AWS environment + uv run cdk -- bootstrap # Deploys the CDK toolkit stack into an AWS environment # or in specific region - $ npm run cdk -- bootstrap aws://${AWS_ACCOUNT_ID}/eu-central-1 + uv run cdk -- bootstrap aws://${AWS_ACCOUNT_ID}/eu-central-1 ``` 2. Pre-Generate CFN template ```bash - $ npm run cdk -- synth # Synthesizes and prints the CloudFormation template for this stack + uv run cdk -- synth # Synthesizes and prints the CloudFormation template for this stack ``` 3. Update settings (see [intro.md](intro.md)) @@ -65,8 +57,8 @@ The Lambda stack is also deployed by the [AWS CDK](https://aws.amazon.com/cdk/) 4. Deploy ```bash - $ npm run cdk -- deploy mytiler-lambda-dev # Deploys the stack(s) titiler-lambda-dev in cdk/app.py + uv run cdk -- deploy mytiler-lambda-dev # Deploys the stack(s) titiler-lambda-dev in cdk/app.py # Deploy in specific region - $ AWS_DEFAULT_REGION=eu-central-1 AWS_REGION=eu-central-1 npm run cdk -- deploy mytiler-lambda-dev + AWS_DEFAULT_REGION=eu-central-1 AWS_REGION=eu-central-1 uv run cdk -- deploy mytiler-lambda-dev ``` diff --git a/docs/src/endpoints/algorithms.md b/docs/src/endpoints/algorithms.md new file mode 100644 index 000000000..58e375eee --- /dev/null +++ b/docs/src/endpoints/algorithms.md @@ -0,0 +1,116 @@ +In addition to the `/cog`, `/stac` and `/mosaicjson` endpoints, the `titiler.application` package FastAPI application commes with additional metadata endpoints. + +# Algorithms + +## API + +| Method | URL | Output | Description +| ------ | ---------------------------- |---------------- |-------------- +| `GET` | `/algorithms` | JSON | retrieve the list of available Algorithms +| `GET` | `/algorithms/{algorithmId}` | JSON | retrieve the metadata of the specified algorithm. + +## Description + + +### List Algorithm + +`:endpoint:/algorithm` - Get the list of supported TileMatrixSet + +```bash +$ curl https://myendpoint/algorithms | jq + +{ + "hillshade": { + "title": "Hillshade", + "description": "Create hillshade from DEM dataset.", + "inputs": { + "nbands": 1 + }, + "outputs": { + "nbands": 1, + "dtype": "uint8", + "min": null, + "max": null + }, + "parameters": { + "azimuth": { + "default": 90, + "maximum": 360, + "minimum": 0, + "title": "Azimuth", + "type": "integer" + }, + "angle_altitude": { + "default": 90.0, + "maximum": 90.0, + "minimum": -90.0, + "title": "Angle Altitude", + "type": "number" + }, + "buffer": { + "default": 3, + "maximum": 99, + "minimum": 0, + "title": "Buffer", + "type": "integer" + } + } + }, + ... +} +``` + +### Get Algorithm info + +`:endpoint:/algorithms/{algorithmId}` - Get the algorithm metadata + +- PathParams: + - **algorithmId**: algorithm name + +```bash +$ curl http://127.0.0.1:8000/algorithms/contours | jq + +{ + "title": "Contours", + "description": "Create contours from DEM dataset.", + "inputs": { + "nbands": 1 + }, + "outputs": { + "nbands": 3, + "dtype": "uint8", + "min": null, + "max": null + }, + "parameters": { + "increment": { + "default": 35, + "maximum": 999, + "minimum": 0, + "title": "Increment", + "type": "integer" + }, + "thickness": { + "default": 1, + "maximum": 10, + "minimum": 0, + "title": "Thickness", + "type": "integer" + }, + "minz": { + "default": -12000, + "maximum": 99999, + "minimum": -99999, + "title": "Minz", + "type": "integer" + }, + "maxz": { + "default": 8000, + "maximum": 99999, + "minimum": -99999, + "title": "Maxz", + "type": "integer" + } + } +} +``` diff --git a/docs/src/endpoints/cog.md b/docs/src/endpoints/cog.md index a1add20b0..13d1761a6 100644 --- a/docs/src/endpoints/cog.md +++ b/docs/src/endpoints/cog.md @@ -9,77 +9,88 @@ The `/cog` routes are based on `titiler.core.factory.TilerFactory` but with `cog | Method | URL | Output | Description | ------ | ------------------------------------------------------------------- |---------- |-------------- -| `GET` | `/cog/bounds` | JSON | return dataset's bounds | `GET` | `/cog/info` | JSON | return dataset's basic info | `GET` | `/cog/info.geojson` | GeoJSON | return dataset's basic info as a GeoJSON feature | `GET` | `/cog/statistics` | JSON | return dataset's statistics | `POST` | `/cog/statistics` | GeoJSON | return dataset's statistics for a GeoJSON -| `GET` | `/cog/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset -| `GET` | `/cog[/{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document -| `GET` | `/cog[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/cog/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/cog/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/cog/tiles/{tileMatrixSetId}/{z}/{x}/{y}[.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/cog/{tileMatrixSetId}/map.html` | HTML | simple map viewer +| `GET` | `/cog/{tileMatrixSetId}/tilejson.json` | JSON | return a Mapbox TileJSON document | `GET` | `/cog/point/{lon},{lat}` | JSON | return pixel values from a dataset -| `GET` | `/cog/preview[.{format}]` | image/bin | create a preview image from a dataset -| `GET` | `/cog/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset -| `POST` | `/cog/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a GeoJSON feature -| `GET` | `/cog[/{TileMatrixSetId}]/map` | HTML | simple map viewer +| `GET` | `/cog/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset +| `POST` | `/cog/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature +| `GET` | `/cog/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from a dataset +| `GET` | `/cog/map` | image/bin | create map image from a dataset | `GET` | `/cog/validate` | JSON | validate a COG and return dataset info (from `titiler.extensions.cogValidateExtension`) | `GET` | `/cog/viewer` | HTML | demo webpage (from `titiler.extensions.cogViewerExtension`) | `GET` | `/cog/stac` | GeoJSON | create STAC Items from a dataset (from `titiler.extensions.stacExtension`) +| `GET` | `/cog/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities (from `titiler.extensions.wmts.wmtsExtension`) + ## Description ### Tiles -`:endpoint:/cog/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` +`:endpoint:/cog/tiles/{tileMatrixSetId}/{z}/{x}/{y}[.{format}]` - PathParams: - - **TileMatrixSetId** (str): TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - **z** (int): TMS tile's zoom level. - **x** (int): TMS tile's column. - **y** (int): TMS tile's row. - - **scale** (int): Tile size scale, default is set to 1 (256x256). **Optional** - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. **Optional** - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** + - **tilesize** (int): overwrite TMS tileWidth x tileHeight with fixed tilesize. - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. Example: -- `https://myendpoint/cog/tiles/1/2/3?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/tiles/1/2/3.jpg?url=https://somewhere.com/mycog.tif&bidx=3&bidx=1&bidx2` +- `https://myendpoint/cog/tiles/WebMercatorQuad/1/2/3?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/tiles/WebMercatorQuad/1/2/3.jpg?url=https://somewhere.com/mycog.tif&bidx=3&bidx=1&bidx2` - `https://myendpoint/cog/tiles/WorldCRS84Quad/1/2/3@2x.png?url=https://somewhere.com/mycog.tif` - `https://myendpoint/cog/tiles/WorldCRS84Quad/1/2/3?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` ### Preview -`:endpoint:/cog/preview[.{format}]` +`:endpoint:/cog/preview` + +`:endpoint:/cog/preview.{format}` + +`:endpoint:/cog/preview/{width}x{height}.{format}` - PathParams: - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str, optional): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. **Also a QueryParam** + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **max_size** (int): Max image size, default is 1024. - - **height** (int): Force output image height. - - **width** (int): Force output image width. + - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -89,35 +100,38 @@ Example: - **algorithm_params** (str): JSON encoded algorithm parameters. !!! important - if **height** and **width** are provided **max_size** will be ignored. + if **height** or **width** is provided **max_size** will be ignored. Example: - `https://myendpoint/cog/preview?url=https://somewhere.com/mycog.tif` - `https://myendpoint/cog/preview.jpg?url=https://somewhere.com/mycog.tif&bidx=3&bidx=1&bidx2` +- `https://myendpoint/cog/preview/100x100.jpg?url=https://somewhere.com/mycog.tif&bidx=3&bidx=1&bidx2` - `https://myendpoint/cog/preview?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` -### Crop / Part +### Bbox -`:endpoint:/cog/crop/{minx},{miny},{maxx},{maxy}.{format}` +`:endpoint:/cog/bbox/{minx},{miny},{maxx},{maxy}.{format}` -`:endpoint:/cog/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` +`:endpoint:/cog/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` - PathParams: - **minx,miny,maxx,maxy** (str): Comma (',') delimited bounding box in WGS84. - - **format** (str): Output image format. - - **height** (int): Force output image height. - - **width** (int): Force output image width. + - **format** (str): Output [image format](../user_guide/output_format.md) + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). - - **coord-crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size, default is 1024. + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). + - **coord_crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -127,33 +141,66 @@ Example: - **algorithm_params** (str): JSON encoded algorithm parameters. !!! important - if **height** and **width** are provided **max_size** will be ignored. + if **height** or **width** is provided **max_size** will be ignored. Example: -- `https://myendpoint/cog/crop/0,0,10,10.png?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/crop/0,0,10,10.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` +- `https://myendpoint/cog/bbox/0,0,10,10.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` +- `https://myendpoint/cog/bbox/0,0,10,10/100x100.png?url=https://somewhere.com/mycog.tif` + +### OGC Maps API - GetMap + +`:endpoint:/cog/map` + +- QueryParams: + - **url** (str): Cloud Optimized GeoTIFF URL. **Required** + - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). + - **nodata** (str, int, float): Overwrite internal Nodata value. + - **unscale** (bool): Apply dataset internal Scale/Offset. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. + - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). + - **color_formula** (str): rio-color formula. + - **colormap** (str): JSON encoded custom Colormap. + - **colormap_name** (str): rio-tiler color map name. + - **return_mask** (bool): Add mask to the output data. Default is True. + - **algorithm** (str): Custom algorithm name (e.g `hillshade`). + - **algorithm_params** (str): JSON encoded algorithm parameters. + - **bbox** (str): Comma (',') delimited bounding box. + - **bbox-crs** (str, optional): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. + - **crs** (str, optional): Output Coordinate Reference System. Default to dataset'crs. + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** + - **f** (str): Output [image format](../user_guide/output_format.md) + +### Feature + +`:endpoint:/cog/feature - [POST]` +`:endpoint:/cog/feature.{format} - [POST]` -`:endpoint:/cog/crop[/{width}x{height}][].{format}] - [POST]` +`:endpoint:/cog/feature/{width}x{height}.{format} - [POST]` - Body: - **feature** (JSON): A valid GeoJSON feature (Polygon or MultiPolygon) - PathParams: - - **height** (int): Force output image height. **Optional** - - **width** (int): Force output image width. **Optional** - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** + - **format** (str, optional): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. **Also a QueryParam** - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). - - **coord-crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size, default is 1024. + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). + - **coord_crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -163,15 +210,13 @@ Example: - **algorithm_params** (str): JSON encoded algorithm parameters. !!! important - if **height** and **width** are provided **max_size** will be ignored. + if **height** or **width** is provided **max_size** will be ignored. Example: -- `https://myendpoint/cog/crop?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/crop.png?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/crop/100x100.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` - -Note: if `height` and `width` are provided `max_size` will be ignored. +- `https://myendpoint/cog/feature?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/feature.png?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/feature/100x100.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` ### Point @@ -183,11 +228,12 @@ Note: if `height` and `width` are provided `max_size` will be ignored. - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). - - **coord-crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). + - **coord_crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. Example: @@ -196,98 +242,97 @@ Example: ### TilesJSON -`:endpoint:/cog[/{TileMatrixSetId}]/tilejson.json` tileJSON document +`:endpoint:/cog/{tileMatrixSetId}/tilejson.json` tileJSON document - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - - **tile_format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. - - **tile_scale** (int): Tile size scale, default is set to 1 (256x256). + - **tile_format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. + - **tilesize** (int): overwrite TMS tileWidth x tileHeight with fixed tilesize. Default to 512. - **minzoom** (int): Overwrite default minzoom. - **maxzoom** (int): Overwrite default maxzoom. - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. Example: -- `https://myendpoint/cog/tilejson.json?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/tilejson.json?url=https://somewhere.com/mycog.tif&tile_format=png` -- `https://myendpoint/cog/WorldCRS84Quad/tilejson.json?url=https://somewhere.com/mycog.tif&tile_scale=2&bidx=1,2,3` +- `https://myendpoint/cog/WebMercatorQuad/tilejson.json?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/WebMercatorQuad/tilejson.json?url=https://somewhere.com/mycog.tif&tile_format=png` +- `https://myendpoint/cog/WorldCRS84Quad/tilejson.json?url=https://somewhere.com/mycog.tif&tilesize=256&bidx=1,2,3` ### Map -`:endpoint:/cog[/{TileMatrixSetId}]/map` Simple viewer +`:endpoint:/cog/{tileMatrixSetId}/map.html` Simple viewer - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - - **tile_format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. - - **tile_scale** (int): Tile size scale, default is set to 1 (256x256). + - **tile_format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. + - **tilesize** (int): overwrite TMS tileWidth x tileHeight with fixed tilesize. Defaults to 256. - **minzoom** (int): Overwrite default minzoom. - **maxzoom** (int): Overwrite default maxzoom. - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. Example: -- `https://myendpoint/cog/map?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/map?url=https://somewhere.com/mycog.tif&tile_format=png` -- `https://myendpoint/cog/WebMercatorQuad/map?url=https://somewhere.com/mycog.tif&tile_scale=2&bidx=1,2,3` +- `https://myendpoint/cog/WebMercatorQuad/map.html?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/WebMercatorQuad/map.html?url=https://somewhere.com/mycog.tif&tile_format=png` +- `https://myendpoint/cog/WorldCRS84Quad/map.html?url=https://somewhere.com/mycog.tif&tilesize=512&bidx=1,2,3` -### Bounds +### Info -`:endpoint:/cog/bounds` general image bounds +`:endpoint:/cog/info` general raster info - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** Example: -- `https://myendpoint/cog/bounds?url=https://somewhere.com/mycog.tif` - -### Info - -`:endpoint:/cog/info` general raster info +- `https://myendpoint/cog/info?url=https://somewhere.com/mycog.tif` `:endpoint:/cog/info.geojson` general raster info as a GeoJSON feature - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** + - **crs** (str): Geographic Coordinate Reference System. Default to `epsg:4326`. Example: -- `https://myendpoint/cog/info?url=https://somewhere.com/mycog.tif` - `https://myendpoint/cog/info.geojson?url=https://somewhere.com/mycog.tif` - ### Statistics Advanced raster statistics @@ -297,13 +342,15 @@ Advanced raster statistics - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **max_size** (int): Max image size from which to calculate statistics, default is 1024. - **height** (int): Force image height from which to calculate statistics. - **width** (int): Force image width from which to calculate statistics. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **algorithm** (str): Custom algorithm name (e.g `hillshade`). + - **algorithm_params** (str): JSON encoded algorithm parameters. - **categorical** (bool): Return statistics for categorical dataset, default is false. - **c** (array[float]): Pixels values for categories. - **p** (array[int]): Percentile values. @@ -322,14 +369,18 @@ Example: - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). - - **coord-crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size from which to calculate statistics, default is 1024. + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). + - **coord_crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size from which to calculate statistics. - **height** (int): Force image height from which to calculate statistics. - **width** (int): Force image width from which to calculate statistics. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. + - **algorithm** (str): Custom algorithm name (e.g `hillshade`). + - **algorithm_params** (str): JSON encoded algorithm parameters. - **categorical** (bool): Return statistics for categorical dataset, default is false. - **c** (array[float]): Pixels values for categories. - **p** (array[int]): Percentile values. diff --git a/docs/src/endpoints/colormaps.md b/docs/src/endpoints/colormaps.md new file mode 100644 index 000000000..8ad4f10db --- /dev/null +++ b/docs/src/endpoints/colormaps.md @@ -0,0 +1,96 @@ +In addition to the `/cog`, `/stac` and `/mosaicjson` endpoints, the `titiler.application` package FastAPI application commes with additional metadata endpoints. + +# Algorithms + +## API + +| Method | URL | Output | Description +| ------ | ---------------------------- |--------|-------------- +| `GET` | `/colorMaps` | JSON | retrieve the list of available colorMaps +| `GET` | `/colorMaps/{colorMapId}` | JSON | retrieve the metadata or image of the specified colorMap. + +## Description + + +### List colormaps + +`:endpoint:/colorMaps` - Get the list of supported ColorMaps + +```bash +$ curl https://myendpoint/colorMaps | jq + +{ + "colorMaps": [ + "dense_r", + "delta", + ... + ], + "links": [ + { + "href": "http://myendpoint/colorMaps", + "rel": "self", + "type": "application/json", + "title": "List of available colormaps" + }, + { + "href": "http://myendpoint/colorMaps/{colorMapId}", + "rel": "data", + "type": "application/json", + "templated": true, + "title": "Retrieve colormap metadata" + }, + { + "href": "http://myendpoint/colorMaps/{colorMapId}?format=png", + "rel": "data", + "type": "image/png", + "templated": true, + "title": "Retrieve colormap as image" + } + ] +} +``` + +### Get ColorMap metadata or as image + +`:endpoint:/colorMaps/{colorMapId}` - Get the ColorMap metadata or image + +- PathParams: + - **colorMapId**: colormap name + +- QueryParams: + - **format** (str): output image format (PNG/JPEG...). Defaults to JSON output. + - **orientation** (["vertical", "horizontal"]): image orientation. Defaults to `horizontal`. + - **height** (int): output image height. Default to 20px for horizontal or 256px for vertical. + - **width** (int): output image width. Defaults to 256px for horizontal or 20px for vertical. + +```bash +$ curl http://myendpoint/colorMaps/viridis | jq + +{ + "0": [ + 68, + 1, + 84, + 255 + ], + ... + "255": [ + 253, + 231, + 36, + 255 + ] +} +``` + +``` +curl http://myendpoint/colorMaps/viridis?format=png +``` + +``` +curl http://myendpoint/colorMaps/viridis?format=png&orientation=vertical +``` + +``` +curl http://myendpoint/colorMaps/viridis?format=png&orientation=vertical&width=100&height=1000 +``` diff --git a/docs/src/endpoints/mosaic.md b/docs/src/endpoints/mosaic.md index 47537129c..47334e12f 100644 --- a/docs/src/endpoints/mosaic.md +++ b/docs/src/endpoints/mosaic.md @@ -9,18 +9,20 @@ Read Mosaic Info/Metadata and create Web map Tiles from a multiple COG. The `mos | Method | URL | Output | Description | ------ | -------------------------------------------------------------------------- |---------- |-------------- -| `GET` | `/mosaicjson/` | JSON | return a MosaicJSON document -| `GET` | `/mosaicjson/bounds` | JSON | return mosaic's bounds | `GET` | `/mosaicjson/info` | JSON | return mosaic's basic info | `GET` | `/mosaicjson/info.geojson` | GeoJSON | return mosaic's basic info as a GeoJSON feature -| `GET` | `/mosaicjson/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from mosaic assets -| `GET` | `/mosaicjson[/{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document -| `GET` | `/mosaicjson[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/mosaicjson/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/mosaicjson/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/mosaicjson/tiles/{tileMatrixSetId}/{z}/{x}/{y}[.{format}]` | image/bin | create a web map tile image from mosaic assets +| `GET` | `/mosaicjson/{tileMatrixSetId}/map.html` | HTML | simple map viewer +| `GET` | `/mosaicjson/{tileMatrixSetId}/tilejson.json` | JSON | return a Mapbox TileJSON document | `GET` | `/mosaicjson/point/{lon},{lat}` | JSON | return pixel value from a mosaic assets -| `GET` | `/mosaicjson/{z}/{x}/{y}/assets` | JSON | return list of assets intersecting a XYZ tile -| `GET` | `/mosaicjson/{lon},{lat}/assets` | JSON | return list of assets intersecting a point -| `GET` | `/mosaicjson/{minx},{miny},{maxx},{maxy}/assets` | JSON | return list of assets intersecting a bounding box -| `GET` | `/mosaicjson[/{TileMatrixSetId}]/map` | HTML | simple map viewer +| `GET` | `/mosaicjson/tiles/{tileMatrixSetId}/{z}/{x}/{y}/assets` | JSON | return list of assets intersecting a XYZ tile +| `GET` | `/mosaicjson/point/{lon},{lat}/assets` | JSON | return list of assets intersecting a point +| `GET` | `/mosaicjson/bbox/{minx},{miny},{maxx},{maxy}/assets` | JSON | return list of assets intersecting a bounding box +| `GET` | `/mosaicjson/` | JSON | return a MosaicJSON document (from titiler.mosaic.extensions.mosaicjson.MosaicJSONExtension) +| `GET` | `/mosaicjson/validate` | JSON | validate a MosaicJSON document (from titiler.mosaic.extensions.mosaicjson.MosaicJSONExtension) +| `GET` | `/mosaicjson/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities (from titiler.mosaic.extensions.wmts.wmtsExtension) ## Description diff --git a/docs/src/endpoints/stac.md b/docs/src/endpoints/stac.md index b70a9700d..656a2ab77 100644 --- a/docs/src/endpoints/stac.md +++ b/docs/src/endpoints/stac.md @@ -10,83 +10,86 @@ The `/stac` routes are based on `titiler.core.factory.MultiBaseTilerFactory` but | Method | URL | Output | Description | ------ | -------------------------------------------------------------------- |---------- |-------------- | `GET` | `/stac/assets` | JSON | return available assets within the STAC item -| `GET` | `/stac/bounds` | JSON | return STAC item bounds | `GET` | `/stac/info` | JSON | return asset's basic info | `GET` | `/stac/info.geojson` | GeoJSON | return asset's basic info as a GeoJSON feature | `GET` | `/stac/asset_statistics` | JSON | return per asset statistics | `GET` | `/stac/statistics` | JSON | return asset's statistics | `POST` | `/stac/statistics` | GeoJSON | return asset's statistics for a GeoJSON -| `GET` | `/stac/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets -| `GET` | `/stac[/{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document -| `GET` | `/stac[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/stac/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/stac/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/stac/tiles/{tileMatrixSetId}/{z}/{x}/{y}[.{format}]` | image/bin | create a web map tile image from assets +| `GET` | `/stac/{tileMatrixSetId}/map.html` | HTML | simple map viewer +| `GET` | `/stac/{tileMatrixSetId}/tilejson.json` | JSON | return a Mapbox TileJSON document | `GET` | `/stac/point/{lon},{lat}` | JSON | return pixel value from assets -| `GET` | `/stac/preview[.{format}]` | image/bin | create a preview image from assets -| `GET` | `/stac/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets -| `POST` | `/stac/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a geojson covering the assets -| `GET` | `/stac[/{TileMatrixSetId}]/map` | HTML | simple map viewer +| `GET` | `/stac/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets +| `POST` | `/stac/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson covering the assets +| `GET` | `/stac/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from assets | `GET` | `/stac/viewer` | HTML | demo webpage (from `titiler.extensions.stacViewerExtension`) +| `GET` | `/stac/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities (from `titiler.extensions.wmts.wmtsExtension`) ## Description ### Tiles -`:endpoint:/stac/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` +`:endpoint:/stac/tiles/{tileMatrixSetId}/{z}/{x}/{y}[.{format}]` - PathParams: - - **TileMatrixSetId** (str): TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - **z** (int): TMS tile's zoom level. - **x** (int): TMS tile's column. - **y** (int): TMS tile's row. - - **scale** (int): Tile size scale, default is set to 1 (256x256). **Optional** - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. **Optional** - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. - - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). + - **assets** (array[str]): asset names. **Required** + - **expression** (str): rio-tiler's math expression (e.g `b1/b2`). + - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset. + - **tilesize** (int): overwrite TMS tileWidth x tileHeight with fixed tilesize. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. -!!! important - **assets** OR **expression** is required - Example: -- `https://myendpoint/stac/tiles/1/2/3?url=https://somewhere.com/item.json&assets=B01&assets=B00` -- `https://myendpoint/stac/tiles/1/2/3.jpg?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/tiles/WorldCRS84Quad/1/2/3@2x.png?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/tiles/WorldCRS84Quad/1/2/3?url=https://somewhere.com/item.json&expression=B01/B02&rescale=0,1000&colormap_name=cfastie` +- `https://myendpoint/stac/tiles/WebMercatorQuad/1/2/3?url=https://somewhere.com/item.json&assets=B01&assets=B00` +- `https://myendpoint/stac/tiles/WebMercatorQuad/1/2/3.jpg?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/tiles/WorldCRS84Quad/1/2/3?url=https://somewhere.com/item.json&assets=B01&assets=B02expression=b1/b2&rescale=0,1000&colormap_name=cfastie` ### Preview -`:endpoint:/stac/preview[.{format}]` +`:endpoint:/stac/preview` + +`:endpoint:/stac/preview/.{format}` + +`:endpoint:/stac/preview/{width}x{height}.{format}` - PathParams: - - **format**: Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str, optional): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. **Also a QueryParam** + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. - - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). + - **assets** (array[str]): asset names. **Required** + - **expression** (str): rio-tiler's math expression (e.g `b1/b2`). + - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset. - **max_size** (int): Max image size, default is 1024. - - **height** (int): Force output image height. - - **width** (int): Force output image width. + - **dst_crs** (str): Output Coordinate Reference System. Default to dataset's CRS. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -96,39 +99,39 @@ Example: - **algorithm_params** (str): JSON encoded algorithm parameters. !!! important - - **assets** OR **expression** is required - - - if **height** and **width** are provided **max_size** will be ignored. + - if **height** or **width** is provided **max_size** will be ignored. Example: - `https://myendpoint/stac/preview?url=https://somewhere.com/item.json&assets=B01` - `https://myendpoint/stac/preview.jpg?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/preview?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` +- `https://myendpoint/stac/preview/100x100.jpg?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/preview?url=https://somewhere.com/item.json&assets=B01&assets=B02expression=b1/b2&rescale=0,1000&colormap_name=cfastie` -### Crop / Part +### Bbox -`:endpoint:/stac/crop/{minx},{miny},{maxx},{maxy}.{format}` +`:endpoint:/stac/bbox/{minx},{miny},{maxx},{maxy}.{format}` -`:endpoint:/stac/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` +`:endpoint:/stac/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` - PathParams: - **minx,miny,maxx,maxy** (str): Comma (',') delimited bounding box in WGS84. - - **height** (int): Force output image height. **Optional** - - **width** (int): Force output image width. **Optional** - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str): Output [image format](../user_guide/output_format.md). + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. - - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **coord-crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size, default is 1024. + - **assets** (array[str]): asset names. **Required** + - **expression** (str): rio-tiler's math expression (e.g `b1/b2`). + - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset. + - **coord_crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -138,36 +141,69 @@ Example: - **algorithm_params** (str): JSON encoded algorithm parameters. !!! important - - **assets** OR **expression** is required - - - if **height** and **width** are provided **max_size** will be ignored. + - if **height** or **width** is provided **max_size** will be ignored. Example: -- `https://myendpoint/stac/crop/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/crop/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` +- `https://myendpoint/stac/bbox/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/bbox/0,0,10,10/100x100.png?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/bbox/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01&assets=B02expression=b1/b2&rescale=0,1000&colormap_name=cfastie` + +### OGC Maps API - GetMap + +`:endpoint:/stac/map` + +- QueryParams: + - **url** (str): STAC Item URL. **Required** + - **assets** (array[str]): asset names. **Required** + - **expression** (str): rio-tiler's math expression (e.g `b1/b2`). + - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset. + - **nodata** (str, int, float): Overwrite internal Nodata value. + - **unscale** (bool): Apply dataset internal Scale/Offset. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. + - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). + - **color_formula** (str): rio-color formula. + - **colormap** (str): JSON encoded custom Colormap. + - **colormap_name** (str): rio-tiler color map name. + - **return_mask** (bool): Add mask to the output data. Default is True. + - **algorithm** (str): Custom algorithm name (e.g `hillshade`). + - **algorithm_params** (str): JSON encoded algorithm parameters. + - **bbox** (str): Comma (',') delimited bounding box. + - **bbox-crs** (str, optional): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. + - **crs** (str, optional): Output Coordinate Reference System. Default to dataset'crs. + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** + - **f** (str): Output [image format](../user_guide/output_format.md) + +### Feature + +`:endpoint:/stac/feature - [POST]` + +`:endpoint:/stac/feature.{format} - [POST]` -`:endpoint:/stac/crop[/{width}x{height}][].{format}] - [POST]` +`:endpoint:/stac/feature/{width}x{height}.{format} - [POST]` - Body: - **feature** (JSON): A valid GeoJSON feature (Polygon or MultiPolygon) - PathParams: - - **height** (int): Force output image height. **Optional** - - **width** (int): Force output image width. **Optional** - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str, optional): Output [image format](../user_guide/output_format.md). **Also a QueryParam** + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. - - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **coord-crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size, default is 1024. + - **assets** (array[str]): asset names. **Required** + - **expression** (str): rio-tiler's math expression (e.g `b1/b2`). + - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset. + - **coord_crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -177,15 +213,13 @@ Example: - **algorithm_params** (str): JSON encoded algorithm parameters. !!! important - - **assets** OR **expression** is required - - - if **height** and **width** are provided **max_size** will be ignored. + - if **height** or **width** is provided **max_size** will be ignored. Example: -- `https://myendpoint/stac/crop?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/crop.png?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/crop/100x100.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` +- `https://myendpoint/stac/feature?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/feature.png?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/feature/100x100.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` ### Point @@ -196,17 +230,14 @@ Example: - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. - - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. + - **assets** (array[str]): asset names. **Required** + - **expression** (str): rio-tiler's math expression (e.g `b1/b2`). + - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset. - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **coord-crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. + - **coord_crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. - -!!! important - **assets** OR **expression** is required + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. Example: @@ -214,91 +245,75 @@ Example: ### TilesJSON -`:endpoint:/stac[/{TileMatrixSetId}]/tilejson.json` tileJSON document +`:endpoint:/stac/{tileMatrixSetId}/tilejson.json` tileJSON document - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. - - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **tile_format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. - - **tile_scale** (int): Tile size scale, default is set to 1 (256x256). + - **assets** (array[str]): asset names. **Required** + - **expression** (str): rio-tiler's math expression (e.g `b1/b2`). + - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset. + - **tile_format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. + - **tilesize** (int): overwrite TMS tileWidth x tileHeight with fixed tilesize. Defaults to 512. - **minzoom** (int): Overwrite default minzoom. - **maxzoom** (int): Overwrite default maxzoom. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. -!!! important - **assets** OR **expression** is required - Example: -- `https://myendpoint/stac/tilejson.json?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/tilejson.json?url=https://somewhere.com/item.json&assets=B01&tile_format=png` -- `https://myendpoint/stac/WorldCRS84Quad/tilejson.json?url=https://somewhere.com/item.json&tile_scale=2&expression=B01/B02` +- `https://myendpoint/stac/WebMercatorQuad/tilejson.json?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/WebMercatorQuad/tilejson.json?url=https://somewhere.com/item.json&assets=B01&tile_format=png` +- `https://myendpoint/stac/WorldCRS84Quad/tilejson.json?url=https://somewhere.com/item.json&tilesize=256&assets=B01&assets=B02&expression=b1/b2` ### Map -`:endpoint:/stac[/{TileMatrixSetId}]/map` Simple viewer +`:endpoint:/stac/{tileMatrixSetId}/map.html` Simple viewer - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. - - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **tile_format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. - - **tile_scale** (int): Tile size scale, default is set to 1 (256x256). + - **assets** (array[str]): asset names. **Required** + - **expression** (str): rio-tiler's math expression (e.g `b1/b2`). + - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset. + - **tile_format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. + - **tilesize** (int): overwrite TMS tileWidth x tileHeight with fixed tilesize. Defaults to 256. - **minzoom** (int): Overwrite default minzoom. - **maxzoom** (int): Overwrite default maxzoom. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. -!!! important - **assets** OR **expression** is required - -Example: - -- `https://myendpoint/stac/tilejson.json?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/tilejson.json?url=https://somewhere.com/item.json&assets=B01&tile_format=png` -- `https://myendpoint/stac/WorldCRS84Quad/tilejson.json?url=https://somewhere.com/item.json&tile_scale=2&expression=B01/B02` - - -### Bounds - -`:endpoint:/stac/bounds` - Return the bounds of the STAC item. - -- QueryParams: - - **url** (str): STAC Item URL. **Required** - Example: -- `https://myendpoint/stac/bounds?url=https://somewhere.com/item.json` +- `https://myendpoint/stac/WebMercatorQuad/tilejson.json?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/WebMercatorQuad/tilejson.json?url=https://somewhere.com/item.json&assets=B01&tile_format=png` +- `https://myendpoint/stac/WorldCRS84Quad/tilejson.json?url=https://somewhere.com/item.json&tilesize=512&expression=B01/B02` ### Info @@ -307,22 +322,26 @@ Example: - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. Default to all available assets. + - **assets** (array[str]): asset names. **Required** Example: - `https://myendpoint/stac/info?url=https://somewhere.com/item.json&assets=B01` +!!! note + + Use `assets=:all:` to use all available assets + `:endpoint:/stac/info.geojson` - Return basic info on STAC item's COG as a GeoJSON feature - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. Default to all available assets. - -Example: - -- `https://myendpoint/stac/info.geojson?url=https://somewhere.com/item.json&assets=B01` + - **assets** (array[str]): asset names. **Required** + - **crs** (str): Geographic Coordinate Reference System. Default to `epsg:4326`. +!!! note + + Use `assets=:all:` to use all available assets `:endpoint:/stac/assets` - Return the list of available assets @@ -336,15 +355,13 @@ Example: - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. Default to all available assets. - - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **asset_expression** (array[str]): Per asset band math expression (e.g `Asset1|b1\*b2`). + - **assets** (array[str]): asset names. **Required** - **max_size** (int): Max image size from which to calculate statistics, default is 1024. - **height** (int): Force image height from which to calculate statistics. - **width** (int): Force image width from which to calculate statistics. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. - **categorical** (bool): Return statistics for categorical dataset, default is false. - **c** (array[float]): Pixels values for categories. - **p** (array[int]): Percentile values. @@ -355,21 +372,25 @@ Example: - `https://myendpoint/stac/statistics?url=https://somewhere.com/item.json&assets=B01&categorical=true&c=1&c=2&c=3&p=2&p98` +!!! note + + Use `assets=:all:` to use all available assets `:endpoint:/stac/statistics - [GET]` - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. Default to all available assets. - - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). + - **assets** (array[str]): asset names. **Required** + - **expression** (str): rio-tiler's math expression (e.g `b1/b2`). + - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset. - **max_size** (int): Max image size from which to calculate statistics, default is 1024. - **height** (int): Force image height from which to calculate statistics. - **width** (int): Force image width from which to calculate statistics. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **algorithm** (str): Custom algorithm name (e.g `hillshade`). + - **algorithm_params** (str): JSON encoded algorithm parameters. - **categorical** (bool): Return statistics for categorical dataset, default is false. - **c** (array[float]): Pixels values for categories. - **p** (array[int]): Percentile values. @@ -380,6 +401,9 @@ Example: - `https://myendpoint/stac/statistics?url=https://somewhere.com/item.json&assets=B01&categorical=true&c=1&c=2&c=3&p=2&p98` +!!! note + + Use `assets=:all:` to use all available assets `:endpoint:/stac/statistics - [POST]` @@ -388,17 +412,20 @@ Example: - QueryParams: - **url** (str): STAC Item URL. **Required** - - **assets** (array[str]): asset names. Default to all available assets. - - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **coord-crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size from which to calculate statistics, default is 1024. + - **assets** (array[str]): asset names. **Required** + - **expression** (str): rio-tiler's math expression (e.g `b1/b2`). + - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset. + - **coord_crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size from which to calculate statistics. - **height** (int): Force image height from which to calculate statistics. - **width** (int): Force image width from which to calculate statistics. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. + - **algorithm** (str): Custom algorithm name (e.g `hillshade`). + - **algorithm_params** (str): JSON encoded algorithm parameters. - **categorical** (bool): Return statistics for categorical dataset, default is false. - **c** (array[float]): Pixels values for categories. - **p** (array[int]): Percentile values. @@ -409,6 +436,10 @@ Example: - `https://myendpoint/stac/statistics?url=https://somewhere.com/item.json&assets=B01&categorical=true&c=1&c=2&c=3&p=2&p98` +!!! note + + Use `assets=:all:` to use all available assets + ### Viewer diff --git a/docs/src/endpoints/tms.md b/docs/src/endpoints/tms.md index 41cd57516..3a634b682 100644 --- a/docs/src/endpoints/tms.md +++ b/docs/src/endpoints/tms.md @@ -1,24 +1,14 @@ -The `titiler.application` package comes with a full FastAPI application with COG, STAC and MosaicJSON supports. -# TileMatrixSets - -The `tms` router extend the default `titiler.core.factory.TMSFactory`, adding some custom TileMatrixSets. - -```python -from fastapi import FastAPI -from titiler.application.routers.tms import tms - -app = FastAPI() -app.include_router(tms.router, tags=["TileMatrixSets"]) -``` +In addition to the `/cog`, `/stac` and `/mosaicjson` endpoints, the `titiler.application` package FastAPI application comes with additional metadata endpoints. +# TileMatrixSets ## API | Method | URL | Output | Description | ------ | ----------------------------------- |---------- |-------------- | `GET` | `/tileMatrixSets` | JSON | return the list of supported TileMatrixSet -| `GET` | `/tileMatrixSets/{TileMatrixSetId}` | JSON | return the TileMatrixSet JSON document +| `GET` | `/tileMatrixSets/{tileMatrixSetId}` | JSON | return the TileMatrixSet JSON document ## Description @@ -50,10 +40,10 @@ $ curl https://myendpoint/tileMatrixSets | jq ### Get TMS info -`:endpoint:/tileMatrixSets/{TileMatrixSetId}` - Get the TileMatrixSet JSON document +`:endpoint:/tileMatrixSets/{tileMatrixSetId}` - Get the TileMatrixSet JSON document - PathParams: - - **TileMatrixSetId**: TileMatrixSet name + - **tileMatrixSetId**: TileMatrixSet name ```bash $ curl http://127.0.0.1:8000/tileMatrixSets/WebMercatorQuad | jq diff --git a/docs/src/examples/code/create_gdal_wmts_extension.md b/docs/src/examples/code/create_gdal_wmts_extension.md index 4a687c84d..396127538 100644 --- a/docs/src/examples/code/create_gdal_wmts_extension.md +++ b/docs/src/examples/code/create_gdal_wmts_extension.md @@ -29,19 +29,7 @@ class gdalwmtsExtension(FactoryExtension): """Register endpoint to the tiler factory.""" @factory.router.get( - "/wmts.xml", - response_class=XMLResponse, - responses={ - 200: { - "description": "GDAL WMTS service description XML file", - "content": { - "application/xml": {}, - }, - }, - }, - ) - @factory.router.get( - "/{TileMatrixSetId}/wmts.xml", + "/{tileMatrixSetId}/wmts.xml", response_class=XMLResponse, responses={ 200: { @@ -54,9 +42,8 @@ class gdalwmtsExtension(FactoryExtension): ) def gdal_wmts( request: Request, - TileMatrixSetId: Literal[tuple(factory.supported_tms.list())] = Query( # type: ignore - factory.default_tms, - description=f"TileMatrixSet Name (default: '{factory.default_tms}')", + tileMatrixSetId: Literal[tuple(factory.supported_tms.list())] = Path( # type: ignore + description="TileMatrixSet Name", ), url: str = Depends(factory.path_dependency), # noqa bandscount: int = Query( @@ -74,7 +61,7 @@ class gdalwmtsExtension(FactoryExtension): ): """Return a GDAL WMTS Service description.""" route_params = { - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } wmts_url = factory.url_for(request, "wmts", **route_params) @@ -151,7 +138,7 @@ add_exception_handlers(app, DEFAULT_STATUS_CODES) ```python from rio_tiler.io import Reader -with Reader("http://0.0.0.0/wmts.xml?url=file.tif&bidx=1&bandscount=1&datatype=float32&tile_format=tif") as src: +with Reader("http://0.0.0.0/WebMercatorQuad/wmts.xml?url=file.tif&bidx=1&bandscount=1&datatype=float32&tile_format=tif") as src: im = src.preview() ``` diff --git a/docs/src/examples/code/img/example_custom_layers_docs.png b/docs/src/examples/code/img/example_custom_layers_docs.png new file mode 100644 index 000000000..b7176a650 Binary files /dev/null and b/docs/src/examples/code/img/example_custom_layers_docs.png differ diff --git a/docs/src/examples/code/img/example_custom_layers_preview.png b/docs/src/examples/code/img/example_custom_layers_preview.png new file mode 100644 index 000000000..f646d61c8 Binary files /dev/null and b/docs/src/examples/code/img/example_custom_layers_preview.png differ diff --git a/docs/src/examples/code/img/stac_xarray_docs.png b/docs/src/examples/code/img/stac_xarray_docs.png new file mode 100644 index 000000000..662f4ce82 Binary files /dev/null and b/docs/src/examples/code/img/stac_xarray_docs.png differ diff --git a/docs/src/examples/code/img/stac_xarray_map.png b/docs/src/examples/code/img/stac_xarray_map.png new file mode 100644 index 000000000..136d67a02 Binary files /dev/null and b/docs/src/examples/code/img/stac_xarray_map.png differ diff --git a/docs/src/examples/code/img/stac_xarray_tile.png b/docs/src/examples/code/img/stac_xarray_tile.png new file mode 100644 index 000000000..81cc36782 Binary files /dev/null and b/docs/src/examples/code/img/stac_xarray_tile.png differ diff --git a/docs/src/examples/code/mosaic_from_urls.md b/docs/src/examples/code/mosaic_from_urls.md index 28f9e5913..31d8a1948 100644 --- a/docs/src/examples/code/mosaic_from_urls.md +++ b/docs/src/examples/code/mosaic_from_urls.md @@ -20,30 +20,24 @@ app/backends.py from typing import Type, List, Tuple, Dict, Union import attr -from rio_tiler.io import BaseReader, COGReader, MultiBandReader, MultiBaseReader +from rio_tiler.io import BaseReader, Reader, MultiBaseReader from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS +from rio_tiler.mosaic.backend import BaseBackend from rasterio.crs import CRS from morecantile import TileMatrixSet -from cogeo_mosaic.backends.base import BaseBackend -from cogeo_mosaic.mosaic import MosaicJSON - @attr.s class MultiFilesBackend(BaseBackend): - input: List[str] = attr.ib() - - reader: Union[ - Type[BaseReader], - Type[MultiBaseReader], - Type[MultiBandReader], - ] = attr.ib(default=COGReader) - reader_options: Dict = attr.ib(factory=dict) + input: list[str] = attr.ib() + tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) - geographic_crs: CRS = attr.ib(default=WGS84_CRS) + reader: type[BaseReader] | type[MultiBaseReader] = ( + attr.ib(default=Reader) + ) + reader_options: dict = attr.ib(factory=dict) - tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) minzoom: int = attr.ib(default=0) maxzoom: int = attr.ib(default=30) @@ -53,51 +47,33 @@ class MultiFilesBackend(BaseBackend): ) crs: CRS = attr.ib(init=False, default=WGS84_CRS) - # mosaic_def is outside the __init__ method - mosaic_def: MosaicJSON = attr.ib(init=False) - - _backend_name = "MultiFiles" - - def __attrs_post_init__(self): - """Post Init.""" - # Construct a FAKE/Empty mosaicJSON - # mosaic_def has to be defined. - self.mosaic_def = MosaicJSON( - mosaicjson="0.0.2", - name="it's fake but it's ok", - minzoom=self.minzoom, - maxzoom=self.maxzoom, - tiles=[] # we set `tiles` to an empty list. - ) - - def write(self, overwrite: bool = True): - """This method is not used but is required by the abstract class.""" - pass - - def update(self): - """We overwrite the default method.""" - pass - - def _read(self) -> MosaicJSON: - """This method is not used but is required by the abstract class.""" - pass - - def assets_for_tile(self, x: int, y: int, z: int) -> List[str]: + def assets_for_tile(self, x: int, y: int, z: int) -> list[str]: """Retrieve assets for tile.""" return self.get_assets() - def assets_for_point(self, lng: float, lat: float) -> List[str]: + def assets_for_point(self, lng: float, lat: float) -> list[str]: """Retrieve assets for point.""" return self.get_assets() - def get_assets(self) -> List[str]: + def assets_for_bbox( + self, + left: float, + bottom: float, + right: float, + top: float, + coord_crs: CRS | None = None, + **kwargs, + ) -> list[str]: + """Retrieve assets for bbox.""" + return self.get_assets() + + def get_assets(self) -> list[str]: """assets are just files we give in path""" return self.input @property def _quadkeys(self) -> List[str]: return [] - ``` 2 - Create endpoints @@ -105,7 +81,7 @@ class MultiFilesBackend(BaseBackend): ```python """routes. -app/router.py +app/routers.py """ @@ -136,7 +112,7 @@ def DatasetPathParams(url: str = Query(..., description="Dataset URL")) -> List[ return url.split(",") -mosaic = MosaicTiler(reader=MultiFilesBackend, path_dependency=DatasetPathParams) +mosaic = MosaicTiler(backend=MultiFilesBackend, path_dependency=DatasetPathParams) ``` diff --git a/docs/src/examples/code/tiler_for_sentinel2.md b/docs/src/examples/code/tiler_for_sentinel2.md deleted file mode 100644 index d0e0256c3..000000000 --- a/docs/src/examples/code/tiler_for_sentinel2.md +++ /dev/null @@ -1,244 +0,0 @@ -**Goal**: Create a dynamic tiler for Sentinel-2 (using AWS Public Dataset) - -**requirements**: titiler.core, titiler.mosaic, rio-tiler-pds - -Note: See https://github.com/developmentseed/titiler-pds for a end-to-end implementation - -### Sentinel 2 - -Thanks to Digital Earth Africa and in collaboration with Sinergise, Element 84, Amazon Web Services (AWS) and the Committee on Earth Observation Satellites (CEOS), Sentinel 2 (Level 2) data over Africa, usually stored as JPEG2000, has been translated to COG. More importantly, a STAC database and API has been set up. - -https://www.digitalearthafrica.org/news/operational-and-ready-use-satellite-data-now-available-across-africa - -The API is provided by [@element84](https://www.element84.com) and follows the latest specification: https://earth-search.aws.element84.com/v0 - - -```python -"""Sentinel 2 (COG) Tiler.""" - -from titiler.core.factory import MultiBandTilerFactory -from titiler.core.dependencies import BandsExprParams -from titiler.mosaic.factory import MosaicTilerFactory - -from rio_tiler_pds.sentinel.aws import S2COGReader -from rio_tiler_pds.sentinel.utils import s2_sceneid_parser - -from fastapi import FastAPI, Query - - -def CustomPathParams( - sceneid: str = Query(..., description="Sentinel 2 Sceneid.") -): - """Create dataset path from args""" - assert s2_sceneid_parser(sceneid) # Makes sure the sceneid is valid - return sceneid - - -app = FastAPI() - -scene_tiler = MultiBandTilerFactory(reader=S2COGReader, path_dependency=CustomPathParams, router_prefix="scenes") -app.include_router(scene_tiler.router, prefix="/scenes", tags=["scenes"]) - -mosaic_tiler = MosaicTilerFactory( - router_prefix="mosaic", - dataset_reader=S2COGReader, - layer_dependency=BandsExprParams, -) -app.include_router(mosaic_tiler.router, prefix="/mosaic", tags=["mosaic"]) -``` - - -### How to - -1. Search for Data -```python -import os -import json -import base64 -import httpx -import datetime -import itertools -import urllib.parse -import pathlib - -from io import BytesIO -from functools import partial -from concurrent import futures - -from rasterio.plot import reshape_as_image -from rasterio.features import bounds as featureBounds - -# Endpoint variables -titiler_endpoint = "http://127.0.0.1:8000" -stac_endpoint = "https://earth-search.aws.element84.com/v0/search" - -# Make sure both are up -assert httpx.get(f"{titiler_endpoint}/docs").status_code == 200 -assert httpx.get(stac_endpoint).status_code == 200 - -geojson = { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "properties": {}, - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - -2.83447265625, - 4.12728532324537 - ], - [ - 2.120361328125, - 4.12728532324537 - ], - [ - 2.120361328125, - 8.254982704877875 - ], - [ - -2.83447265625, - 8.254982704877875 - ], - [ - -2.83447265625, - 4.12728532324537 - ] - ] - ] - } - } - ] -} - -bounds = featureBounds(geojson) - -start = datetime.datetime.strptime("2019-01-01", "%Y-%m-%d").strftime("%Y-%m-%dT00:00:00Z") -end = datetime.datetime.strptime("2019-12-11", "%Y-%m-%d").strftime("%Y-%m-%dT23:59:59Z") - -# POST body -query = { - "collections": ["sentinel-s2-l2a-cogs"], - "datetime": f"{start}/{end}", - "query": { - "eo:cloud_cover": { - "lt": 3 - }, - "sentinel:data_coverage": { - "gt": 10 - } - }, - "intersects": geojson["features"][0]["geometry"], - "limit": 1000, - "fields": { - 'include': ['id', 'properties.datetime', 'properties.eo:cloud_cover'], # This will limit the size of returned body - 'exclude': ['assets', 'links'] # This will limit the size of returned body - }, - "sortby": [ - { - "field": "properties.eo:cloud_cover", - "direction": "desc" - }, - ] -} - -# POST Headers -headers = { - "Content-Type": "application/json", - "Accept-Encoding": "gzip", - "Accept": "application/geo+json", -} - -data = httpx.post(stac_endpoint, headers=headers, json=query).json() -print("Results context:") -print(data["context"]) - -sceneid = [f["id"] for f in data["features"]] -cloudcover = [f["properties"]["eo:cloud_cover"] for f in data["features"]] -dates = [f["properties"]["datetime"][0:10] for f in data["features"]] -``` - -2. Get TileJSON -```python -# Fetch TileJSON -# For this example we use the first `sceneid` returned from the STAC API -# and we sent the Bands to B04,B03,B02 which are red,green,blue -data = httpx.get(f"{titiler_endpoint}/scenes/tilejson.json?sceneid={sceneid[4]}&bands=B04&bands=B03&bands=B02&rescale=0,2000").json() -print(data) -``` - -3. Mosaic - -```python -from cogeo_mosaic.backends import MosaicBackend -from typing import Dict, List, Sequence, Optional -from pygeos import polygons -import mercantile - -# Simple Mosaic -def custom_accessor(feature): - """Return feature identifier.""" - return feature["id"] - -with MosaicBackend( - "stac+https://earth-search.aws.element84.com/v0/search", - query, - minzoom=8, - maxzoom=15, - mosaic_options={"accessor": custom_accessor}, -) as mosaic: - print(mosaic.metadata) - mosaic_doc = mosaic.mosaic_def.dict(exclude_none=True) - -# Optimized Mosaic -def optimized_filter( - tile: mercantile.Tile, # noqa - dataset: Sequence[Dict], - geoms: Sequence[polygons], - minimum_tile_cover=None, # noqa - tile_cover_sort=False, # noqa - maximum_items_per_tile: Optional[int] = None, -) -> List: - """Optimized filter that keeps only one item per grid ID.""" - gridid: List[str] = [] - selected_dataset: List[Dict] = [] - - for item in dataset: - grid = item["id"].split("_")[1] - if grid not in gridid: - gridid.append(grid) - selected_dataset.append(item) - - dataset = selected_dataset - - indices = list(range(len(dataset))) - if maximum_items_per_tile: - indices = indices[:maximum_items_per_tile] - - return [dataset[ind] for ind in indices] - - -with MosaicBackend( - "stac+https://earth-search.aws.element84.com/v0/search", - query, - minzoom=8, - maxzoom=14, - mosaic_options={"accessor": custom_accessor, "asset_filter": optimized_filter}, -) as mosaic: - print(mosaic.metadata) - mosaic_doc = mosa - -# Write the mosaic -mosaic_file = "mymosaic.json.gz" -with MosaicBackend(mosaic_file, mosaic_def=mosaic_doc) as mosaic: - mosaic.write(overwrite=True) -``` - -Use the mosaic in titiler -```python -mosaic = str(pathlib.Path(mosaic_file).absolute()) -data = httpx.get(f"{titiler_endpoint}/mosaic/tilejson.json?url=file:///{mosaic}&bands=B01&rescale=0,1000").json() -print(data) -``` diff --git a/docs/src/examples/code/tiler_with_auth.md b/docs/src/examples/code/tiler_with_auth.md index b45acf690..801499d42 100644 --- a/docs/src/examples/code/tiler_with_auth.md +++ b/docs/src/examples/code/tiler_with_auth.md @@ -43,7 +43,7 @@ app/models.py """ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import List, Optional from jose import jwt @@ -67,7 +67,7 @@ class AccessToken(BaseModel): @validator("iat", pre=True, always=True) def set_creation_time(cls, v) -> datetime: """Set token creation time (iat).""" - return datetime.utcnow() + return datetime.now(timezone.UTC) @validator("exp", always=True) def set_expiration_time(cls, v, values) -> datetime: @@ -140,12 +140,12 @@ def DatasetPathParams( """Create dataset path from args""" if not api_key_query: - raise HTTPException(status_code=403, detail="Missing `access_token`") + raise HTTPException(status_code=401, detail="Missing `access_token`") try: AccessToken.from_string(api_key_query) except JWTError: - raise HTTPException(status_code=403, detail="Invalid `access_token`") + raise HTTPException(status_code=401, detail="Invalid `access_token`") return url ``` diff --git a/docs/src/examples/code/tiler_with_cache.md b/docs/src/examples/code/tiler_with_cache.md index 880ed6875..a29d7bd82 100644 --- a/docs/src/examples/code/tiler_with_cache.md +++ b/docs/src/examples/code/tiler_with_cache.md @@ -167,7 +167,7 @@ from rio_tiler.io import BaseReader, Reader from titiler.core.factory import img_endpoint_params from titiler.core.factory import TilerFactory as TiTilerFactory -from titiler.core.dependencies import ImageParams, RescalingParams +from titiler.core.dependencies import RescalingParams from titiler.core.models.mapbox import TileJSON from titiler.core.resources.enums import ImageType @@ -186,15 +186,15 @@ class TilerFactory(TiTilerFactory): @self.router.get(r"/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params) @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params) @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) + @self.router.get(r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params, ) # Add default cache config dictionary into cached alias. @@ -204,7 +204,7 @@ class TilerFactory(TiTilerFactory): z: int = Path(..., ge=0, le=30, description="TMS tiles's zoom level"), x: int = Path(..., description="TMS tiles's column"), y: int = Path(..., description="TMS tiles's row"), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( + tileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( self.default_tms, description=f"TileMatrixSet Name (default: '{self.default_tms}')", ), @@ -235,7 +235,7 @@ class TilerFactory(TiTilerFactory): reader_params=Depends(self.reader_dependency), ): """Create map tile from a dataset.""" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with self.reader(src_path, tms=tms, **reader_params) as src_dst: image = src_dst.tile( @@ -280,7 +280,7 @@ class TilerFactory(TiTilerFactory): response_model_exclude_none=True, ) @self.router.get( - "/{TileMatrixSetId}/tilejson.json", + "/{tileMatrixSetId}/tilejson.json", response_model=TileJSON, responses={200: {"description": "Return a tilejson"}}, response_model_exclude_none=True, @@ -288,7 +288,7 @@ class TilerFactory(TiTilerFactory): @cached(alias="default") def tilejson( request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( + tileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( self.default_tms, description=f"TileMatrixSet Name (default: '{self.default_tms}')", ), @@ -332,7 +332,7 @@ class TilerFactory(TiTilerFactory): "x": "{x}", "y": "{y}", "scale": tile_scale, - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } if tile_format: route_params["format"] = tile_format.value @@ -354,7 +354,7 @@ class TilerFactory(TiTilerFactory): if qs: tiles_url += f"?{urlencode(qs)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with self.reader(src_path, tms=tms, **reader_params) as src_dst: return { "bounds": src_dst.geographic_bounds, diff --git a/docs/src/examples/code/tiler_with_custom_algorithm.md b/docs/src/examples/code/tiler_with_custom_algorithm.md index e606982e6..547d5ffe5 100644 --- a/docs/src/examples/code/tiler_with_custom_algorithm.md +++ b/docs/src/examples/code/tiler_with_custom_algorithm.md @@ -32,7 +32,6 @@ class Multiply(BaseAlgorithm): # Create output ImageData return ImageData( data, - img.mask, assets=img.assets, crs=img.crs, bounds=img.bounds, diff --git a/docs/src/examples/code/tiler_with_custom_colormap.md b/docs/src/examples/code/tiler_with_custom_colormap.md index 8a9e5f86e..77ab08ef8 100644 --- a/docs/src/examples/code/tiler_with_custom_colormap.md +++ b/docs/src/examples/code/tiler_with_custom_colormap.md @@ -15,7 +15,7 @@ cmap = urlencode( } ) response = requests.get( - f"http://127.0.0.1:8000/cog/tiles/8/53/50.png?url=https://myurl.com/cog.tif&bidx=1&rescale=0,10000&{cmap}" + f"http://127.0.0.1:8000/cog/tiles/WebMercatorQuad/8/53/50.png?url=https://myurl.com/cog.tif&bidx=1&rescale=0,10000&{cmap}" ) ``` @@ -33,34 +33,33 @@ app/dependencies.py """ import json -from enum import Enum -from typing import Dict, Optional + +from typing import Annotated, Dict, Optional, Literal import numpy import matplotlib -from rio_tiler.colormap import cmap, parse_color +from rio_tiler.colormap import parse_color +from rio_tiler.colormap import cmap as default_cmap from fastapi import HTTPException, Query -ColorMapName = Enum( # type: ignore - "ColorMapName", [(a, a) for a in sorted(cmap.list())] -) - -class ColorMapType(str, Enum): - """Colormap types.""" - - explicit = "explicit" - linear = "linear" - - def ColorMapParams( - colormap_name: ColorMapName = Query(None, description="Colormap name"), - colormap: str = Query(None, description="JSON encoded custom Colormap"), - colormap_type: ColorMapType = Query(ColorMapType.explicit, description="User input colormap type."), + colormap_name: Annotated[ # type: ignore + Literal[tuple(default_cmap.list())], + Query(description="Colormap name"), + ] = None, + colormap: Annotated[ + str, + Query(description="JSON encoded custom Colormap"), + ] = None, + colormap_type: Annotated[ + Literal["explicit", "linear"], + Query(description="User input colormap type."), + ] = "explicit", ) -> Optional[Dict]: """Colormap Dependency.""" if colormap_name: - return cmap.get(colormap_name.value) + return default_cmap.get(colormap_name) if colormap: try: @@ -73,7 +72,7 @@ def ColorMapParams( status_code=400, detail="Could not parse the colormap value." ) - if colormap_type == ColorMapType.linear: + if colormap_type == "linear": # input colormap has to start from 0 to 255 ? cm = matplotlib.colors.LinearSegmentedColormap.from_list( 'custom', diff --git a/docs/src/examples/code/tiler_with_custom_stac+xarray.md b/docs/src/examples/code/tiler_with_custom_stac+xarray.md new file mode 100644 index 000000000..99eb2eb45 --- /dev/null +++ b/docs/src/examples/code/tiler_with_custom_stac+xarray.md @@ -0,0 +1,260 @@ + +**Goal**: Create a custom STAC Reader supporting both COG and NetCDF/Zarr dataset + +**requirements**: + +- `titiler.core` +- `titiler.xarray` +- `fsspec` +- `zarr` +- `h5netcdf` +- `aiohttp` (optional) +- `s3fs` (optional) + +**links**: + +- https://cogeotiff.github.io/rio-tiler/examples/STAC_datacube_support/ + + +#### 1. Custom STACReader + +First, we need to create a custom `STACReader` which will support both COG and NetCDF/Zarr dataset. The custom parts will be: + +- add `netcdf` and `zarr` as valid asset media types +- introduce a new `md://` prefixed asset form, so users can pass `assets=md://{netcdf asset name}?variable={variable name}` as we do for the `GDAL vrt string connection` support. + +```python title="stac.py" +import attr +from urllib.parse import urlparse, parse_qsl +from rio_tiler.types import AssetInfo +from rio_tiler.io import BaseReader, Reader +from rio_tiler.io.stac import DEFAULT_VALID_TYPE, STAC_ALTERNATE_KEY +from rio_tiler.io.stac import STACReader as BaseSTACReader + +from titiler.xarray.io import FsReader as XarrayReader + +valid_types = { + *DEFAULT_VALID_TYPE, + "application/x-netcdf", + "application/vnd+zarr", +} + + +@attr.s +class STACReader(BaseSTACReader): + """Custom STACReader which adds support for XarrayReader. + + Example: + >>> with STACReader("https://raw.githubusercontent.com/cogeotiff/rio-tiler/refs/heads/main/tests/fixtures/stac_netcdf.json") as src: + print(src.assets) + print(src._get_asset_info("netcdf|variable=dataset")) + + ['geotiff', 'netcdf'] + {'url': 'https://raw.githubusercontent.com/cogeotiff/rio-tiler/refs/heads/main/tests/fixtures/dataset_2d.nc', 'name': 'netcdf', 'metadata': {}, 'reader_options': {'variable': 'dataset'}, 'method_options': {}, 'media_type': 'application/x-netcdf'} + + """ + include_asset_types: set[str] = attr.ib(default=valid_types) + + def _get_reader(self, asset_info: AssetInfo) -> type[BaseReader]: + """Get Asset Reader.""" + asset_type = asset_info.get("media_type", None) + if asset_type and asset_type in [ + "application/x-netcdf", + "application/vnd+zarr", + "application/x-hdf5", + "application/x-hdf", + ]: + return XarrayReader + + return Reader + + def _get_asset_info(self, asset: str) -> AssetInfo: + """Validate asset names and return asset's info. + + Args: + asset (str): STAC asset name. + + Returns: + AssetInfo: STAC asset info. + + """ + asset, vrt_options = self._parse_vrt_asset(asset) + + reader_options: dict[str, Any] = {} + method_options: dict[str, Any] = {} + # NOTE: asset can be in form of + # "{asset_name}|some_option=some_value&another_option=another_value" + if "|" in asset: + asset, params = asset.split("|", 1) + # NOTE: Construct method options from params + if params: + for param in params.split("&"): + key, value = param.split("=", 1) + if key == "indexes": + method_options["indexes"] = list(map(int, value.split(","))) + elif key == "expression": + method_options["expression"] = value + # XarrayReader Reader-Options + elif key == "variable": + reader_options["variable"] = value + elif key == "group": + reader_options["group"] = value + elif key == "decode_times": + reader_options["decode_times"] = value.lower() in ["true", "yes", "1"] + elif key == "datetime": + reader_options["datetime"] = value + elif key == "drop_dim": + reader_options["drop_dim"] = value + + if asset not in self.assets: + raise InvalidAssetName( + f"'{asset}' is not valid, should be one of {self.assets}" + ) + + asset_info = self.item.assets[asset] + extras = asset_info.extra_fields + + info = AssetInfo( + url=asset_info.get_absolute_href() or asset_info.href, + name=asset, + metadata=extras if not vrt_options else None, + reader_options=reader_options, + method_options=method_options, + ) + + if STAC_ALTERNATE_KEY and extras.get("alternate"): + if alternate := extras["alternate"].get(STAC_ALTERNATE_KEY): + info["url"] = alternate["href"] + + if asset_info.media_type: + info["media_type"] = asset_info.media_type + + # https://github.com/stac-extensions/file + if head := extras.get("file:header_size"): + info["env"] = {"GDAL_INGESTED_BYTES_AT_OPEN": head} + + # https://github.com/stac-extensions/raster + if extras.get("raster:bands") and not vrt_options: + bands = extras.get("raster:bands") + stats = [ + (b["statistics"]["minimum"], b["statistics"]["maximum"]) + for b in bands + if {"minimum", "maximum"}.issubset(b.get("statistics", {})) + ] + # check that stats data are all double and make warning if not + if ( + stats + and all(isinstance(v, (int, float)) for stat in stats for v in stat) + and len(stats) == len(bands) + ): + info["dataset_statistics"] = stats + else: + warnings.warn( + "Some statistics data in STAC are invalid, they will be ignored." + ) + + if vrt_options: + info["url"] = f"vrt://{info['url']}?{vrt_options}" + + return info +``` + +#### 2. Application + +```python title="main.py" +"""FastAPI application.""" +from fastapi import FastAPI +from titiler.core.factory import MultiBaseTilerFactory +from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers + +from .stac import STACReader + +# STAC uses MultiBaseReader so we use MultiBaseTilerFactory to built the default endpoints +stac = MultiBaseTilerFactory( + reader=STACReader, + add_preview=False, + add_ogc_maps=False, +) + +# Create FastAPI application +app = FastAPI() +app.include_router(stac.router) +add_exception_handlers(app, DEFAULT_STATUS_CODES) +``` + +``` +uvicorn app:app --port 8080 --reload +``` + + + +##### Available Assets + +```bash +curl http://127.0.0.1:8080/assets\?url\=https%3A%2F%2Fraw.githubusercontent.com%2Fcogeotiff%2Frio-tiler%2Frefs%2Fheads%2Fmain%2Ftests%2Ffixtures%2Fstac_netcdf.json | jq + +[ + "geotiff", + "netcdf" +] +``` + +##### Info + +```bash +curl http://127.0.0.1:8080/info?url=https://raw.githubusercontent.com/cogeotiff/rio-tiler/refs/heads/main/tests/fixtures/stac_netcdf.json&assets=netcdf|variable=dataset | jq +{ + "netcdf|variable=dataset": { + "bounds": [ + -170.085, + -80.08, + 169.914999999975, + 79.91999999999659 + ], + "crs": "http://www.opengis.net/def/crs/EPSG/0/4326", + "band_metadata": [ + [ + "b1", + {} + ] + ], + "band_descriptions": [ + [ + "b1", + "dataset" + ] + ], + "dtype": "float64", + "nodata_type": "Nodata", + "name": "dataset", + "count": 1, + "width": 2000, + "height": 1000, + "dimensions": [ + "y", + "x" + ], + "attrs": { + "valid_min": 1.0, + "valid_max": 1000.0, + "fill_value": 0 + } + } +} +``` + +##### Map.html + +``` +http://127.0.0.1:8080/WebMercatorQuad/map.html?url=https://raw.githubusercontent.com/cogeotiff/rio-tiler/refs/heads/main/tests/fixtures/stac_netcdf.json&assets=netcdf|variable=dataset&rescale=0,1000&colormap_name=viridis +``` + + + +##### Tile Request + +``` +http://127.0.0.1:8080/tiles/WebMercatorQuad/1/0/0?url=https://raw.githubusercontent.com/cogeotiff/rio-tiler/refs/heads/main/tests/fixtures/stac_netcdf.json&assets=netcdf|variable=dataset&rescale=0,1000 +``` + + diff --git a/docs/src/examples/code/tiler_with_custom_tms.md b/docs/src/examples/code/tiler_with_custom_tms.md index eb4edacc1..0813dee9c 100644 --- a/docs/src/examples/code/tiler_with_custom_tms.md +++ b/docs/src/examples/code/tiler_with_custom_tms.md @@ -20,15 +20,14 @@ from pyproj import CRS EPSG6933 = TileMatrixSet.custom( (-17357881.81713629, -7324184.56362408, 17357881.81713629, 7324184.56362408), CRS.from_epsg(6933), - identifier="EPSG6933", + id="EPSG6933", matrix_scale=[1, 1], ) - # 2. Register TMS -tms = tms.register([EPSG6933]) +tms = tms.register({EPSG6933.id:EPSG6933}) -tms = TMSFactory(supported_tms=tms) -cog = TilerFactory(supported_tms=tms) +tms_factory = TMSFactory(supported_tms=tms) +cog_factory = TilerFactory(supported_tms=tms) ``` 2 - Create app and register our custom endpoints @@ -44,11 +43,11 @@ from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers from fastapi import FastAPI -from .routes import cog, tms +from .routes import cog_factory, tms_factory app = FastAPI(title="My simple app with custom TMS") -app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) -app.include_router(tms.router, tags=["Tiling Schemes"]) +app.include_router(cog_factory.router, tags=["Cloud Optimized GeoTIFF"]) +app.include_router(tms_factory.router, tags=["Tiling Schemes"]) add_exception_handlers(app, DEFAULT_STATUS_CODES) ``` diff --git a/docs/src/examples/code/tiler_with_layers.md b/docs/src/examples/code/tiler_with_layers.md new file mode 100644 index 000000000..1465ba05d --- /dev/null +++ b/docs/src/examples/code/tiler_with_layers.md @@ -0,0 +1,243 @@ + +**Goal**: Create a Custom TiTiler with a limited set of supported `layers` + +**requirements**: titiler.core + +**How**: + +The idea is to create a set of endpoints with a `/layers/{layer_id}` prefix and a set of configuration, e.g + +``` +config = { + "layer_1": { + "url": "dataset_1 url", + "indexes": [1], + "render": { + "rescale": [(0, 1000)], + "colormap_name": "viridis" + } + }, + ... +} +``` + +We then use custom set of endpoint dependencies to get the `layer` configuration and `inject` the parameters. + + +```python +import json +from dataclasses import dataclass, field +from typing import Dict, Literal, Annotated, Optional, Sequence + +from fastapi import FastAPI, Path, HTTPException, Query +from rio_tiler.colormap import ColorMaps +from rio_tiler.colormap import cmap as default_cmap +from rio_tiler.colormap import parse_color +from starlette.requests import Request + +from titiler.core import dependencies +from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers +from titiler.core.factory import TilerFactory + + +# Layers Configuration +available_layers = { + "red": { + "url": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B04.tif", + "render": { + "rescale": [ + (0, 1000), + ], + }, + }, + "green": { + "url": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B03.tif", + "render": { + "rescale": [ + (0, 1000), + ], + }, + }, + "bleue": { + "url": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B02.tif", + "render": { + "rescale": [ + (0, 1000), + ], + }, + }, +} + +# VRT of bands B04, B03, B02, and B05 files +# gdalbuildvrt vrt.vrt /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B{04,03,02,08}.tif -separate +# cat vrt.vrt | tr -d '\n' | tr -d ' ' +vrt_rdbnir = ' PROJCS["WGS 84 / UTM zone 21S",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-57],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32721"]] 6.0000000000000000e+05, 1.0000000000000000e+01, 0.0000000000000000e+00, 7.3000000000000000e+06, 0.0000000000000000e+00, -1.0000000000000000e+01 0 /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B04.tif 1 0 0 /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B03.tif 1 0 0 /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B02.tif 1 0 0 /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B08.tif 1 0 ' + +# Mpre configs (using VRT) +available_layers.update( + { + "true_color": { + "url": vrt_rdbnir, + "indexes": [1, 2, 3], + "render": { + "rescale": [ + (0, 3000), + (0, 3000), + (0, 3000), + ], + }, + }, + "false_color": { + "url": vrt_rdbnir, + "indexes": [4, 1, 2], + "render": { + "rescale": [ + (0, 4000), + (0, 3000), + (0, 3000), + ], + }, + }, + "ndvi": { + "url": vrt_rdbnir, + "expression": "(b4-b1)/(b4+b1)", + "render": { + "rescale": [ + (-1, 1), + ], + "colormap_name": "viridis", + }, + }, + } +) + +# List of all Layers +layers_list = Literal["red", "green", "blue", "true_color", "false_color", "ndvi"] + + +# Custom `DatasetPathParams` which return the dataset URL for a `layer_id` +def DatasetPathParams(layer_id: layers_list = Path()) -> str: + return available_layers[layer_id]["url"] + + +@dataclass +class CustomAsDict: + """Custom `DefaultDependency` to ignore `requests`""" + def as_dict(self, exclude_none: bool = True) -> Dict: + """Transform dataclass to dict.""" + exclude_keys = {"request"} + if exclude_none: + return { + k: v + for k, v in self.__dict__.items() + if v is not None and k not in exclude_keys + } + + return {k: v for k, v in self.__dict__.items() if k not in exclude_keys} + + +# Custom Layer Param +@dataclass +class LayerParams(CustomAsDict, dependencies.BidxExprParams): + + request: Request = field(default=None) + + def __post_init__(self): + if (layer := self.request.path_params.get("layer_id")) and not any( + [self.indexes, self.expression] + ): + layer_params = available_layers[layer] + if indexes := layer_params.get("indexes"): + self.indexes = indexes + elif expr := layer_params.get("expression"): + self.expression = expr + +# Custom Rendering Params +@dataclass +class RenderingParams(CustomAsDict, dependencies.ImageRenderingParams): + + request: Request = field(default=None) + + def __post_init__(self): + super().__post_init__() + + if layer := self.request.path_params.get("layer_id"): + layer_params = available_layers[layer].get("render", {}) + + if not self.rescale and (rescale := layer_params.get("rescale")): + self.rescale = rescale + + if not self.color_formula and (color_formula := layer_params.get("color_formula")): + self.color_formula = color_formula + + if self.add_mask is not None and (add_mask := layer_params.get("add_mask")): + self.add_mask = add_mask + + +# Custom ColorMap Params +def ColorMapParams( + request: Request, + colormap_name: Annotated[ # type: ignore + Literal[tuple(default_cmap.list())], + Query(description="Colormap name"), + ] = None, + colormap: Annotated[ + Optional[str], Query(description="JSON encoded custom Colormap") + ] = None, +): + if layer := request.path_params.get("layer_id"): + layer_params = available_layers[layer].get("render", {}) + colormap_name = layer_params.get("colormap_name", colormap_name) + colormap = layer_params.get("colormap", colormap) + + if colormap_name: + return default_cmap.get(colormap_name) + + if colormap: + try: + c = json.loads( + colormap, + object_hook=lambda x: { + int(k): parse_color(v) for k, v in x.items() + }, + ) + + # Make sure to match colormap type + if isinstance(c, Sequence): + c = [(tuple(inter), parse_color(v)) for (inter, v) in c] + + return c + except json.JSONDecodeError as e: + raise HTTPException( + status_code=400, detail="Could not parse the colormap value." + ) from e + + return None + + +app = FastAPI() + +cog = TilerFactory( + path_dependency=DatasetPathParams, + layer_dependency=LayerParams, + render_dependency=RenderingParams, + colormap_dependency=ColorMapParams, + router_prefix="/layers/{layer_id}", +) +app.include_router(cog.router, prefix="/layers/{layer_id}") +add_exception_handlers(app, DEFAULT_STATUS_CODES) + +# Run the application +import uvicorn +uvicorn.run(app=app, host="127.0.0.1", port=8080, log_level="info") +``` + + +`http://127.0.0.1:8080/docs` + +![](img/example_custom_layers_docs.png) + + +`http://127.0.0.1:8080/layers/true_color/preview` + +![](img/example_custom_layers_preview.png) diff --git a/docs/src/examples/notebooks/Working_with_Algorithm.ipynb b/docs/src/examples/notebooks/Working_with_Algorithm.ipynb index cb0fe237c..b25ca3d2f 100644 --- a/docs/src/examples/notebooks/Working_with_Algorithm.ipynb +++ b/docs/src/examples/notebooks/Working_with_Algorithm.ipynb @@ -21,7 +21,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "id": "55915667", "metadata": {}, "outputs": [], @@ -34,17 +34,19 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "3ac532e8", "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind." + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "5c65b3d5", "metadata": {}, "outputs": [], @@ -62,25 +64,17 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "7abeb9c0", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'bounds': [7.090624928537461, 45.916058441028206, 7.1035698381384185, 45.925093000254144], 'minzoom': 15, 'maxzoom': 18, 'band_metadata': [['b1', {'STATISTICS_COVARIANCES': '10685.98787505646', 'STATISTICS_EXCLUDEDVALUES': '-9999', 'STATISTICS_MAXIMUM': '2015.0944824219', 'STATISTICS_MEAN': '1754.471184271', 'STATISTICS_MINIMUM': '1615.8128662109', 'STATISTICS_SKIPFACTORX': '1', 'STATISTICS_SKIPFACTORY': '1', 'STATISTICS_STDDEV': '103.37305197708'}]], 'band_descriptions': [['b1', '']], 'dtype': 'float32', 'nodata_type': 'Nodata', 'colorinterp': ['gray'], 'width': 2000, 'nodata_value': -9999.0, 'overviews': [2, 4, 8], 'count': 1, 'height': 2000, 'driver': 'GTiff'}\n" - ] - } - ], + "outputs": [], "source": [ "# Fetch dataset Metadata\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/info\",\n", - " params = {\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "print(r)" @@ -98,226 +92,57 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "80803e00", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"]\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"],\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Office fédéral de topographie swisstopo\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(\n", + " tiles=r[\"tiles\"][0], opacity=1, attr=\"Office fédéral de topographie swisstopo\"\n", + ").add_to(m)\n", "m" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "id": "64c2faab", "metadata": { "scrolled": false }, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", " # rio-tiler cannot rescale automatically the data when using a colormap\n", " \"rescale\": \"1615.812,2015.09448\",\n", " \"colormap_name\": \"terrain\",\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"]\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"],\n", ")\n", "\n", "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Office fédéral de topographie swisstopo\"\n", + " tiles=r[\"tiles\"][0], opacity=1, attr=\"Office fédéral de topographie swisstopo\"\n", ")\n", "aod_layer.add_to(m)\n", "m" @@ -333,29 +158,12 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "id": "26ef9eef", "metadata": { "scrolled": true }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Available algorithm\n", - "['hillshade', 'contours', 'normalizedIndex', 'terrarium', 'terrainrgb']\n", - "\n", - "Metadata from `Hillshade` algorithm\n", - "Inputs\n", - "{'nbands': 1}\n", - "Outputs\n", - "{'nbands': 1, 'dtype': 'uint8'}\n", - "Parameters\n", - "{'azimuth': {'title': 'Azimuth', 'default': 90, 'type': 'integer'}, 'angle_altitude': {'title': 'Angle Altitude', 'default': 90, 'type': 'number'}, 'buffer': {'title': 'Buffer', 'default': 3, 'type': 'integer'}}\n" - ] - } - ], + "outputs": [], "source": [ "# Fetch algorithms\n", "print(\"Available algorithm\")\n", @@ -381,117 +189,31 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "id": "4cc8c900", "metadata": { "scrolled": false }, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", " \"algorithm\": \"hillshade\",\n", - " # Hillshade algorithm use a 3pixel buffer so we need \n", + " # Hillshade algorithm use a 3pixel buffer so we need\n", " # to tell the tiler to apply a 3 pixel buffer around each tile\n", " \"buffer\": 3,\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"]\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"],\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Yo!!\"\n", - ")\n", + "aod_layer = TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Yo!!\")\n", "aod_layer.add_to(m)\n", "m" ] @@ -506,241 +228,67 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "id": "54d674e9", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", " \"algorithm\": \"contours\",\n", " \"algorithm_params\": json.dumps(\n", " {\n", - " \"increment\": 20, # contour line every 20 meters\n", - " \"thickness\": 2, # 2m thickness\n", + " \"increment\": 20, # contour line every 20 meters\n", + " \"thickness\": 2, # 2m thickness\n", " \"minz\": 1600,\n", - " \"maxz\": 2000\n", + " \"maxz\": 2000,\n", " }\n", " ),\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"]\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"],\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Yo!!\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Yo!!\").add_to(m)\n", "m" ] }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "id": "1c80efe0", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", " \"algorithm\": \"contours\",\n", " \"algorithm_params\": json.dumps(\n", " {\n", - " \"increment\": 5, # contour line every 5 meters\n", - " \"thickness\": 1, # 1m thickness\n", + " \"increment\": 5, # contour line every 5 meters\n", + " \"thickness\": 1, # 1m thickness\n", " \"minz\": 1600,\n", - " \"maxz\": 2000\n", + " \"maxz\": 2000,\n", " }\n", " ),\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"]\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"],\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Yo!!\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Yo!!\").add_to(m)\n", "m" ] }, @@ -755,9 +303,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 with Fil", + "display_name": "titiler (3.13.9)", "language": "python", - "name": "filprofile" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -769,12 +317,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13 (main, May 24 2022, 21:13:51) \n[Clang 13.1.6 (clang-1316.0.21.2)]" - }, - "vscode": { - "interpreter": { - "hash": "8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1" - } + "version": "3.13.9" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF.ipynb b/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF.ipynb index 94b3be3bc..dcc679ad8 100644 --- a/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF.ipynb +++ b/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF.ipynb @@ -42,6 +42,7 @@ "outputs": [], "source": [ "import os\n", + "import datetime\n", "import json\n", "import urllib.parse\n", "from io import BytesIO\n", @@ -49,7 +50,7 @@ "from concurrent import futures\n", "\n", "import httpx\n", - "\n", + "import numpy\n", "from boto3.session import Session as boto3_session\n", "\n", "from rasterio.plot import reshape_as_image\n", @@ -57,9 +58,10 @@ "\n", "from tqdm.notebook import tqdm\n", "\n", - "from folium import Map, TileLayer\n", + "from folium import Map, TileLayer, GeoJson\n", "\n", - "%pylab inline" + "import matplotlib.pyplot as plt\n", + "import matplotlib.dates as mdates" ] }, { @@ -68,7 +70,9 @@ "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind." + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")" ] }, { @@ -86,40 +90,25 @@ "source": [ "# use geojson.io\n", "geojson = {\n", - " \"type\": \"FeatureCollection\",\n", - " \"features\": [\n", - " {\n", - " \"type\": \"Feature\",\n", - " \"properties\": {},\n", - " \"geometry\": {\n", - " \"type\": \"Polygon\",\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " -74.1796875,\n", - " 45.18978009667531\n", - " ],\n", - " [\n", - " -73.092041015625,\n", - " 45.18978009667531\n", - " ],\n", - " [\n", - " -73.092041015625,\n", - " 46.00459325574482\n", - " ],\n", - " [\n", - " -74.1796875,\n", - " 46.00459325574482\n", - " ],\n", - " [\n", - " -74.1796875,\n", - " 45.18978009667531\n", - " ]\n", - " ]\n", - " ]\n", - " }\n", - " }\n", - " ]\n", + " \"type\": \"FeatureCollection\",\n", + " \"features\": [\n", + " {\n", + " \"type\": \"Feature\",\n", + " \"properties\": {},\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [-74.1796875, 45.18978009667531],\n", + " [-73.092041015625, 45.18978009667531],\n", + " [-73.092041015625, 46.00459325574482],\n", + " [-74.1796875, 46.00459325574482],\n", + " [-74.1796875, 45.18978009667531],\n", + " ]\n", + " ],\n", + " },\n", + " }\n", + " ],\n", "}\n", "\n", "bounds = featureBounds(geojson)" @@ -131,11 +120,14 @@ "metadata": {}, "outputs": [], "source": [ - "Map(\n", + "m = Map(\n", " tiles=\"OpenStreetMap\",\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=6\n", - ")" + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=6,\n", + ")\n", + "\n", + "GeoJson(geojson).add_to(m)\n", + "m" ] }, { @@ -169,22 +161,29 @@ "session = boto3_session(region_name=\"us-west-2\")\n", "client = session.client(\"s3\")\n", "\n", - "bucket = \"omi-no2-nasa\" #https://registry.opendata.aws/omi-no2-nasa/\n", + "bucket = \"omi-no2-nasa\" # https://registry.opendata.aws/omi-no2-nasa/\n", "\n", "\n", "def list_objects(bucket, prefix):\n", " \"\"\"AWS s3 list objects.\"\"\"\n", "\n", - " paginator = client.get_paginator('list_objects_v2')\n", + " paginator = client.get_paginator(\"list_objects_v2\")\n", "\n", " files = []\n", " for subset in paginator.paginate(Bucket=bucket, Prefix=prefix):\n", " files.extend(subset.get(\"Contents\", []))\n", "\n", - " return [r[\"Key\"] for r in files]\n", + " return files\n", + "\n", + "\n", + "list_files = list_objects(bucket, \"OMI-Aura_L3\")\n", + "\n", + "print(\"Archive Size\")\n", + "files = [r[\"Key\"] for r in list_files]\n", + "print(f\"Found {len(files)} OMI-NO2 files\")\n", "\n", - "files = list_objects(bucket, \"OMI-Aura_L3\")\n", - "print(f\"Found : {len(files)}\")" + "size = sum([r[\"Size\"] / 1000000.0 for r in list_files])\n", + "print(f\"Size of the archive: {size} Mo ({size / 1000} Go)\")" ] }, { @@ -227,7 +226,11 @@ }, "outputs": [], "source": [ - "files_Oct5 = list(filter(lambda x: (x.split(\"_\")[2][5:7] == \"10\") & (x.split(\"_\")[2][7:9] == \"05\"), files))\n", + "files_Oct5 = list(\n", + " filter(\n", + " lambda x: (x.split(\"_\")[2][5:7] == \"10\") & (x.split(\"_\")[2][7:9] == \"05\"), files\n", + " )\n", + ")\n", "print(len(files_Oct5))\n", "print(files_Oct5)" ] @@ -238,10 +241,10 @@ "source": [ "### DATA Endpoint\n", "\n", - "`{endpoint}/cog/tiles/{z}/{x}/{y}.{format}?url={cog}&{otherquery params}`\n", + "`{endpoint}/cog/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}?url={cog}&{otherquery params}`\n", "\n", "\n", - "`{endpoint}/cog/crop/{minx},{miny},{maxx},{maxy}.{format}?url={cog}&{otherquery params}`\n", + "`{endpoint}/cog/bbox/{minx},{miny},{maxx},{maxy}.{format}?url={cog}&{otherquery params}`\n", "\n", "\n", "`{endpoint}/cog/point/{minx},{miny}?url={cog}&{otherquery params}`\n" @@ -275,10 +278,7 @@ "# Fetch File Metadata to get min/max rescaling values (because the file is stored as float32)\n", "\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/statistics\",\n", - " params = {\n", - " \"url\": _url(files[0])\n", - " }\n", + " f\"{titiler_endpoint}/cog/statistics\", params={\"url\": _url(files[0])}\n", ").json()\n", "\n", "print(json.dumps(r, indent=4))" @@ -291,25 +291,24 @@ "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": _url(files[2]),\n", " \"rescale\": \"0,3000000000000000\",\n", " \"colormap_name\": \"viridis\",\n", - " }\n", + " },\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=6\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=6\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"NASA\"\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"NASA\").add_to(m)\n", + "\n", + "GeoJson(geojson, style_function=lambda feature: {\"fill\": False, \"color\": \"red\"}).add_to(\n", + " m\n", ")\n", - "aod_layer.add_to(m)\n", + "\n", "m" ] }, @@ -334,8 +333,9 @@ "\n", "xmin, ymin, xmax, ymax = bounds\n", "\n", + "\n", "def fetch_bbox(file):\n", - " url = f\"{titiler_endpoint}/cog/crop/{xmin},{ymin},{xmax},{ymax}.npy\"\n", + " url = f\"{titiler_endpoint}/cog/bbox/{xmin},{ymin},{xmax},{ymax}.npy\"\n", " params = {\n", " \"url\": _url(file),\n", " \"bidx\": \"1\",\n", @@ -344,7 +344,10 @@ " r = httpx.get(url, params=params)\n", " data = numpy.load(BytesIO(r.content))\n", " s = _stats(data[0:-1], data[-1])\n", - " return s[1], file.split(\"_\")[2]\n", + " return (\n", + " _stats(data[0:-1], data[-1]),\n", + " datetime.datetime.strptime(file.split(\"_\")[2].replace(\"m\", \"\"), \"%Y%m%d\"),\n", + " )\n", "\n", "\n", "# small tool to filter invalid response from the API\n", @@ -382,19 +385,20 @@ "outputs": [], "source": [ "with futures.ThreadPoolExecutor(max_workers=10) as executor:\n", - " future_work = [\n", - " executor.submit(fetch_bbox, file) for file in files_15\n", - " ]\n", + " future_work = [executor.submit(fetch_bbox, file) for file in files_15]\n", "\n", - " for f in tqdm(futures.as_completed(future_work), total=len(future_work)): \n", + " for f in tqdm(futures.as_completed(future_work), total=len(future_work)):\n", " pass\n", "\n", - "values, dates = zip(*list(_filter_futures(future_work)))\n", + "values, dates = zip(*list(_filter_futures(future_work)))\n", "\n", - "fig, ax1 = plt.subplots(dpi=150)\n", + "max_values = [v[1] for v in values]\n", + "\n", + "fig, ax1 = plt.subplots(dpi=300)\n", "fig.autofmt_xdate()\n", "\n", - "ax1.plot(dates, values, label=\"No2\")\n", + "ax1.plot(dates, max_values, label=\"No2\")\n", + "ax1.xaxis.set_major_locator(mdates.YearLocator(1, 7))\n", "\n", "ax1.set_xlabel(\"Dates\")\n", "ax1.set_ylabel(\"No2\")\n", @@ -416,19 +420,20 @@ "outputs": [], "source": [ "with futures.ThreadPoolExecutor(max_workers=50) as executor:\n", - " future_work = [\n", - " executor.submit(fetch_bbox, file) for file in files\n", - " ]\n", + " future_work = [executor.submit(fetch_bbox, file) for file in files]\n", "\n", - " for f in tqdm(futures.as_completed(future_work), total=len(future_work)): \n", + " for f in tqdm(futures.as_completed(future_work), total=len(future_work)):\n", " pass\n", "\n", - "values, dates = zip(*list(_filter_futures(future_work)))\n", + "values, dates = zip(*list(_filter_futures(future_work)))\n", + "\n", + "max_values = [v[1] for v in values]\n", "\n", "fig, ax1 = plt.subplots(dpi=150)\n", "fig.autofmt_xdate()\n", "\n", - "ax1.plot(dates, values, label=\"No2\")\n", + "ax1.plot(dates, max_values, label=\"No2\")\n", + "ax1.xaxis.set_major_locator(mdates.YearLocator())\n", "\n", "ax1.set_xlabel(\"Dates\")\n", "ax1.set_ylabel(\"No2\")\n", @@ -460,7 +465,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.2" + "version": "3.9.18" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF_simple.ipynb b/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF_simple.ipynb index 78c4509ea..751091bdd 100644 --- a/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF_simple.ipynb +++ b/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF_simple.ipynb @@ -50,7 +50,9 @@ "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "url = \"https://opendata.digitalglobe.com/events/mauritius-oil-spill/post-event/2020-08-12/105001001F1B5B00/105001001F1B5B00.tif\"" ] }, @@ -70,9 +72,9 @@ "# Fetch File Metadata to get min/max rescaling values (because the file is stored as float32)\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/info\",\n", - " params = {\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", @@ -95,9 +97,9 @@ "# Fetch File Metadata to get min/max rescaling values (because the file is stored as float32)\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", - " params = {\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "print(json.dumps(r, indent=4))" @@ -117,23 +119,18 @@ "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=13\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=13\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"DigitalGlobe OpenData\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"DigitalGlobe OpenData\").add_to(m)\n", + "\n", "m" ] }, @@ -150,24 +147,25 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "url = \"https://data.geo.admin.ch/ch.swisstopo.swissalti3d/swissalti3d_2019_2573-1085/swissalti3d_2019_2573-1085_0.5_2056_5728.tif\"\n", "\n", "# Fetch File Metadata to get min/max rescaling values (because the file is stored as float32)\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/info\",\n", - " params = {\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "print(r)\n", "print(\"Data is of type:\", r[\"dtype\"])\n", "\n", "# This dataset has statistics metadata\n", - "minv, maxv = r[\"band_metadata\"][0][1][\"STATISTICS_MINIMUM\"], r[\"band_metadata\"][0][1][\"STATISTICS_MAXIMUM\"]\n", - "print(\"With values from \", minv, \"to \", maxv)\n", - "\n" + "minv, maxv = (\n", + " r[\"band_metadata\"][0][1][\"STATISTICS_MINIMUM\"],\n", + " r[\"band_metadata\"][0][1][\"STATISTICS_MAXIMUM\"],\n", + ")\n", + "print(\"With values from \", minv, \"to \", maxv)" ] }, { @@ -179,12 +177,12 @@ "# We could get the min/max values using the statistics endpoint\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", - " params = {\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", - "print(json.dumps(r[\"1\"], indent=4))" + "print(json.dumps(r[\"b1\"], indent=4))" ] }, { @@ -194,7 +192,7 @@ "### Display Tiles\n", "\n", "\n", - "1. Without `rescaling` values, TiTiler will return black/grey tiles because it will rescale the data base on min/max value of the datatype." + "Note: By default if the metadata has `min/max` statistics, titiler will use those to rescale the data" ] }, { @@ -204,24 +202,19 @@ "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"] + 1\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"] + 1,\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Swisstopo\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Swisstopo\").add_to(m)\n", "m" ] }, @@ -229,45 +222,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "2. Apply linear rescaling using Min/Max value \n", - "\n", - "This is needed to rescale the value to byte (0 -> 255) which can then be encoded in JPEG or PNG" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", - " \"url\": url,\n", - " \"rescale\": f\"{minv},{maxv}\"\n", - " }\n", - ").json()\n", - "\n", - "bounds = r[\"bounds\"]\n", - "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"] + 1\n", - ")\n", - "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Swisstopo\"\n", - ")\n", - "aod_layer.add_to(m)\n", - "m" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "3. Apply ColorMap\n", + "Apply ColorMap\n", "\n", "Now that the data is rescaled to byte values (0 -> 255) we can apply a colormap" ] @@ -279,26 +234,17 @@ "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", - " \"url\": url,\n", - " \"rescale\": f\"{minv},{maxv}\",\n", - " \"colormap_name\": \"terrain\"\n", - " }\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\"url\": url, \"rescale\": f\"{minv},{maxv}\", \"colormap_name\": \"terrain\"},\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"] + 1\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"] + 1,\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Swisstopo\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Swisstopo\").add_to(m)\n", "m" ] }, @@ -306,7 +252,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "4. Apply non-linear colormap (intervals)\n", + "Apply non-linear colormap (intervals)\n", "\n", "see https://cogeotiff.github.io/rio-tiler/colormap/#intervals-colormaps" ] @@ -322,49 +268,35 @@ "cmap = json.dumps(\n", " [\n", " # ([min, max], [r, g, b, a])\n", - " ([0, 1500], [255,255,204, 255]),\n", - " ([1500, 1700], [161,218,180, 255]),\n", - " ([1700, 1900], [65,182,196, 255]),\n", - " ([1900, 2000], [44,127,184, 255]),\n", - " ([2000, 3000], [37,52,148, 255]),\n", + " ([0, 1500], [255, 255, 204, 255]),\n", + " ([1500, 1700], [161, 218, 180, 255]),\n", + " ([1700, 1900], [65, 182, 196, 255]),\n", + " ([1900, 2000], [44, 127, 184, 255]),\n", + " ([2000, 3000], [37, 52, 148, 255]),\n", " ]\n", ")\n", "# https://colorbrewer2.org/#type=sequential&scheme=YlGnBu&n=5\n", "\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", - " \"url\": url,\n", - " \"colormap\": cmap\n", - " }\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\"url\": url, \"colormap\": cmap},\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"] + 1\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"] + 1,\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Swisstopo\"\n", - ")\n", + "aod_layer = TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Swisstopo\")\n", "aod_layer.add_to(m)\n", "m" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "titiler (3.13.9)", "language": "python", "name": "python3" }, @@ -378,7 +310,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.2" + "version": "3.13.9" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_MosaicJSON.ipynb b/docs/src/examples/notebooks/Working_with_MosaicJSON.ipynb index b62c9220e..40a54b4f7 100644 --- a/docs/src/examples/notebooks/Working_with_MosaicJSON.ipynb +++ b/docs/src/examples/notebooks/Working_with_MosaicJSON.ipynb @@ -1,7 +1,6 @@ { "cells": [ { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -30,7 +29,7 @@ "\n", "By default, TiTiler has `mosaicjson` endpoints.\n", "\n", - "Docs: https://api.cogeo.xyz/docs#/MosaicJSON" + "Docs: https://titiler.xyz/api.html#/MosaicJSON" ] }, { @@ -118,13 +117,13 @@ "session = boto3_session(region_name=\"us-west-2\")\n", "client = session.client(\"s3\")\n", "\n", - "bucket = \"noaa-eri-pds\" #https://registry.opendata.aws/omi-no2-nasa/\n", + "bucket = \"noaa-eri-pds\" # https://registry.opendata.aws/omi-no2-nasa/\n", "\n", "\n", "def list_objects(bucket, prefix):\n", " \"\"\"AWS s3 list objects.\"\"\"\n", "\n", - " paginator = client.get_paginator('list_objects_v2')\n", + " paginator = client.get_paginator(\"list_objects_v2\")\n", "\n", " files = []\n", " for subset in paginator.paginate(Bucket=bucket, Prefix=prefix):\n", @@ -132,6 +131,7 @@ "\n", " return [r[\"Key\"] for r in files]\n", "\n", + "\n", "files = list_objects(bucket, \"2020_Nashville_Tornado/20200307a_RGB\")\n", "files = [f\"s3://{bucket}/{f}\" for f in files if f.endswith(\".tif\")]\n", "\n", @@ -163,8 +163,8 @@ "outputs": [], "source": [ "# We can derive the `bbox` from the filename\n", - "# s3://noaa-eri-pds/2020_Nashville_Tornado/20200307a_RGB/20200307aC0870130w361200n.tif \n", - "# -> 20200307aC0870130w361200n.tif \n", + "# s3://noaa-eri-pds/2020_Nashville_Tornado/20200307a_RGB/20200307aC0870130w361200n.tif\n", + "# -> 20200307aC0870130w361200n.tif\n", "# -> 20200307aC \"0870130w\" \"361200n\" .tif\n", "# -> 0870130w -> 87.025 (West)\n", "# -> 361200n -> 36.2 (Top)\n", @@ -174,16 +174,20 @@ "from geojson_pydantic.features import Feature\n", "from geojson_pydantic.geometries import Polygon\n", "\n", + "\n", "def dms_to_degree(v: str) -> float:\n", " \"\"\"convert degree minute second to decimal degrees.\n", - " \n", + "\n", " '0870130w' -> 87.025\n", " \"\"\"\n", " deg = int(v[0:3])\n", " minutes = int(v[3:5])\n", " seconds = int(v[5:7])\n", " direction = v[-1].upper()\n", - " return (float(deg) + float(minutes)/60 + float(seconds)/(60*60)) * (-1 if direction in ['W', 'S'] else 1)\n", + " return (float(deg) + float(minutes) / 60 + float(seconds) / (60 * 60)) * (\n", + " -1 if direction in [\"W\", \"S\"] else 1\n", + " )\n", + "\n", "\n", "def fname_to_feature(src_path: str) -> Feature:\n", " bname = os.path.basename(src_path)\n", @@ -194,16 +198,14 @@ " lat = dms_to_degree(\"0\" + lat_dms)\n", "\n", " return Feature(\n", - " geometry=Polygon.from_bounds(\n", - " lon, lat - 0.025, lon + 0.025, lat \n", - " ),\n", + " geometry=Polygon.from_bounds(lon, lat - 0.025, lon + 0.025, lat),\n", " properties={\n", " \"path\": src_path,\n", - " }\n", + " },\n", " )\n", - "features = [\n", - " fname_to_feature(f).dict(exclude_none=True) for f in files\n", - "]\n", + "\n", + "\n", + "features = [fname_to_feature(f).dict(exclude_none=True) for f in files]\n", "\n", "# OR We could use Rasterio/rio-tiler\n", "\n", @@ -231,20 +233,23 @@ "metadata": {}, "outputs": [], "source": [ - "geojson = {'type': 'FeatureCollection', 'features': features}\n", + "geojson = {\"type\": \"FeatureCollection\", \"features\": features}\n", "\n", "bounds = featureBounds(geojson)\n", "\n", "m = Map(\n", " tiles=\"OpenStreetMap\",\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=6\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=6,\n", ")\n", "\n", "geo_json = GeoJson(\n", " data=geojson,\n", " style_function=lambda x: {\n", - " 'opacity': 1, 'dashArray': '1', 'fillOpacity': 0, 'weight': 1\n", + " \"opacity\": 1,\n", + " \"dashArray\": \"1\",\n", + " \"fillOpacity\": 0,\n", + " \"weight\": 1,\n", " },\n", ")\n", "geo_json.add_to(m)\n", @@ -280,10 +285,11 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "# We are creating the mosaicJSON using the features we created earlier\n", "# by default MosaicJSON.from_feature will look in feature.properties.path to get the path of the dataset\n", - "mosaicdata = MosaicJSON.from_features(features, minzoom=info.minzoom, maxzoom=info.maxzoom)\n", + "mosaicdata = MosaicJSON.from_features(\n", + " features, minzoom=info.minzoom, maxzoom=info.maxzoom\n", + ")\n", "with MosaicBackend(\"NOAA_Nashville_Tornado.json.gz\", mosaic_def=mosaicdata) as mosaic:\n", " mosaic.write(overwrite=True)\n", " print(mosaic.info())" @@ -297,20 +303,21 @@ }, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/mosaicjson/tilejson.json\",\n", + " f\"{titiler_endpoint}/mosaicjson/WebMercatorQuad/tilejson.json\",\n", " params={\n", " # For this demo we are use the same mosaic but stored on the web\n", - " \"url\": \"https://gist.githubusercontent.com/vincentsarago/c6ace3ccd29a82a4a5531693bbcd61fc/raw/e0d0174a64a9acd2fb820f2c65b1830aab80f52b/NOAA_Nashville_Tornado.json\" \n", - " }\n", + " \"url\": \"https://gist.githubusercontent.com/vincentsarago/c6ace3ccd29a82a4a5531693bbcd61fc/raw/e0d0174a64a9acd2fb820f2c65b1830aab80f52b/NOAA_Nashville_Tornado.json\"\n", + " },\n", ").json()\n", "print(r)\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=13\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=13\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -318,13 +325,16 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"NOAA\"\n", + " attr=\"NOAA\",\n", ")\n", "\n", "geo_json = GeoJson(\n", " data=geojson,\n", " style_function=lambda x: {\n", - " 'opacity': 1, 'dashArray': '1', 'fillOpacity': 0, 'weight': 1\n", + " \"opacity\": 1,\n", + " \"dashArray\": \"1\",\n", + " \"fillOpacity\": 0,\n", + " \"weight\": 1,\n", " },\n", ")\n", "tiles.add_to(m)\n", @@ -342,7 +352,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -356,7 +366,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.2" + "version": "3.9.17" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_NumpyTile.ipynb b/docs/src/examples/notebooks/Working_with_NumpyTile.ipynb index b6b580506..d3e46e00e 100755 --- a/docs/src/examples/notebooks/Working_with_NumpyTile.ipynb +++ b/docs/src/examples/notebooks/Working_with_NumpyTile.ipynb @@ -54,7 +54,9 @@ "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "url = \"https://opendata.digitalglobe.com/events/mauritius-oil-spill/post-event/2020-08-12/105001001F1B5B00/105001001F1B5B00.tif\"" ] }, @@ -64,7 +66,7 @@ "metadata": {}, "outputs": [], "source": [ - "r = httpx.get(f\"{titiler_endpoint}/cog/tilejson.json?url={url}\").json()\n", + "r = httpx.get(f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json?url={url}\").json()\n", "print(r)" ] }, @@ -88,7 +90,9 @@ "# Call TiTiler endpoint using the first tile\n", "\n", "tile = tiles[0]\n", - "r = httpx.get(f\"{titiler_endpoint}/cog/tiles/{tile.z}/{tile.x}/{tile.y}.npy?url={url}\")" + "r = httpx.get(\n", + " f\"{titiler_endpoint}/cog/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}.npy?url={url}\"\n", + ")" ] }, { diff --git a/docs/src/examples/notebooks/Working_with_STAC.ipynb b/docs/src/examples/notebooks/Working_with_STAC.ipynb index 4d4660d52..74e7e32b6 100644 --- a/docs/src/examples/notebooks/Working_with_STAC.ipynb +++ b/docs/src/examples/notebooks/Working_with_STAC.ipynb @@ -1,7 +1,6 @@ { "cells": [ { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -13,7 +12,7 @@ "\n", "> The SpatioTemporal Asset Catalog (STAC) specification aims to standardize the way geospatial assets are exposed online and queried. A 'spatiotemporal asset' is any file that represents information about the earth captured in a certain space and time. The initial focus is primarily remotely-sensed imagery (from satellites, but also planes, drones, balloons, etc), but the core is designed to be extensible to SAR, full motion video, point clouds, hyperspectral, LiDAR and derived data like NDVI, Digital Elevation Models, mosaics, etc.\n", "\n", - "Ref: https://github.com/radiantearth/stac-spechttps://github.com/radiantearth/stac-spec\n", + "Ref: https://github.com/radiantearth/stac-spec\n", "\n", "Using STAC makes data indexation and discovery really easy. In addition to the Collection/Item/Asset (data) specifications, data providers are also encouraged to follow a STAC API specification: https://github.com/radiantearth/stac-api-spec\n", "\n", @@ -30,7 +29,7 @@ "\n", "# TiTiler: STAC + COG\n", "\n", - "Docs: https://github.com/developmentseed/titiler/blob/main/docs/endpoints/stac.md\n", + "Docs: https://github.com/developmentseed/titiler/blob/main/docs/src/endpoints/stac.md\n", "\n", "\n", "TiTiler was first designed to work with single COG by passing the file URL to the tiler.\n", @@ -106,30 +105,21 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Uncomment this line if you need to install the dependencies\n", - "# !pip rasterio folium httpx tqdm" + "# !pip install rasterio folium httpx tqdm" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "%pylab is deprecated, use %matplotlib inline and import the required libraries.\n", - "Populating the interactive namespace from numpy and matplotlib\n" - ] - } - ], + "outputs": [], "source": [ "import os\n", "import json\n", @@ -143,7 +133,7 @@ "from functools import partial\n", "from concurrent import futures\n", "\n", - "from tqdm.notebook import tqdm\n", + "# from tqdm.notebook import tqdm\n", "\n", "from rasterio.plot import reshape_as_image\n", "from rasterio.features import bounds as featureBounds\n", @@ -155,12 +145,14 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Endpoint variables\n", - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "stac_endpoint = \"https://earth-search.aws.element84.com/v0/search\"" ] }, @@ -179,148 +171,38 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "geojson = {\n", - " \"type\": \"FeatureCollection\",\n", - " \"features\": [\n", - " {\n", - " \"type\": \"Feature\",\n", - " \"properties\": {},\n", - " \"geometry\": {\n", - " \"type\": \"Polygon\",\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " 30.810813903808594,\n", - " 29.454247067148533\n", - " ],\n", - " [\n", - " 30.88600158691406,\n", - " 29.454247067148533\n", - " ],\n", - " [\n", - " 30.88600158691406,\n", - " 29.51879923863822\n", - " ],\n", - " [\n", - " 30.810813903808594,\n", - " 29.51879923863822\n", - " ],\n", - " [\n", - " 30.810813903808594,\n", - " 29.454247067148533\n", - " ]\n", - " ]\n", - " ]\n", - " }\n", - " }\n", - " ]\n", + " \"type\": \"FeatureCollection\",\n", + " \"features\": [\n", + " {\n", + " \"type\": \"Feature\",\n", + " \"properties\": {},\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [30.810813903808594, 29.454247067148533],\n", + " [30.88600158691406, 29.454247067148533],\n", + " [30.88600158691406, 29.51879923863822],\n", + " [30.810813903808594, 29.51879923863822],\n", + " [30.810813903808594, 29.454247067148533],\n", + " ]\n", + " ],\n", + " },\n", + " }\n", + " ],\n", "}\n", "\n", "bounds = featureBounds(geojson)\n", "\n", "m = Map(\n", " tiles=\"OpenStreetMap\",\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=11\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=11,\n", ")\n", "\n", "geo_json = GeoJson(data=geojson)\n", @@ -337,83 +219,36 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Results context:\n", - "{'page': 1, 'limit': 100, 'matched': 85, 'returned': 85}\n", - "\n", - "Example of item:\n", - "{\n", - " \"bbox\": [\n", - " 30.155974613579858,\n", - " 28.80949327971016,\n", - " 31.050481437029678,\n", - " 29.815791988006527\n", - " ],\n", - " \"geometry\": {\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " 30.155974613579858,\n", - " 28.80949327971016\n", - " ],\n", - " [\n", - " 30.407037927198104,\n", - " 29.805008695373978\n", - " ],\n", - " [\n", - " 31.031551610920825,\n", - " 29.815791988006527\n", - " ],\n", - " [\n", - " 31.050481437029678,\n", - " 28.825387639743422\n", - " ],\n", - " [\n", - " 30.155974613579858,\n", - " 28.80949327971016\n", - " ]\n", - " ]\n", - " ],\n", - " \"type\": \"Polygon\"\n", - " },\n", - " \"id\": \"S2B_36RTT_20191205_0_L2A\",\n", - " \"collection\": \"sentinel-s2-l2a-cogs\",\n", - " \"type\": \"Feature\",\n", - " \"properties\": {\n", - " \"datetime\": \"2019-12-05T08:42:04Z\",\n", - " \"eo:cloud_cover\": 2.75\n", - " }\n", - "}\n" - ] - } - ], + "outputs": [], "source": [ - "start = datetime.datetime.strptime(\"2019-01-01\", \"%Y-%m-%d\").strftime(\"%Y-%m-%dT00:00:00Z\")\n", - "end = datetime.datetime.strptime(\"2019-12-11\", \"%Y-%m-%d\").strftime(\"%Y-%m-%dT23:59:59Z\")\n", + "start = datetime.datetime.strptime(\"2019-01-01\", \"%Y-%m-%d\").strftime(\n", + " \"%Y-%m-%dT00:00:00Z\"\n", + ")\n", + "end = datetime.datetime.strptime(\"2019-12-11\", \"%Y-%m-%d\").strftime(\n", + " \"%Y-%m-%dT23:59:59Z\"\n", + ")\n", "\n", "# POST body\n", "query = {\n", " \"collections\": [\"sentinel-s2-l2a-cogs\"],\n", " \"datetime\": f\"{start}/{end}\",\n", " \"query\": {\n", - " \"eo:cloud_cover\": {\n", - " \"lt\": 5\n", - " },\n", + " \"eo:cloud_cover\": {\"lt\": 5},\n", " },\n", " \"intersects\": geojson[\"features\"][0][\"geometry\"],\n", " \"limit\": 100,\n", " \"fields\": {\n", - " 'include': ['id', 'properties.datetime', 'properties.eo:cloud_cover'], # This will limit the size of returned body\n", - " 'exclude': ['assets', 'links'] # This will limit the size of returned body\n", - " }\n", + " \"include\": [\n", + " \"id\",\n", + " \"properties.datetime\",\n", + " \"properties.eo:cloud_cover\",\n", + " ], # This will limit the size of returned body\n", + " \"exclude\": [\"assets\", \"links\"], # This will limit the size of returned body\n", + " },\n", "}\n", "\n", "# POST Headers\n", @@ -437,122 +272,23 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "m = Map(\n", " tiles=\"OpenStreetMap\",\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=8\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=8,\n", ")\n", "\n", "geo_json = GeoJson(\n", " data=data,\n", " style_function=lambda x: {\n", - " 'opacity': 1, 'dashArray': '1', 'fillOpacity': 0, 'weight': 1\n", + " \"opacity\": 1,\n", + " \"dashArray\": \"1\",\n", + " \"fillOpacity\": 0,\n", + " \"weight\": 1,\n", " },\n", ")\n", "geo_json.add_to(m)\n", @@ -568,35 +304,21 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAigAAAFyCAYAAADf8CGJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAAC1M0lEQVR4nOz9d5gc5ZnvjX8rd+6ePBpplFBECRACZHIGkTE2YExyNvLu+njPb/dwzr676/PuLnvevd59ffYsOAM2wRhMNBgwUWQQQaCcszQaTeqezl1Vz++P1vR0qNzVYUbP57p0XdNV1U89mumuuut+7vv7ZQghBBQKhUKhUChNBNvoCVAoFAqFQqGUQwMUCoVCoVAoTQcNUCgUCoVCoTQdNEChUCgUCoXSdNAAhUKhUCgUStNBAxQKhUKhUChNBw1QKBQKhUKhNB00QKFQKBQKhdJ08PU+oaqqOHToEILBIBiGqffpKRQKhUKhOIAQgtHRUfT09IBla5/fqHuAcujQIfT29tb7tBQKhUKhUFxg//79mDZtWs3PU/cAJRgMAsj/B0OhUL1PT6FQKBQKxQGxWAy9vb2F+3itqXuAMrasEwqFaIBCoVAoFMoEo17lGbRIlkKhUCgUStNBAxQKhUKhUChNh60A5R//8R/BMEzJvwULFtRqbhQKhUKhUI5TbNegLFq0CK+++ur4AHzdy1goFAqF4hKKoiCXyzV6GpQmQBAEcBzX6GkUsB1d8DyP7u7uWsyFQqFQKHWCEIK+vj6MjIw0eiqUJiISiaC7u7spdMpsByjbt29HT08PPB4PVq5ciXvuuQfTp0/XPT6TySCTyRRex2IxZzOlUCgUimuMBSednZ3w+XxNcUOiNA5CCJLJJPr7+wEAU6ZMafCMbAYop59+Oh588EHMnz8fhw8fxo9//GOcffbZ2LBhg25f9D333IMf//jHrkyWQqFQKNWjKEohOGlra2v0dChNgtfrBQD09/ejs7Oz4cs9DCGEOH3zyMgIZsyYgX//93/HN7/5Tc1jtDIovb29iEajVAeFQqFQGkA6ncbu3bsxc+bMwk2JQgGAVCqFPXv2YNasWfB4PCX7YrEYwuFw3e7fVVW4RiIRzJs3Dzt27NA9RpIkSJJUzWkoFAqFUgPosg6lnGb6TFSlgxKPx7Fz586mWKuiUCgUCoUyebAVoPzX//pfsWbNGuzZswfvvfcerrvuOnAch5tvvrlW86NQKBQKxRYMw+CZZ56p+XlmzpyJn/zkJzU/z/GKrQDlwIEDuPnmmzF//nx89atfRVtbGz744AN0dHTUan4UiiNyR44gvmZNo6dBoVBcpq+vD3/xF3+B2bNnQ5Ik9Pb24qqrrsJrr73W6KlpEovF8D/+x//AggUL4PF40N3djYsuughPPfUUqigBPS6wVYPy2GOP1WoeFIqryAMDiL/7LgLnntvoqVAoFJfYs2cPzjzzTEQiEfzbv/0blixZglwuh5dffhmrV6/Gli1bGj3FEkZGRnDWWWchGo3in/7pn7BixQrwPI81a9bgb/7mb3DBBRcgEok0ZG7ZbBaiKDbk3FahXjwTAHlwkGYD7JLLQRkYbPQsKBSKi9x1111gGAYfffQRvvzlL2PevHlYtGgRfvSjH+GDDz7Qfd/69etxwQUXwOv1oq2tDd/5zncQj8cL+8877zz88Ic/LHnPtddeizvuuKPwur+/H1dddRW8Xi9mzZqFRx55xHS+//2//3fs2bMHH374IW6//XaceOKJmDdvHr797W9j3bp1CAQCAIDh4WHcdtttaGlpgc/nw+WXX47t27cDyGdgvF4vXnzxxZKxn376aQSDQSSTSQDA/v378dWvfhWRSAStra245pprsGfPnsLxd9xxB6699lr88z//M3p6ejB//nzT+TcaGqBMADJbt9IAxSZcewd8Z5ze6GlQKBSXGBoawksvvYTVq1fD7/dX7NfLRCQSCVx66aVoaWnB2rVr8cQTT+DVV1/FD37wA1vnv+OOO7B//3688cYb+MMf/oD77ruvIGqmhaqqeOyxx3DLLbegp6enYn8gEChYxdxxxx34+OOP8dxzz+H9998HIQSrVq1CLpdDKBTClVdeiUcffbTk/Y888giuvfZa+Hw+5HI5XHrppQgGg3j77bfx7rvvIhAI4LLLLkM2my2857XXXsPWrVvxyiuv4Pnnn7f1/28E1EhnAiBMmwae1vnYIrtzB7J79jZ6GhQKxSV27NgBQohtg9pHH30U6XQav/3tbwuBzX/+53/iqquuwv/6X/8LXV1dpmNs27YNL774Ij766COsWLECAPDrX/8aCxcu1H3PwMAAhoeHTee7fft2PPfcc3j33XfxpS99CUA++Ojt7cUzzzyDr3zlK7jllltw6623IplMwufzIRaL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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "fig = plt.figure(dpi=100)\n", "fig.autofmt_xdate()\n", "ax = fig.add_subplot(1, 1, 1)\n", - "ax.plot(dates, cloudcover, label=\"Cloud Cover\", color=\"tab:red\", linewidth=0.4, linestyle=\"-.\")\n", + "ax.plot(\n", + " dates,\n", + " cloudcover,\n", + " label=\"Cloud Cover\",\n", + " color=\"tab:red\",\n", + " linewidth=0.4,\n", + " linestyle=\"-.\",\n", + ")\n", "\n", "ax.legend()" ] @@ -607,12 +329,12 @@ "source": [ "## Use Titiler endpoint\n", "\n", - "https://api.cogeo.xyz/docs#/SpatioTemporal%20Asset%20Catalog\n", + "https://titiler.xyz/api.html#/SpatioTemporal%20Asset%20Catalog\n", "\n", - "`{endpoint}/stac/tiles/{z}/{x}/{y}.{format}?url={stac_item}&{otherquery params}`\n", + "`{endpoint}/stac/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}?url={stac_item}&{otherquery params}`\n", "\n", "\n", - "`{endpoint}/stac/crop/{minx},{miny},{maxx},{maxy}.{format}?url={stac_item}&{otherquery params}`\n", + "`{endpoint}/stac/bbox/{minx},{miny},{maxx},{maxy}.{format}?url={stac_item}&{otherquery params}`\n", "\n", "\n", "`{endpoint}/stac/point/{minx},{miny}?url={stac_item}&{otherquery params}`\n" @@ -620,7 +342,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -636,123 +358,44 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2B_36RTT_20190102_0_L2A\n", - "{'tilejson': '2.2.0', 'version': '1.0.0', 'scheme': 'xyz', 'tiles': ['http://127.0.0.1:8081/stac/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=https%3A%2F%2Fearth-search.aws.element84.com%2Fv0%2Fcollections%2Fsentinel-s2-l2a-cogs%2Fitems%2FS2B_36RTT_20190102_0_L2A&assets=B04&assets=B03&assets=B02&color_formula=Gamma+RGB+3.5+Saturation+1.7+Sigmoidal+RGB+15+0.35'], 'minzoom': 8, 'maxzoom': 14, 'bounds': [29.896473859714554, 28.804454491507947, 31.006314627204915, 29.815413491817537], 'center': [30.451394243459735, 29.309933991662742, 8]}\n" - ] - }, - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Get Tile URL\n", - "item = url_template.format(id=sceneid[-1])\n", + "item = url_template.format(id=sceneid[-1])\n", "print(item)\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = (\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params=(\n", " (\"url\", item),\n", " # Simple RGB combination (True Color)\n", - " (\"assets\", \"B04\"), # red, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"assets\", \"B03\"), # green, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"assets\", \"B02\"), # blue, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"color_formula\", \"Gamma RGB 3.5 Saturation 1.7 Sigmoidal RGB 15 0.35\"), # We use a rio-color formula to make the tiles look nice\n", + " (\n", + " \"assets\",\n", + " \"B04\",\n", + " ), # red, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"assets\",\n", + " \"B03\",\n", + " ), # green, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"assets\",\n", + " \"B02\",\n", + " ), # blue, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"color_formula\",\n", + " \"Gamma RGB 3.5 Saturation 1.7 Sigmoidal RGB 15 0.35\",\n", + " ), # We use a rio-color formula to make the tiles look nice\n", " (\"minzoom\", 8), # By default titiler will use 0\n", - " (\"maxzoom\", 14), # By default titiler will use 24\n", - " )\n", + " (\"maxzoom\", 14), # By default titiler will use 24\n", + " ),\n", ").json()\n", "print(r)\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=10\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -760,7 +403,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"Digital Earth Africa\"\n", + " attr=\"Digital Earth Africa\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -768,119 +411,41 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'tilejson': '2.2.0', 'version': '1.0.0', 'scheme': 'xyz', 'tiles': ['http://127.0.0.1:8081/stac/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=https%3A%2F%2Fearth-search.aws.element84.com%2Fv0%2Fcollections%2Fsentinel-s2-l2a-cogs%2Fitems%2FS2B_36RTT_20191205_0_L2A&assets=B08&assets=B04&assets=B03&color_formula=Gamma+RGB+3.5+Saturation+1.7+Sigmoidal+RGB+15+0.35'], 'minzoom': 8, 'maxzoom': 14, 'bounds': [30.155974613579858, 28.80949327971016, 31.050481437029678, 29.815791988006527], 'center': [30.603228025304766, 29.312642633858346, 8]}\n" - ] - }, - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = (\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params=(\n", " (\"url\", url_template.format(id=sceneid[0])),\n", " # False Color Infrared\n", - " (\"assets\", \"B08\"), # nir, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"assets\", \"B04\"), # red, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"assets\", \"B03\"), # green, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"color_formula\", \"Gamma RGB 3.5 Saturation 1.7 Sigmoidal RGB 15 0.35\"), # We use a rio-color formula to make the tiles look nice\n", + " (\n", + " \"assets\",\n", + " \"B08\",\n", + " ), # nir, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"assets\",\n", + " \"B04\",\n", + " ), # red, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"assets\",\n", + " \"B03\",\n", + " ), # green, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"color_formula\",\n", + " \"Gamma RGB 3.5 Saturation 1.7 Sigmoidal RGB 15 0.35\",\n", + " ), # We use a rio-color formula to make the tiles look nice\n", " (\"minzoom\", 8), # By default titiler will use 0\n", - " (\"maxzoom\", 14), # By default titiler will use 24\n", - " )\n", + " (\"maxzoom\", 14), # By default titiler will use 24\n", + " ),\n", ").json()\n", "print(r)\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=10\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -888,7 +453,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"Digital Earth Africa\"\n", + " attr=\"Digital Earth Africa\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -896,120 +461,32 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'tilejson': '2.2.0', 'version': '1.0.0', 'scheme': 'xyz', 'tiles': ['http://127.0.0.1:8081/stac/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=https%3A%2F%2Fearth-search.aws.element84.com%2Fv0%2Fcollections%2Fsentinel-s2-l2a-cogs%2Fitems%2FS2B_36RTT_20191205_0_L2A&expression=%28B08-B04%29%2F%28B08%2BB04%29&asset_as_band=true&rescale=-1%2C1&colormap_name=viridis'], 'minzoom': 8, 'maxzoom': 14, 'bounds': [30.155974613579858, 28.80949327971016, 31.050481437029678, 29.815791988006527], 'center': [30.603228025304766, 29.312642633858346, 8]}\n" - ] - }, - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = {\n", - " \"url\": url_template.format(id=sceneid[0]),\n", - " \"expression\": \"(B08-B04)/(B08+B04)\", # NDVI (nir-red)/(nir+red), in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " # We need to tell rio-tiler that each asset is a Band \n", - " # (so it will select the first band within each asset automatically)\n", - " \"asset_as_band\": True,\n", - " \"rescale\": \"-1,1\",\n", - " \"minzoom\": 8, # By default titiler will use 0\n", - " \"maxzoom\": 14, # By default titiler will use 24\n", - " \"colormap_name\": \"viridis\",\n", - " }\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params=(\n", + " (\"url\", url_template.format(id=sceneid[0])),\n", + " (\"assets\", \"B08\"),\n", + " (\"assets\", \"B04\"),\n", + " (\n", + " \"expression\",\n", + " \"(b1-b2)/(b1+b2)\",\n", + " ), # NDVI (nir-red)/(nir+red), in next STAC item version (see\n", + " (\"rescale\", \"-1,1\"),\n", + " (\"colormap_name\", \"viridis\"),\n", + " (\"minzoom\", 8), # By default titiler will use 0\n", + " (\"maxzoom\", 14), # By default titiler will use 24\n", + " ),\n", ").json()\n", "print(r)\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=10\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -1017,7 +494,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"Digital Earth Africa\"\n", + " attr=\"Digital Earth Africa\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -1034,36 +511,39 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "def fetch_bbox_array(sceneid, bbox, assets = None, expression = None, **kwargs):\n", + "def fetch_bbox_array(sceneid, bbox, assets, expression=None, **kwargs):\n", " \"\"\"Helper function to fetch and decode Numpy array using Titiler endpoint.\"\"\"\n", " # STAC ITEM URL\n", " stac_item = f\"https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/{sceneid}\"\n", "\n", " xmin, ymin, xmax, ymax = bbox\n", - " \n", + "\n", " # TiTiler required URL + asset or expression parameters\n", - " params = ((\"url\", stac_item), )\n", - " if assets:\n", - " for asset in assets:\n", - " params += ((\"assets\", asset), )\n", - " elif expression:\n", - " params += ((\"expression\", expression), (\"asset_as_band\", True),)\n", - " else:\n", - " raise Exception(\"Missing band or expression input\")\n", + " params = ((\"url\", stac_item), (\"max_size\", 1024))\n", + "\n", + " for asset in assets:\n", + " params += ((\"assets\", asset),)\n", + "\n", + " if expression:\n", + " params += (\n", + " (\"expression\", expression),\n", + " (\"asset_as_band\", True),\n", + " )\n", "\n", " params += tuple(kwargs.items())\n", "\n", " # TITILER ENDPOINT\n", - " url = f\"{titiler_endpoint}/stac/crop/{xmin},{ymin},{xmax},{ymax}.npy\"\n", + " url = f\"{titiler_endpoint}/stac/bbox/{xmin},{ymin},{xmax},{ymax}.npy\"\n", " r = httpx.get(url, params=params)\n", " data = numpy.load(BytesIO(r.content))\n", - " \n", + "\n", " return sceneid, data[0:-1], data[-1]\n", "\n", + "\n", "def _filter_futures(tasks):\n", " for future in tasks:\n", " try:\n", @@ -1071,6 +551,7 @@ " except Exception:\n", " pass\n", "\n", + "\n", "def _stats(data, mask):\n", " arr = numpy.ma.array(data)\n", " arr.mask = mask == 0\n", @@ -1079,43 +560,16 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(1, 128, 128)\n", - "(128, 128)\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Fetch one data\n", - "_, data, mask = fetch_bbox_array(sceneid[0], bounds, assets=[\"B02\"], width=128, height=128)\n", + "_, data, mask = fetch_bbox_array(\n", + " sceneid[0], bounds, assets=[\"B02\"], width=128, height=128\n", + ")\n", "\n", "print(data.shape)\n", "print(mask.shape)\n", @@ -1125,43 +579,11 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "900812f792d44a349a3b2e1579b17338", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - " 0%| | 0/85 [00:00" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Let's fetch the data over our AOI for all our Items\n", "# Here we use `futures.ThreadPoolExecutor` to run the requests in parallel\n", @@ -1170,119 +592,77 @@ "bbox_worker = partial(\n", " fetch_bbox_array,\n", " bbox=bounds,\n", - " assets=(\"B04\", \"B03\", \"B02\"), #(\"red\", \"green\", \"blue\"), in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " assets=[\n", + " \"B04\",\n", + " \"B03\",\n", + " \"B02\",\n", + " ], # (\"red\", \"green\", \"blue\"), in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", " color_formula=\"gamma RGB 3.5, saturation 1.7, sigmoidal RGB 15 0.35\",\n", " width=64,\n", " height=64,\n", ")\n", "\n", "with futures.ThreadPoolExecutor(max_workers=10) as executor:\n", - " future_work = [\n", - " executor.submit(bbox_worker, scene) for scene in sceneid\n", - " ]\n", + " future_work = [executor.submit(bbox_worker, scene) for scene in sceneid]\n", "\n", - " for f in tqdm(futures.as_completed(future_work), total=len(future_work)): \n", + " for f in tqdm(futures.as_completed(future_work), total=len(future_work)):\n", " pass\n", "\n", - "results_rgb = list(_filter_futures(future_work))\n", + "results_rgb = list(_filter_futures(future_work))\n", "\n", "print(\"diplay all results\")\n", "\n", - "fig = plt.figure(figsize=(10,20))\n", + "fig = plt.figure(figsize=(10, 20))\n", "col = 5\n", "row = math.ceil(len(dates) / col)\n", "for i in range(1, len(results_rgb) + 1):\n", " fig.add_subplot(row, col, i)\n", - " plt.imshow(reshape_as_image(results_rgb[i-1][1]))" + " plt.imshow(reshape_as_image(results_rgb[i - 1][1]))" ] }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "93982f5e7e9a4a01838b940b72b43b8c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - " 0%| | 0/85 [00:00" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "## Fetch NDVI\n", "\n", "bbox_worker = partial(\n", " fetch_bbox_array,\n", " bbox=bounds,\n", - " expression=\"(B08-B04)/(B08+B04)\", # (nir-red)/(nir+red), in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " assets=[\n", + " \"B08\",\n", + " \"B04\",\n", + " ],\n", + " expression=\"(b1-b2)/(b1+b2)\", # (nir-red)/(nir+red), in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", " width=64,\n", " height=64,\n", ")\n", "\n", "with futures.ThreadPoolExecutor(max_workers=10) as executor:\n", - " future_work = [\n", - " executor.submit(bbox_worker, scene) for scene in sceneid\n", - " ]\n", + " future_work = [executor.submit(bbox_worker, scene) for scene in sceneid]\n", "\n", - " for f in tqdm(futures.as_completed(future_work), total=len(future_work)): \n", + " for f in tqdm(futures.as_completed(future_work), total=len(future_work)):\n", " pass\n", "\n", - "results_ndvi = list(_filter_futures(future_work))\n", + "results_ndvi = list(_filter_futures(future_work))\n", "\n", - "fig = plt.figure(figsize=(10,20))\n", + "fig = plt.figure(figsize=(10, 20))\n", "col = 5\n", "row = math.ceil(len(dates) / col)\n", - "for i in range(1, len(results_rgb) + 1):\n", + "for i in range(1, len(results_ndvi) + 1):\n", " fig.add_subplot(row, col, i)\n", - " plt.imshow(results_ndvi[i-1][1][0])" + " plt.imshow(results_ndvi[i - 1][1][0])" ] }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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flj8bp5++mn9n3v27Il//vOfyM/Px7Zt27B7927k5OQgJycHKSkp2L17t/59OSIianls4IiIiJrQtWvXUFBQAAAYNmyYvn3Tpk0AgA8++AA//OEPfW5z+vTpR5orJiYGpaWleO+99xAREfHAt3M4HEhPT0d6ejoA4Pjx4xg/fjwOHDiAlStX4vXXX3+kPERE1Ph4ERMiIqImNHv2bJSXl+Opp55CSkqKvt17QZOYmJg6t8nJyal3LO8nYR6Pp976iBEjAHzXHD6qbt264Sc/+QkA4NixYw0ai4iIGhcbOCIioiZw+vRpjB07FqtWrYLL5cKqVat86t/73vcAACtWrPA5VXLfvn1499136x0zKioKAHDy5Ml667Nnz4bT6cScOXOwcePGOvXKykps2LAB58+fBwBUVFRg2bJluHr1qs9+mqYhPz8fABAbG/sAR0tERM2FP+RNRET0iLxXlfR+50zTNFy/fh3FxcX4+uuvISLo3Lkz1q5dW+cy/cXFxejTpw/Ky8vRtWtX9OzZE2VlZSgsLMTs2bPx+9//HnFxcfqFSQDg1q1biIuLw3/+8x8888wzSEhIgMViQWZmJgYMGACg5qcHxo8fj4qKCiQmJqJLly5wuVwoKyvDV199hfLychw5cgRJSUm4evUqwsPDYbPZ0LdvX8THx6Oqqgp///vf4Xa7ER8fj8OHDz/U6ZhERNS02MARERE9ojt/FsBqtSIkJARRUVHo27cv0tLSkJqaetcrOX799dd44403cOjQIdy8eRNPPvkkpk2bhtdeew1KqToNHAAcPnwY8+bNQ1FREa5fvw4RwYcffohJkybp+5SWlmLx4sXYuXMn3G43bDYboqKi0Lt3b2RkZCAtLQ2BgYHweDxYsWIFdu3ahaNHj+LixYsIDAxEhw4dMGrUKEyfPt3nyplERNTy2MARERERERGZBL8DR0REREREZBJs4IiIiIiIiEyCDRwREREREZFJsIEjIiIiIiIyCTZwREREREREJsEGjoiIiIiIyCTYwBEREREREZkEGzgiIiIiIiKTYANHRERERERkEmzgiIiIiIiITIINHBERERERkUmwgSMiIiIiIjIJNnBEREREREQmwQaOiIiIiIjIJNjAERERERERmQQbOCIiIiIiIpNgA0dERERERGQS/w/ouQYkb72MZQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "stats = [_stats(data, mask) for _, data, mask in results_ndvi]\n", "\n", @@ -1308,7 +688,7 @@ ], "metadata": { "kernelspec": { - "display_name": "py39", + "display_name": "titiler (3.13.9)", "language": "python", "name": "python3" }, @@ -1322,12 +702,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13 (main, May 24 2022, 21:13:51) \n[Clang 13.1.6 (clang-1316.0.21.2)]" - }, - "vscode": { - "interpreter": { - "hash": "2590a9e34ee6c8bdce5141410f2a072bbabd2a859a8a48acdaa85720923a90ef" - } + "version": "3.13.9" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_STAC_simple.ipynb b/docs/src/examples/notebooks/Working_with_STAC_simple.ipynb index e52aeb858..d91400788 100644 --- a/docs/src/examples/notebooks/Working_with_STAC_simple.ipynb +++ b/docs/src/examples/notebooks/Working_with_STAC_simple.ipynb @@ -13,7 +13,7 @@ "\n", "> The SpatioTemporal Asset Catalog (STAC) specification aims to standardize the way geospatial assets are exposed online and queried. A 'spatiotemporal asset' is any file that represents information about the earth captured in a certain space and time. The initial focus is primarily remotely-sensed imagery (from satellites, but also planes, drones, balloons, etc), but the core is designed to be extensible to SAR, full motion video, point clouds, hyperspectral, LiDAR and derived data like NDVI, Digital Elevation Models, mosaics, etc.\n", "\n", - "Ref: https://github.com/radiantearth/stac-spechttps://github.com/radiantearth/stac-spec\n", + "Ref: https://github.com/radiantearth/stac-spec\n", "\n", "Using STAC makes data indexation and discovery really easy. In addition to the Collection/Item/Asset (data) specifications, data providers are also encouraged to follow a STAC API specification: https://github.com/radiantearth/stac-api-spec\n", "\n", @@ -47,20 +47,11 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "%pylab is deprecated, use %matplotlib inline and import the required libraries.\n", - "Populating the interactive namespace from numpy and matplotlib\n" - ] - } - ], + "outputs": [], "source": [ "import httpx\n", "\n", @@ -73,30 +64,24 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "stac_item = \"https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items/S2A_30TVT_20221112_0_L2A\"" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": { "scrolled": true, "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'type': 'Feature', 'stac_version': '1.0.0', 'id': 'S2A_30TVT_20221112_0_L2A', 'properties': {'created': '2022-11-14T06:54:49.284Z', 'platform': 'sentinel-2a', 'constellation': 'sentinel-2', 'instruments': ['msi'], 'eo:cloud_cover': 0.005979, 'proj:epsg': 32630, 'mgrs:utm_zone': 30, 'mgrs:latitude_band': 'T', 'mgrs:grid_square': 'VT', 'grid:code': 'MGRS-30TVT', 'view:sun_azimuth': 169.467826196677, 'view:sun_elevation': 24.259740600657594, 's2:degraded_msi_data_percentage': 0, 's2:nodata_pixel_percentage': 0.000226, 's2:saturated_defective_pixel_percentage': 0, 's2:dark_features_percentage': 0, 's2:cloud_shadow_percentage': 0.002296, 's2:vegetation_percentage': 10.348745, 's2:not_vegetated_percentage': 2.478484, 's2:water_percentage': 87.111628, 's2:unclassified_percentage': 0.002548, 's2:medium_proba_clouds_percentage': 0.003716, 's2:high_proba_clouds_percentage': 0.000508, 's2:thin_cirrus_percentage': 0.001755, 's2:snow_ice_percentage': 0.050325, 's2:product_type': 'S2MSI2A', 's2:processing_baseline': '04.00', 's2:product_uri': 'S2A_MSIL2A_20221112T111321_N0400_R137_T30TVT_20221112T145700.SAFE', 's2:generation_time': '2022-11-12T14:57:00.000000Z', 's2:datatake_id': 'GS2A_20221112T111321_038601_N04.00', 's2:datatake_type': 'INS-NOBS', 's2:datastrip_id': 'S2A_OPER_MSI_L2A_DS_ATOS_20221112T145700_S20221112T111315_N04.00', 's2:granule_id': 'S2A_OPER_MSI_L2A_TL_ATOS_20221112T145700_A038601_T30TVT_N04.00', 's2:reflectance_conversion_factor': 1.0193600036007, 'datetime': '2022-11-12T11:18:11.455000Z', 's2:sequence': '0', 'earthsearch:s3_path': 's3://sentinel-cogs/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A', 'earthsearch:payload_id': 'roda-sentinel2/workflow-sentinel2-to-stac/d5f624f4b32b7ca4b39180d6eceea7fd', 'earthsearch:boa_offset_applied': True, 'processing:software': {'sentinel2-to-stac': '0.1.0'}, 'updated': '2022-11-14T06:54:49.284Z'}, 'geometry': {'type': 'Polygon', 'coordinates': [[[-4.337121116089946, 47.8459059875105], [-2.86954302848021, 47.85361872923358], [-2.8719559380291044, 46.865637260938634], [-4.312398603410253, 46.85818510451771], [-4.337121116089946, 47.8459059875105]]]}, 'links': [{'rel': 'self', 'href': 'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items/S2A_30TVT_20221112_0_L2A'}, {'rel': 'canonical', 'href': 's3://sentinel-cogs/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/S2A_30TVT_20221112_0_L2A.json', 'type': 'application/json'}, {'rel': 'license', 'href': 'https://sentinel.esa.int/documents/247904/690755/Sentinel_Data_Legal_Notice'}, {'rel': 'derived_from', 'href': 'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l1c/items/S2A_30TVT_20221112_0_L1C', 'type': 'application/geo+json'}, {'rel': 'parent', 'href': 'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a'}, {'rel': 'collection', 'href': 'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a'}, {'rel': 'root', 'href': 'https://earth-search.aws.element84.com/v1/'}], 'assets': {'aot': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/AOT.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Aerosol optical thickness (AOT)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.001, 'offset': 0}], 'roles': ['data', 'reflectance']}, 'blue': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B02.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Blue (band 2) - 10m', 'eo:bands': [{'name': 'blue', 'common_name': 'blue', 'description': 'Blue (band 2)', 'center_wavelength': 0.49, 'full_width_half_max': 0.098}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'coastal': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B01.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Coastal aerosol (band 1) - 60m', 'eo:bands': [{'name': 'coastal', 'common_name': 'coastal', 'description': 'Coastal aerosol (band 1)', 'center_wavelength': 0.443, 'full_width_half_max': 0.027}], 'gsd': 60, 'proj:shape': [1830, 1830], 'proj:transform': [60, 0, 399960, 0, -60, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 60, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'granule_metadata': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/granule_metadata.xml', 'type': 'application/xml', 'roles': ['metadata']}, 'green': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B03.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Green (band 3) - 10m', 'eo:bands': [{'name': 'green', 'common_name': 'green', 'description': 'Green (band 3)', 'center_wavelength': 0.56, 'full_width_half_max': 0.045}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B08.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'NIR 1 (band 8) - 10m', 'eo:bands': [{'name': 'nir', 'common_name': 'nir', 'description': 'NIR 1 (band 8)', 'center_wavelength': 0.842, 'full_width_half_max': 0.145}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir08': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B8A.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'NIR 2 (band 8A) - 20m', 'eo:bands': [{'name': 'nir08', 'common_name': 'nir08', 'description': 'NIR 2 (band 8A)', 'center_wavelength': 0.865, 'full_width_half_max': 0.033}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir09': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B09.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'NIR 3 (band 9) - 60m', 'eo:bands': [{'name': 'nir09', 'common_name': 'nir09', 'description': 'NIR 3 (band 9)', 'center_wavelength': 0.945, 'full_width_half_max': 0.026}], 'gsd': 60, 'proj:shape': [1830, 1830], 'proj:transform': [60, 0, 399960, 0, -60, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 60, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'red': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B04.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Red (band 4) - 10m', 'eo:bands': [{'name': 'red', 'common_name': 'red', 'description': 'Red (band 4)', 'center_wavelength': 0.665, 'full_width_half_max': 0.038}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge1': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B05.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Red edge 1 (band 5) - 20m', 'eo:bands': [{'name': 'rededge1', 'common_name': 'rededge', 'description': 'Red edge 1 (band 5)', 'center_wavelength': 0.704, 'full_width_half_max': 0.019}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge2': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B06.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Red edge 2 (band 6) - 20m', 'eo:bands': [{'name': 'rededge2', 'common_name': 'rededge', 'description': 'Red edge 2 (band 6)', 'center_wavelength': 0.74, 'full_width_half_max': 0.018}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge3': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B07.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Red edge 3 (band 7) - 20m', 'eo:bands': [{'name': 'rededge3', 'common_name': 'rededge', 'description': 'Red edge 3 (band 7)', 'center_wavelength': 0.783, 'full_width_half_max': 0.028}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'scl': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/SCL.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Scene classification map (SCL)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint8', 'spatial_resolution': 20}], 'roles': ['data', 'reflectance']}, 'swir16': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B11.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'SWIR 1 (band 11) - 20m', 'eo:bands': [{'name': 'swir16', 'common_name': 'swir16', 'description': 'SWIR 1 (band 11)', 'center_wavelength': 1.61, 'full_width_half_max': 0.143}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'swir22': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B12.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'SWIR 2 (band 12) - 20m', 'eo:bands': [{'name': 'swir22', 'common_name': 'swir22', 'description': 'SWIR 2 (band 12)', 'center_wavelength': 2.19, 'full_width_half_max': 0.242}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'thumbnail': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/thumbnail.jpg', 'type': 'image/jpeg', 'title': 'Thumbnail image', 'roles': ['thumbnail']}, 'tileinfo_metadata': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/tileinfo_metadata.json', 'type': 'application/json', 'roles': ['metadata']}, 'visual': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/TCI.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'True color image', 'eo:bands': [{'name': 'red', 'common_name': 'red', 'description': 'Red (band 4)', 'center_wavelength': 0.665, 'full_width_half_max': 0.038}, {'name': 'green', 'common_name': 'green', 'description': 'Green (band 3)', 'center_wavelength': 0.56, 'full_width_half_max': 0.045}, {'name': 'blue', 'common_name': 'blue', 'description': 'Blue (band 2)', 'center_wavelength': 0.49, 'full_width_half_max': 0.098}], 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'roles': ['visual']}, 'wvp': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/WVP.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Water vapour (WVP)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'unit': 'cm', 'scale': 0.001, 'offset': 0}], 'roles': ['data', 'reflectance']}, 'aot-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/AOT.jp2', 'type': 'image/jp2', 'title': 'Aerosol optical thickness (AOT)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.001, 'offset': 0}], 'roles': ['data', 'reflectance']}, 'blue-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B02.jp2', 'type': 'image/jp2', 'title': 'Blue (band 2) - 10m', 'eo:bands': [{'name': 'blue', 'common_name': 'blue', 'description': 'Blue (band 2)', 'center_wavelength': 0.49, 'full_width_half_max': 0.098}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'coastal-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B01.jp2', 'type': 'image/jp2', 'title': 'Coastal aerosol (band 1) - 60m', 'eo:bands': [{'name': 'coastal', 'common_name': 'coastal', 'description': 'Coastal aerosol (band 1)', 'center_wavelength': 0.443, 'full_width_half_max': 0.027}], 'gsd': 60, 'proj:shape': [1830, 1830], 'proj:transform': [60, 0, 399960, 0, -60, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 60, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'green-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B03.jp2', 'type': 'image/jp2', 'title': 'Green (band 3) - 10m', 'eo:bands': [{'name': 'green', 'common_name': 'green', 'description': 'Green (band 3)', 'center_wavelength': 0.56, 'full_width_half_max': 0.045}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B08.jp2', 'type': 'image/jp2', 'title': 'NIR 1 (band 8) - 10m', 'eo:bands': [{'name': 'nir', 'common_name': 'nir', 'description': 'NIR 1 (band 8)', 'center_wavelength': 0.842, 'full_width_half_max': 0.145}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir08-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B8A.jp2', 'type': 'image/jp2', 'title': 'NIR 2 (band 8A) - 20m', 'eo:bands': [{'name': 'nir08', 'common_name': 'nir08', 'description': 'NIR 2 (band 8A)', 'center_wavelength': 0.865, 'full_width_half_max': 0.033}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir09-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B09.jp2', 'type': 'image/jp2', 'title': 'NIR 3 (band 9) - 60m', 'eo:bands': [{'name': 'nir09', 'common_name': 'nir09', 'description': 'NIR 3 (band 9)', 'center_wavelength': 0.945, 'full_width_half_max': 0.026}], 'gsd': 60, 'proj:shape': [1830, 1830], 'proj:transform': [60, 0, 399960, 0, -60, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 60, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'red-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B04.jp2', 'type': 'image/jp2', 'title': 'Red (band 4) - 10m', 'eo:bands': [{'name': 'red', 'common_name': 'red', 'description': 'Red (band 4)', 'center_wavelength': 0.665, 'full_width_half_max': 0.038}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge1-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B05.jp2', 'type': 'image/jp2', 'title': 'Red edge 1 (band 5) - 20m', 'eo:bands': [{'name': 'rededge1', 'common_name': 'rededge', 'description': 'Red edge 1 (band 5)', 'center_wavelength': 0.704, 'full_width_half_max': 0.019}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge2-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B06.jp2', 'type': 'image/jp2', 'title': 'Red edge 2 (band 6) - 20m', 'eo:bands': [{'name': 'rededge2', 'common_name': 'rededge', 'description': 'Red edge 2 (band 6)', 'center_wavelength': 0.74, 'full_width_half_max': 0.018}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge3-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B07.jp2', 'type': 'image/jp2', 'title': 'Red edge 3 (band 7) - 20m', 'eo:bands': [{'name': 'rededge3', 'common_name': 'rededge', 'description': 'Red edge 3 (band 7)', 'center_wavelength': 0.783, 'full_width_half_max': 0.028}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'scl-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/SCL.jp2', 'type': 'image/jp2', 'title': 'Scene classification map (SCL)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint8', 'spatial_resolution': 20}], 'roles': ['data', 'reflectance']}, 'swir16-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B11.jp2', 'type': 'image/jp2', 'title': 'SWIR 1 (band 11) - 20m', 'eo:bands': [{'name': 'swir16', 'common_name': 'swir16', 'description': 'SWIR 1 (band 11)', 'center_wavelength': 1.61, 'full_width_half_max': 0.143}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'swir22-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B12.jp2', 'type': 'image/jp2', 'title': 'SWIR 2 (band 12) - 20m', 'eo:bands': [{'name': 'swir22', 'common_name': 'swir22', 'description': 'SWIR 2 (band 12)', 'center_wavelength': 2.19, 'full_width_half_max': 0.242}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'visual-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/TCI.jp2', 'type': 'image/jp2', 'title': 'True color image', 'eo:bands': [{'name': 'red', 'common_name': 'red', 'description': 'Red (band 4)', 'center_wavelength': 0.665, 'full_width_half_max': 0.038}, {'name': 'green', 'common_name': 'green', 'description': 'Green (band 3)', 'center_wavelength': 0.56, 'full_width_half_max': 0.045}, {'name': 'blue', 'common_name': 'blue', 'description': 'Blue (band 2)', 'center_wavelength': 0.49, 'full_width_half_max': 0.098}], 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'roles': ['visual']}, 'wvp-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/WVP.jp2', 'type': 'image/jp2', 'title': 'Water vapour (WVP)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'unit': 'cm', 'scale': 0.001, 'offset': 0}], 'roles': ['data', 'reflectance']}}, 'bbox': [-4.337121116089946, 46.85818510451771, -2.86954302848021, 47.85361872923358], 'stac_extensions': ['https://stac-extensions.github.io/grid/v1.0.0/schema.json', 'https://stac-extensions.github.io/eo/v1.0.0/schema.json', 'https://stac-extensions.github.io/mgrs/v1.0.0/schema.json', 'https://stac-extensions.github.io/projection/v1.0.0/schema.json', 'https://stac-extensions.github.io/processing/v1.1.0/schema.json', 'https://stac-extensions.github.io/view/v1.0.0/schema.json', 'https://stac-extensions.github.io/raster/v1.1.0/schema.json'], 'collection': 'sentinel-2-l2a'}\n" - ] - } - ], + "outputs": [], "source": [ "item = httpx.get(stac_item).json()\n", "print(item)" @@ -104,53 +89,11 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Name: aot | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: blue | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: coastal | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: granule_metadata | Format: application/xml\n", - "Name: green | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: nir | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: nir08 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: nir09 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: red | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: rededge1 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: rededge2 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: rededge3 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: scl | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: swir16 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: swir22 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: thumbnail | Format: image/jpeg\n", - "Name: tileinfo_metadata | Format: application/json\n", - "Name: visual | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: wvp | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: aot-jp2 | Format: image/jp2\n", - "Name: blue-jp2 | Format: image/jp2\n", - "Name: coastal-jp2 | Format: image/jp2\n", - "Name: green-jp2 | Format: image/jp2\n", - "Name: nir-jp2 | Format: image/jp2\n", - "Name: nir08-jp2 | Format: image/jp2\n", - "Name: nir09-jp2 | Format: image/jp2\n", - "Name: red-jp2 | Format: image/jp2\n", - "Name: rededge1-jp2 | Format: image/jp2\n", - "Name: rededge2-jp2 | Format: image/jp2\n", - "Name: rededge3-jp2 | Format: image/jp2\n", - "Name: scl-jp2 | Format: image/jp2\n", - "Name: swir16-jp2 | Format: image/jp2\n", - "Name: swir22-jp2 | Format: image/jp2\n", - "Name: visual-jp2 | Format: image/jp2\n", - "Name: wvp-jp2 | Format: image/jp2\n" - ] - } - ], + "outputs": [], "source": [ "for it, asset in item[\"assets\"].items():\n", " print(\"Name:\", it, \"| Format:\", asset[\"type\"])" @@ -158,111 +101,16 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "bounds = featureBounds(item)\n", "\n", "m = Map(\n", " tiles=\"OpenStreetMap\",\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=8\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=8,\n", ")\n", "\n", "geo_json = GeoJson(data=item)\n", @@ -272,28 +120,23 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'visual': {'bounds': [-4.337134709547373, 46.85817595750231, -2.869529638083867, 47.85370180403547], 'minzoom': 7, 'maxzoom': 13, 'band_metadata': [['b1', {}], ['b2', {}], ['b3', {}]], 'band_descriptions': [['b1', ''], ['b2', ''], ['b3', '']], 'dtype': 'uint8', 'nodata_type': 'Nodata', 'colorinterp': ['red', 'green', 'blue'], 'height': 10980, 'driver': 'GTiff', 'overviews': [2, 4, 8, 16], 'count': 3, 'width': 10980, 'nodata_value': 0.0}, 'red': {'bounds': [-4.337134709547373, 46.85817595750231, -2.869529638083867, 47.85370180403547], 'minzoom': 7, 'maxzoom': 13, 'band_metadata': [['b1', {}]], 'band_descriptions': [['b1', '']], 'dtype': 'uint16', 'nodata_type': 'Nodata', 'colorinterp': ['gray'], 'height': 10980, 'driver': 'GTiff', 'overviews': [2, 4, 8, 16], 'count': 1, 'width': 10980, 'nodata_value': 0.0}, 'blue': {'bounds': [-4.337134709547373, 46.85817595750231, -2.869529638083867, 47.85370180403547], 'minzoom': 7, 'maxzoom': 13, 'band_metadata': [['b1', {}]], 'band_descriptions': [['b1', '']], 'dtype': 'uint16', 'nodata_type': 'Nodata', 'colorinterp': ['gray'], 'height': 10980, 'driver': 'GTiff', 'overviews': [2, 4, 8, 16], 'count': 1, 'width': 10980, 'nodata_value': 0.0}, 'green': {'bounds': [-4.337134709547373, 46.85817595750231, -2.869529638083867, 47.85370180403547], 'minzoom': 7, 'maxzoom': 13, 'band_metadata': [['b1', {}]], 'band_descriptions': [['b1', '']], 'dtype': 'uint16', 'nodata_type': 'Nodata', 'colorinterp': ['gray'], 'height': 10980, 'driver': 'GTiff', 'overviews': [2, 4, 8, 16], 'count': 1, 'width': 10980, 'nodata_value': 0.0}}\n" - ] - } - ], + "outputs": [], "source": [ "# Get Tile URL\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/stac/info\",\n", - " params = (\n", + " params=(\n", " (\"url\", stac_item),\n", " # Get info for multiple assets\n", - " (\"assets\",\"visual\"), (\"assets\",\"red\"), (\"assets\",\"blue\"), (\"assets\",\"green\"),\n", - " )\n", + " (\"assets\", \"visual\"),\n", + " (\"assets\", \"red\"),\n", + " (\"assets\", \"blue\"),\n", + " (\"assets\", \"green\"),\n", + " ),\n", ").json()\n", "print(r)" ] @@ -307,105 +150,22 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": stac_item,\n", " \"assets\": \"visual\",\n", " \"minzoom\": 8, # By default titiler will use 0\n", - " \"maxzoom\": 14, # By default titiler will use 24\n", - " }\n", + " \"maxzoom\": 14, # By default titiler will use 24\n", + " },\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=10\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -413,7 +173,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"ESA\"\n", + " attr=\"ESA\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -428,107 +188,23 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Get Tile URL\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": stac_item,\n", - " \"assets\": \"visual\",\n", - " \"asset_bidx\": \"visual|3,1,2\",\n", + " \"assets\": \"visual|indexes=1,2,3\",\n", " \"minzoom\": 8, # By default titiler will use 0\n", - " \"maxzoom\": 14, # By default titiler will use 24\n", - " }\n", + " \"maxzoom\": 14, # By default titiler will use 24\n", + " },\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=12\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=12\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -536,7 +212,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"ESA\"\n", + " attr=\"ESA\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -544,96 +220,14 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Get Tile URL\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = (\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params=(\n", " (\"url\", stac_item),\n", " (\"assets\", \"red\"),\n", " (\"assets\", \"green\"),\n", @@ -646,12 +240,11 @@ " (\"minzoom\", 8),\n", " (\"maxzoom\", 14),\n", " (\"rescale\", \"0,2000\"),\n", - " )\n", + " ),\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=11\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=11\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -659,7 +252,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"ESA\"\n", + " attr=\"ESA\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -669,234 +262,32 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Apply Expression between assets" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Get Tile URL\n", - "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = (\n", - " (\"url\", stac_item),\n", - " (\"expression\", \"(nir-red)/(nir+red)\"), # NDVI\n", - " # We need to tell rio-tiler that each asset is a Band \n", - " # (so it will select the first band within each asset automatically)\n", - " (\"asset_as_band\", True),\n", - " (\"rescale\", \"-1,1\"),\n", - " (\"minzoom\", 8),\n", - " (\"maxzoom\", 14),\n", - " (\"colormap_name\", \"viridis\"),\n", - " )\n", - ").json()\n", - "\n", - "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", - ")\n", - "\n", - "tiles = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " min_zoom=r[\"minzoom\"],\n", - " max_zoom=r[\"maxzoom\"],\n", - " opacity=1,\n", - " attr=\"ESA\"\n", - ")\n", - "tiles.add_to(m)\n", - "m" + "Use an expression to calculate a band index (NDVI) based on information contained in multiple assets." ] }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Get Tile URL\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = (\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params=(\n", " (\"url\", stac_item),\n", - " # if you don't use `asset_as_band=True` option you need to pass the band indexes within the expression\n", - " (\"expression\", \"(nir_b1-red_b1)/(nir_b1+red_b1)\"), # NDVI\n", + " (\"assets\", \"nir\"),\n", + " (\"assets\", \"red\"),\n", + " (\"expression\", \"(b1-b2)/(b1+b2)\"), # NDVI\n", " (\"rescale\", \"-1,1\"),\n", " (\"minzoom\", 8),\n", " (\"maxzoom\", 14),\n", " (\"colormap_name\", \"viridis\"),\n", - " )\n", + " ),\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=10\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -904,7 +295,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"ESA\"\n", + " attr=\"ESA\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -920,7 +311,7 @@ ], "metadata": { "kernelspec": { - "display_name": "py39", + "display_name": "titiler (3.13.9)", "language": "python", "name": "python3" }, @@ -934,12 +325,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13 (main, May 24 2022, 21:13:51) \n[Clang 13.1.6 (clang-1316.0.21.2)]" - }, - "vscode": { - "interpreter": { - "hash": "2590a9e34ee6c8bdce5141410f2a072bbabd2a859a8a48acdaa85720923a90ef" - } + "version": "3.13.9" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_Statistics.ipynb b/docs/src/examples/notebooks/Working_with_Statistics.ipynb index d9f2ac5e4..83adc19c0 100644 --- a/docs/src/examples/notebooks/Working_with_Statistics.ipynb +++ b/docs/src/examples/notebooks/Working_with_Statistics.ipynb @@ -2,139 +2,95 @@ "cells": [ { "cell_type": "markdown", - "source": [ - "# Working with Statistics" - ], "metadata": { "collapsed": false - } + }, + "source": [ + "# Working with Statistics" + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "## Intro\n", "\n", "Titiler allows you to get statistics and summaries of your data without having to load the entire dataset yourself. These statistics can be summaries of entire COG files, STAC items, or individual parts of the file, specified using GeoJSON.\n", "\n", - "Below, we will go over some of the statistical endpoints in Titiler - `/bounds`, `/info`, and `/statistics`.\n", + "Below, we will go over some of the statistical endpoints in Titiler - `/info` and `/statistics`.\n", "\n", "(Note: these examples will be using the `/cog` endpoint, but everything is also available for `/stac` and `/mosaicjson` unless otherwise noted)" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 16, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:40.161502Z", + "start_time": "2023-04-06T14:25:40.153667Z" + }, + "collapsed": false + }, "outputs": [], "source": [ "# setup\n", "import httpx\n", "import json\n", "\n", - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "cog_url = \"https://opendata.digitalglobe.com/events/mauritius-oil-spill/post-event/2020-08-12/105001001F1B5B00/105001001F1B5B00.tif\"" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:40.153667Z", - "end_time": "2023-04-06T14:25:40.161502Z" - } - } + ] }, { "cell_type": "markdown", - "source": [ - "## Bounds\n", - "\n", - "The `/bounds` endpoint returns the bounding box of the image/asset. These bounds are returned in the projection EPSG:4326 (WGS84), in the format `(minx, miny, maxx, maxy)`." - ], "metadata": { "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 10, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'bounds': [57.664053823239804, -20.55473177712791, 57.84021477996238, -20.25261582755764]}\n" - ] - } - ], + }, "source": [ - "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/bounds\",\n", - " params = {\n", - " \"url\": cog_url,\n", - " }\n", - ").json()\n", + "## Info\n", "\n", - "bounds = r[\"bounds\"]\n", - "print(r)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:40.781598Z", - "end_time": "2023-04-06T14:25:40.921234Z" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "For a bit more information, you can get summary statistics from the `/info` endpoint. This includes info such as:\n", - "- Bounds (identical to the `/bounds` endpoint)\n", - "- Min and max zoom\n", + "The `/info` endpoint returns general metadata about the image/asset.\n", + "\n", + "- Bounds\n", + "- CRS \n", "- Band metadata, such as names of the bands and their descriptions\n", "- Number of bands in the image\n", "- Overview levels\n", - "- Image width and height\n", - "\n", - "These are statistics available in the metadata of the image, so should be fast to read.\n" - ], - "metadata": { - "collapsed": false - } + "- Image width and height" + ] }, { "cell_type": "code", - "execution_count": 11, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"bounds\": [57.664053823239804, -20.55473177712791, 57.84021477996238, -20.25261582755764], \"minzoom\": 10, \"maxzoom\": 18, \"band_metadata\": [[\"b1\", {}], [\"b2\", {}], [\"b3\", {}]], \"band_descriptions\": [[\"b1\", \"\"], [\"b2\", \"\"], [\"b3\", \"\"]], \"dtype\": \"uint8\", \"nodata_type\": \"Mask\", \"colorinterp\": [\"red\", \"green\", \"blue\"], \"count\": 3, \"width\": 38628, \"driver\": \"GTiff\", \"overviews\": [2, 4, 8, 16, 32, 64, 128], \"height\": 66247}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:42.410135Z", + "start_time": "2023-04-06T14:25:42.355858Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/info\",\n", - " params = {\n", + " params={\n", " \"url\": cog_url,\n", - " }\n", + " },\n", ").json()\n", "\n", "print(json.dumps(r))" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:42.355858Z", - "end_time": "2023-04-06T14:25:42.410135Z" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "## Statistics\n", "\n", @@ -144,98 +100,86 @@ "- Percentiles\n", "\n", "Statistics are generated both for the image as a whole and for each band individually." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 12, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"b1\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 36.94901407469342, \"count\": 574080.0, \"sum\": 21211690.0, \"std\": 48.282133573955264, \"median\": 3.0, \"majority\": 1.0, \"minority\": 246.0, \"unique\": 256.0, \"histogram\": [[330584.0, 54820.0, 67683.0, 57434.0, 30305.0, 14648.0, 9606.0, 5653.0, 2296.0, 1051.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": 0.0, \"percentile_98\": 171.0}, \"b2\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 57.1494356187291, \"count\": 574080.0, \"sum\": 32808348.0, \"std\": 56.300819175100656, \"median\": 37.0, \"majority\": 5.0, \"minority\": 0.0, \"unique\": 256.0, \"histogram\": [[271018.0, 34938.0, 54030.0, 69429.0, 70260.0, 32107.0, 29375.0, 9697.0, 2001.0, 1225.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": 5.0, \"percentile_98\": 180.0}, \"b3\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 51.251764562430324, \"count\": 574080.0, \"sum\": 29422613.0, \"std\": 39.65505035854822, \"median\": 36.0, \"majority\": 16.0, \"minority\": 252.0, \"unique\": 254.0, \"histogram\": [[203263.0, 150865.0, 104882.0, 42645.0, 30652.0, 25382.0, 12434.0, 2397.0, 1097.0, 463.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": 14.0, \"percentile_98\": 158.0}}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:42.866905Z", + "start_time": "2023-04-06T14:25:42.816337Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", - " params = {\n", + " params={\n", " \"url\": cog_url,\n", - " }\n", + " },\n", ").json()\n", "\n", "print(json.dumps(r))" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:42.816337Z", - "end_time": "2023-04-06T14:25:42.866905Z" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ - "This endpoint is far more configurable than `/bounds` and `info`. You can specify which bands to analyse, how to generate the histogram, and pre-process the image.\n", + "This endpoint is far more configurable than `/info`. You can specify which bands to analyse, how to generate the histogram, and pre-process the image.\n", "\n", "For example, if you wanted to get the statistics of the [VARI](https://www.space4water.org/space/visible-atmospherically-resistant-index-vari) of the image you can use the `expression` parameter to conduct simple band math:" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 13, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"(b2-b1)/(b1+b2-b3)\": {\"min\": -1.7976931348623157e+308, \"max\": 1.7976931348623157e+308, \"mean\": null, \"count\": 574080.0, \"sum\": null, \"std\": null, \"median\": -0.15384615384615385, \"majority\": -0.4, \"minority\": -149.0, \"unique\": 18718.0, \"histogram\": [[5646.0, 10176.0, 130905.0, 97746.0, 50184.0, 95842.0, 60322.0, 21478.0, 13552.0, 12204.0], [-1.0, -0.8, -0.6, -0.3999999999999999, -0.19999999999999996, 0.0, 0.20000000000000018, 0.40000000000000013, 0.6000000000000001, 0.8, 1.0]], \"valid_percent\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": -3.5, \"percentile_98\": 3.3870967741935485}}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:43.393610Z", + "start_time": "2023-04-06T14:25:43.304442Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", - " params = {\n", + " params={\n", " \"url\": cog_url,\n", - " \"expression\": \"(b2-b1)/(b1+b2-b3)\", # expression for the VARI\n", - " \"histogram_range\": \"-1,1\"\n", - " }\n", + " \"expression\": \"(b2-b1)/(b1+b2-b3)\", # expression for the VARI\n", + " \"histogram_range\": \"-1,1\",\n", + " },\n", ").json()\n", "\n", "print(json.dumps(r))" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:43.304442Z", - "end_time": "2023-04-06T14:25:43.393610Z" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "Alternatively, if you would like to get statistics for only a certain area, you can specify an area via a feature or a feature collection.\n", "\n", "(Note: this endpoint is not available in the mosaicjson endpoint, only `/cog` and `/stac`)" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 5, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:43.877434Z", + "start_time": "2023-04-06T14:25:43.867923Z" + }, + "collapsed": false + }, "outputs": [], "source": [ "mahebourg = \"\"\"\n", @@ -284,55 +228,40 @@ " ]\n", "}\n", "\"\"\"" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:43.867923Z", - "end_time": "2023-04-06T14:25:43.877434Z" - } - } + ] }, { "cell_type": "code", - "execution_count": 15, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"type\": \"FeatureCollection\", \"features\": [{\"type\": \"Feature\", \"geometry\": {\"type\": \"Polygon\", \"coordinates\": [[[57.70358910197049, -20.384114558699935], [57.68564920588395, -20.384114558699935], [57.68209507552771, -20.39855066753664], [57.68666467170024, -20.421074640746554], [57.70341985766697, -20.434397129770545], [57.72999121319131, -20.42392955694521], [57.70358910197049, -20.384114558699935]]]}, \"properties\": {\"statistics\": {\"b1\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 88.5634794986129, \"count\": 619641.0, \"sum\": 54877563.0, \"std\": 55.18714964714274, \"median\": 77.0, \"majority\": 52.0, \"minority\": 253.0, \"unique\": 256.0, \"histogram\": [[67233.0, 110049.0, 129122.0, 90849.0, 77108.0, 44091.0, 44606.0, 37790.0, 18033.0, 760.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 62.0, \"masked_pixels\": 379783.0, \"valid_pixels\": 619641.0, \"percentile_2\": 4.0, \"percentile_98\": 208.0}, \"b2\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 112.07155594933195, \"count\": 619641.0, \"sum\": 69444131.0, \"std\": 42.64508357271268, \"median\": 107.0, \"majority\": 103.0, \"minority\": 1.0, \"unique\": 256.0, \"histogram\": [[6004.0, 31108.0, 107187.0, 126848.0, 130731.0, 73650.0, 107827.0, 33264.0, 2403.0, 619.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 62.0, \"masked_pixels\": 379783.0, \"valid_pixels\": 619641.0, \"percentile_2\": 34.0, \"percentile_98\": 189.0}, \"b3\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 84.54690377170006, \"count\": 619641.0, \"sum\": 52388728.0, \"std\": 44.64862735915829, \"median\": 77.0, \"majority\": 53.0, \"minority\": 254.0, \"unique\": 256.0, \"histogram\": [[40704.0, 130299.0, 138014.0, 85866.0, 86381.0, 91182.0, 41872.0, 4116.0, 993.0, 214.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 62.0, \"masked_pixels\": 379783.0, \"valid_pixels\": 619641.0, \"percentile_2\": 11.0, \"percentile_98\": 170.0}}}}]}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:45.709013Z", + "start_time": "2023-04-06T14:25:44.592051Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "# NOTE: This is a POST request, unlike all other requests in this example\n", "r = httpx.post(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", " data=mahebourg,\n", - " params = {\n", + " params={\n", " \"url\": cog_url,\n", - " }\n", + " \"max_size\": 1024,\n", + " },\n", + " timeout=20,\n", ").json()\n", "\n", - "print(json.dumps(r))\n" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:44.592051Z", - "end_time": "2023-04-06T14:25:45.709013Z" - } - } + "print(json.dumps(r))" + ] }, { "cell_type": "code", "execution_count": null, + "metadata": {}, "outputs": [], - "source": [], - "metadata": { - "collapsed": false - } + "source": [] } ], "metadata": { @@ -344,14 +273,14 @@ "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.9.19" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_Zarr.ipynb b/docs/src/examples/notebooks/Working_with_Zarr.ipynb new file mode 100644 index 000000000..f1939df86 --- /dev/null +++ b/docs/src/examples/notebooks/Working_with_Zarr.ipynb @@ -0,0 +1,279 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "# Working with Zarr" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## Intro\n", + "\n", + "`titiler.xarray` is a submodule designed specifically for working with multidimensional dataset. With version `0.25.0`, we've introduced a default application with only support for Zarr dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:40.161502Z", + "start_time": "2023-04-06T14:25:40.153667Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "# setup\n", + "import httpx\n", + "import json\n", + "from IPython.display import Image\n", + "\n", + "# Developmentseed Demo endpoint. Please be kind. Ref: https://github.com/developmentseed/titiler/discussions/1223\n", + "# titiler_endpoint = \"https://xarray.titiler.xyz\"\n", + "\n", + "# Or launch your own local instance with:\n", + "# uv run --group server uvicorn titiler.xarray.main:app --host 127.0.0.1 --port 8080 --reload\n", + "titiler_endpoint = \"http://127.0.0.1:8080\"\n", + "\n", + "zarr_url = \"https://nasa-power.s3.us-west-2.amazonaws.com/syn1deg/temporal/power_syn1deg_monthly_temporal_lst.zarr\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## Dataset Metadata\n", + "\n", + "The `/dataset/dict` endpoint returns general metadata about the Zarr Dataset\n", + "\n", + "Endpoint: `/dataset/dict`\n", + "\n", + "QueryParams:\n", + "- **url**: Zarr store URL" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:42.410135Z", + "start_time": "2023-04-06T14:25:42.355858Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/dataset/dict\",\n", + " params={\n", + " \"url\": zarr_url,\n", + " },\n", + ").json()\n", + "\n", + "print(json.dumps(r, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### List of available variables\n", + "\n", + "Endpoint: `/dataset/keys`\n", + "\n", + "QueryParams:\n", + "- **url**: Zarr store URL" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/dataset/keys\",\n", + " params={\n", + " \"url\": zarr_url,\n", + " },\n", + ").json()\n", + "\n", + "print(json.dumps(r, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Variable Info\n", + "\n", + "We can use `/info` endpoint to get more `Geo` information about a specific variable.\n", + "\n", + "QueryParams:\n", + "- **url**: Zarr store URL\n", + "- **variable**: Variable's name (e.g `AIRMASS`, found in `/dataset/keys` response)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/info\",\n", + " params={\"url\": zarr_url, \"variable\": \"AIRMASS\"},\n", + ").json()\n", + "\n", + "print(json.dumps(r, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or as a GeoJSON feature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/info.geojson\",\n", + " params={\"url\": zarr_url, \"variable\": \"AIRMASS\"},\n", + ").json()\n", + "\n", + "print(json.dumps(r, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Knowledge\n", + "\n", + "Looking at the `info` response we can see that the `AIRMASS` variable has `348` (count) bands, each one corresponding to as specific `TIME` (day).\n", + "\n", + "We can also see that the data is stored as `float32` which mean that we will have to apply linear rescaling in order to get output image as PNG/JPEG.\n", + "\n", + "The `min/max` values are also indicated with `valid_max=31.73` and `valid_min=1.0`.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Dimension Reduction\n", + "\n", + "We cannot visualize all the `bands` at once, so we need to perform dimension reduction to go from array in shape (348, 360, 180) to a 1b (1, 360, 180) or 3b (3, 360, 180) image. \n", + "\n", + "To do it, we have two methods whitin `titiler.xarray`:\n", + "- using `bidx=`: same as for COG we can select a band index\n", + "- using `sel={dimension}=value`: which will be using xarray `.sel` method" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/bbox/-180,-90,180,90.png\",\n", + " params=(\n", + " (\"url\", zarr_url),\n", + " (\"variable\", \"AIRMASS\"),\n", + " # Select 1 specific band\n", + " (\"bidx\", 50),\n", + " (\"rescale\", \"1,20\"),\n", + " (\"colormap_name\", \"viridis\"),\n", + " ),\n", + " timeout=10,\n", + ")\n", + "\n", + "Image(r.content)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/bbox/-180,-90,180,90.png\",\n", + " params=(\n", + " (\"url\", zarr_url),\n", + " (\"variable\", \"AIRMASS\"),\n", + " # Select 1 specific time slices\n", + " (\"sel\", \"time=2003-06-30\"),\n", + " (\"rescale\", \"1,20\"),\n", + " (\"colormap_name\", \"viridis\"),\n", + " ),\n", + " timeout=10,\n", + ")\n", + "\n", + "Image(r.content)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/bbox/-180,-90,180,90.png\",\n", + " params=(\n", + " (\"url\", zarr_url),\n", + " (\"variable\", \"AIRMASS\"),\n", + " # Select 3 specific time slices to create a 3 band image\n", + " (\"sel\", \"time=2003-06-30\"),\n", + " (\"sel\", \"time=2004-06-30\"),\n", + " (\"sel\", \"time=2005-06-30\"),\n", + " (\"rescale\", \"1,10\"),\n", + " ),\n", + " timeout=10,\n", + ")\n", + "\n", + "Image(r.content)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3.13 (3.13.7)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/docs/src/examples/notebooks/Working_with_nonWebMercatorTMS.ipynb b/docs/src/examples/notebooks/Working_with_nonWebMercatorTMS.ipynb index c9e728f6b..e2744b8c3 100644 --- a/docs/src/examples/notebooks/Working_with_nonWebMercatorTMS.ipynb +++ b/docs/src/examples/notebooks/Working_with_nonWebMercatorTMS.ipynb @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -51,18 +51,20 @@ " TileLayer,\n", " WMSLayer,\n", " GeoJSON,\n", - " projections\n", + " projections,\n", ")" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"http://127.0.0.1:8081\" # \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", - "url = \"https://s3.amazonaws.com/opendata.remotepixel.ca/cogs/natural_earth/world.tif\" # Natural Earth WORLD tif" + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", + "url = \"https://s3.amazonaws.com/opendata.remotepixel.ca/cogs/natural_earth/world.tif\" # Natural Earth WORLD tif" ] }, { @@ -74,31 +76,11 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "scrolled": false }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Supported TMS:\n", - "- LINZAntarticaMapTilegrid\n", - "- EuropeanETRS89_LAEAQuad\n", - "- CanadianNAD83_LCC\n", - "- UPSArcticWGS84Quad\n", - "- NZTM2000\n", - "- NZTM2000Quad\n", - "- UTM31WGS84Quad\n", - "- UPSAntarcticWGS84Quad\n", - "- WorldMercatorWGS84Quad\n", - "- WGS1984Quad\n", - "- WorldCRS84Quad\n", - "- WebMercatorQuad\n" - ] - } - ], + "outputs": [], "source": [ "r = httpx.get(f\"{titiler_endpoint}/tileMatrixSets\").json()\n", "\n", @@ -119,28 +101,12 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "7d78a0adf5954e65b3f46db3cf943f7a", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Map(center=[0, 0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_text'…" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\", params = {\"url\": url}\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\", params={\"url\": url}\n", ").json()\n", "\n", "m = Map(center=(0, 0), zoom=2, basemap={}, crs=projections.EPSG3857)\n", @@ -161,28 +127,12 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8c5d6ab05bce4aef9e29ff7ebd0ac02f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Map(center=[0, 0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_text'…" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/WorldCRS84Quad/tilejson.json\", params = {\"url\": url}\n", + " f\"{titiler_endpoint}/cog/WorldCRS84Quad/tilejson.json\", params={\"url\": url}\n", ").json()\n", "\n", "m = Map(center=(0, 0), zoom=1, basemap={}, crs=projections.EPSG4326)\n", @@ -203,43 +153,20 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0615b0bb04ad46198d05f6eb95ed8e6b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Map(center=[50, 65], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_tex…" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/EuropeanETRS89_LAEAQuad/tilejson.json\", params = {\"url\": url}\n", + " f\"{titiler_endpoint}/cog/EuropeanETRS89_LAEAQuad/tilejson.json\", params={\"url\": url}\n", ").json()\n", "\n", "my_projection = {\n", - " 'name': 'EPSG:3035',\n", - " 'custom': True, #This is important, it tells ipyleaflet that this projection is not on the predefined ones.\n", - " 'proj4def': '+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs',\n", - " 'origin': [6500000.0, 5500000.0],\n", - " 'resolutions': [\n", - " 8192.0,\n", - " 4096.0,\n", - " 2048.0,\n", - " 1024.0,\n", - " 512.0,\n", - " 256.0\n", - " ]\n", + " \"name\": \"EPSG:3035\",\n", + " \"custom\": True, # This is important, it tells ipyleaflet that this projection is not on the predefined ones.\n", + " \"proj4def\": \"+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs\",\n", + " \"origin\": [6500000.0, 5500000.0],\n", + " \"resolutions\": [8192.0, 4096.0, 2048.0, 1024.0, 512.0, 256.0],\n", "}\n", "\n", "m = Map(center=(50, 65), zoom=0, basemap={}, crs=my_projection)\n", @@ -273,7 +200,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.9.18" }, "vscode": { "interpreter": { diff --git a/docs/src/external_links.md b/docs/src/external_links.md index 9cd60516a..be8a6de95 100644 --- a/docs/src/external_links.md +++ b/docs/src/external_links.md @@ -13,24 +13,38 @@ * David McCracken's Blog on [Plotly Dash Interactive Mapping - Dash Leaflet & TiTiler](https://www.pywram.com/t/blog-plotly-dash-interactive-mapping-dash-leaflet-titiler/287) +## TiTiler extensions/plugins + +* [stac-utils/titiler-pgstac](https://github.com/stac-utils/titiler-pgstac): TiTiler extension which connects to a PgSTAC database to create dynamic mosaics based on search queries. + +* [developmentseed/titiler-xarray](https://github.com/developmentseed/titiler-xarray): TiTiler extension for xarray + +* [developmentseed/titiler-image](https://github.com/developmentseed/titiler-image): TiTiler demo application for Sentinel-2 Digital Twin dataset + + ## Projects / Demo using TiTiler * ESA Charter Mapper [geobrowser](https://docs.charter.uat.esaportal.eu/webPortal/geobrowser/titiler/) * [developmentseed/titiler-digitaltwin](https://github.com/developmentseed/titiler-digitaltwin): TiTiler demo application for Sentinel-2 Digital Twin dataset -* [developmentseed/titiler-pds](https://github.com/developmentseed/titiler-pds): TiTiler demo application for Sentinel-2 and Landsat-8 AWS Public Datasets - * [developmentseed/titiler-mvt](https://github.com/developmentseed/titiler-mvt): TiTiler demo application to create Mapbox Vector Tiles from COG +* [developmentseed/titiler-pds](https://github.com/developmentseed/titiler-pds): TiTiler demo application for Sentinel-2 and Landsat-8 AWS Public Datasets + * [stac-utils/stac-fastapi](https://github.com/stac-utils/stac-fastapi): STAC API implementation with FastAPI. * [c-core-labs/stac-api](https://github.com/c-core-labs/stac-api): STAC compliant API implementation (built from stac-fastapi) -* [lambgeo/titiler-layer](https://github.com/lambgeo/titiler-layer): TiTiler Lambda layers for easy deployment on AWS +* [developmentseed/titiler-lambda-layer](https://github.com/developmentseed/titiler-lambda-layer): TiTiler Lambda layers for easy deployment on AWS * [Terradue/Stars](https://github.com/Terradue/Stars): Spatio Temporal Asset Runtime Services +* [developmentseed/rio-viz](https://github.com/developmentseed/rio-viz): Visualize Cloud Optimized GeoTIFF in browser + +* [developmentseed/pearl-backend](https://github.com/developmentseed/pearl-backend): PEARL (Planetary Computer Land Cover Mapping) Platform API and Infrastructure + +* [microsoft/planetary-computer-apis](https://github.com/microsoft/planetary-computer-apis): Microsoft Planetary Computer APIs ## Conferences / presentations / videos diff --git a/docs/src/img/api_docs.png b/docs/src/img/api_docs.png new file mode 100644 index 000000000..18724abd4 Binary files /dev/null and b/docs/src/img/api_docs.png differ diff --git a/docs/src/img/browser.png b/docs/src/img/browser.png new file mode 100644 index 000000000..965b6b9ac Binary files /dev/null and b/docs/src/img/browser.png differ diff --git a/docs/src/img/false_color.png b/docs/src/img/false_color.png new file mode 100644 index 000000000..f9491c553 Binary files /dev/null and b/docs/src/img/false_color.png differ diff --git a/docs/src/img/full_ndvi.png b/docs/src/img/full_ndvi.png new file mode 100644 index 000000000..f01e1aed7 Binary files /dev/null and b/docs/src/img/full_ndvi.png differ diff --git a/docs/src/img/ndvi_without_color.png b/docs/src/img/ndvi_without_color.png new file mode 100644 index 000000000..866432048 Binary files /dev/null and b/docs/src/img/ndvi_without_color.png differ diff --git a/docs/src/img/server_logs.png b/docs/src/img/server_logs.png new file mode 100644 index 000000000..90cae500d Binary files /dev/null and b/docs/src/img/server_logs.png differ diff --git a/docs/src/img/stac_api_docs.png b/docs/src/img/stac_api_docs.png new file mode 100644 index 000000000..abb1034b4 Binary files /dev/null and b/docs/src/img/stac_api_docs.png differ diff --git a/docs/src/img/stac_asset_info.png b/docs/src/img/stac_asset_info.png new file mode 100644 index 000000000..dc309f089 Binary files /dev/null and b/docs/src/img/stac_asset_info.png differ diff --git a/docs/src/img/stac_asset_preview.png b/docs/src/img/stac_asset_preview.png new file mode 100644 index 000000000..465ded0e5 Binary files /dev/null and b/docs/src/img/stac_asset_preview.png differ diff --git a/docs/src/img/stac_preview_map.png b/docs/src/img/stac_preview_map.png new file mode 100644 index 000000000..f3a316adb Binary files /dev/null and b/docs/src/img/stac_preview_map.png differ diff --git a/docs/src/img/stac_render.png b/docs/src/img/stac_render.png new file mode 100644 index 000000000..d767e72ad Binary files /dev/null and b/docs/src/img/stac_render.png differ diff --git a/docs/src/img/stac_tile_zxy.png b/docs/src/img/stac_tile_zxy.png new file mode 100644 index 000000000..26efcfc15 Binary files /dev/null and b/docs/src/img/stac_tile_zxy.png differ diff --git a/docs/src/intro.md b/docs/src/intro.md deleted file mode 100644 index 8941f6aa0..000000000 --- a/docs/src/intro.md +++ /dev/null @@ -1,129 +0,0 @@ - -![](https://user-images.githubusercontent.com/10407788/203526990-f58783cf-a288-4801-8fa6-ee511de91a48.png) - -`TiTiler` is a set of python modules whose goal are to help users in creating a dynamic tile server. To learn more about `dynamic tiling` please refer to the [docs](dynamic_tiling.md). - -Users can choose to extend or use `TiTiler` as it is. - -## Default Application - -`TiTiler` comes with a default (complete) application with support for COG, STAC, and MosaicJSON. You can install and start the application locally by doing: - -```bash -# Update pip -python -m pip install -U pip - -# Install titiler packages -python -m pip install uvicorn titiler.application - -# Start application using uvicorn -uvicorn titiler.application.main:app - -> INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -See default endpoints documentation pages: - -* [`/cog` - Cloud Optimized GeoTIFF](endpoints/cog.md) -* [`/mosaicjson` - MosaicJSON](endpoints/mosaic.md) -* [`/stac` - Spatio Temporal Asset Catalog](endpoints/stac.md) -* [`/tms` - TileMatrixSets](endpoints/tms.md) - -#### Settings - -The default application can be customized using environment variables defined in `titiler.application.settings.ApiSettings` class. Each variable needs to be prefixed with `TITILER_API_`. - -- `NAME` (str): name of the application. Defaults to `titiler`. -- `CORS_ORIGINS` (str, `,` delimited origins): allowed CORS origin. Defaults to `*`. -- `CACHECONTROL` (str): Cache control header to add to responses. Defaults to `"public, max-age=3600"`. -- `ROOT_PATH` (str): path behind proxy. -- `DEBUG` (str): adds `LoggerMiddleware` and `TotalTimeMiddleware` in the middleware stack. -- `DISABLE_COG` (bool): disable `/cog` endpoints. -- `DISABLE_STAC` (bool): disable `/stac` endpoints. -- `DISABLE_MOSAIC` (bool): disable `/mosaic` endpoints. -- `LOWER_CASE_QUERY_PARAMETERS` (bool): transform all query-parameters to lower case (see https://github.com/developmentseed/titiler/pull/321). - -## Customized, minimal app - -`TiTiler` has been developed so users can build their own app using only the portions they need. Using [TilerFactories](advanced/tiler_factories.md), users can create a fully customized application with only the endpoints needed. - -When building a custom application, you may wish to only install the `core` and/or `mosaic` modules. To install these from PyPI: - -```bash -# Update pip -python -m pip install -U pip - -# Install titiler.core and uvicorn packages -python -m pip install titiler.core uvicorn -``` - -These can then be used like: - -```py -# app.py -import uvicorn -from titiler.core.factory import TilerFactory -from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers - -from fastapi import FastAPI - -app = FastAPI() -cog = TilerFactory() -app.include_router(cog.router) -add_exception_handlers(app, DEFAULT_STATUS_CODES) - - -if __name__ == '__main__': - uvicorn.run(app=app, host="127.0.0.1", port=8080, log_level="info") -``` - -![](img/custom_app.png) - -## Extending TiTiler's app - -If you want to include all of Titiler's built-in endpoints, but also include -customized endpoints, you can import and extend the app directly. - -```bash -python -m pip install titiler.application uvicorn # also installs titiler.core and titiler.mosaic -``` - -These can then be used like: - - -```py -# Add private COG endpoints requiring token validation -from fastapi import APIRouter, Depends, HTTPException, Security -from fastapi.security.api_key import APIKeyQuery - -from titiler.application.main import app -from titiler.core.factory import TilerFactory - - -api_key_query = APIKeyQuery(name="access_token", auto_error=False) - - -def token_validation(access_token: str = Security(api_key_query)): - """stupid token validation.""" - if not access_token: - raise HTTPException(status_code=403, detail="Missing `access_token`") - - # if access_token == `token` then OK - if not access_token == "token": - raise HTTPException(status_code=403, detail="Invalid `access_token`") - - return True - - -# Custom router with token dependency -router = APIRouter(dependencies=[Depends(token_validation)]) -tiler = TilerFactory(router_prefix="private/cog", router=router) - -app.include_router(tiler.router, prefix="/private/cog", tags=["Private"]) - - -if __name__ == '__main__': - uvicorn.run(app=app, host="127.0.0.1", port=8080, log_level="info") -``` - -More on [customization](advanced/customization.md) diff --git a/docs/src/migrations/v1_migration.md b/docs/src/migrations/v1_migration.md new file mode 100644 index 000000000..ab85ff18a --- /dev/null +++ b/docs/src/migrations/v1_migration.md @@ -0,0 +1,282 @@ +# Migration Guide: TiTiler 0.26 to 1.0 + +This guide covers the breaking changes and new features when upgrading from TiTiler 0.26 to 1.0. + +## Breaking Changes + +### Data Type Changes + +#### UINT8 Output for JPEG/PNG + +**Impact:** High - Affects all automatic image format outputs + +When no output format is explicitly specified, TiTiler now returns `UINT8` datatype for JPEG and PNG formats. + +```python +# If your data needs specific datatypes, explicitly specify the format +# Example: Request with explicit format control +# In this case, if the input data is in uint16, the ouput png will be in UINT16 +response = requests.get("/tiles/1/2/3.png?url=data_in_uint16.tif") +``` + +**Action Required:** Review your endpoints that rely on automatic format detection. If you need specific data type handling, consider explicitly specifying the output format and rescaling parameters. + +### WMTS Changes + +#### WMTS Endpoint Restructuring + +**Impact:** High - Affects all WMTS usage + +The `/{tileMatrixSetId}/WMTSCapabilities.xml` endpoints have been removed from the default factories. WMTS functionality is now provided through a dedicated extension. + +```python +# Before (0.26) +# WMTS available at: /{tileMatrixSetId}/WMTSCapabilities.xml + +# Now (1.0) +from titiler.extensions import wmtsExtension + +# Add extension to factory +factory = TilerFactory( + router_prefix="/cog", + extensions=[wmtsExtension()] +) + +# WMTS now available at: /WMTSCapabilities.xml +``` + +Additionally, the WMTS response now supports all TileMatrixSets as separate layers. + +**Action Required:** +1. Add `wmtsExtension` to your factory configurations +2. Update client applications to use the new `/WMTSCapabilities.xml` endpoint path +3. Update any code that expects a single layer to handle multiple TileMatrixSets + +### titiler.core Changes + +#### Point Endpoint Response Model + +**Impact:** Medium - Affects `/point` endpoint consumers + +The `/point` endpoint now includes a `band_description` attribute in its response model. + +```python +# Before (0.26) +class Point(BaseModel): + coordinates: List[float] + values: List[Optional[float]] + # ... other fields + +# Now (1.0) +class Point(BaseModel): + coordinates: List[float] + values: List[Optional[float]] + band_description: List[str] | None # New field + # ... other fields +``` + +**Action Required:** Update client code that parses `/point` responses to handle the new `band_description` field. + +### titiler.mosaic Changes + +#### Point Endpoint Response Restructuring + +**Impact:** High - Affects mosaic `/point` endpoint consumers + +The response model for the mosaic `/point` endpoint has been completely restructured for better clarity. + +```python +# Before (0.26) +class Point(BaseModel): + coordinates: List[float] + values: List[Tuple[str, List[Optional[float]], List[str]]] + +# Now (1.0) +class AssetPoint(BaseModel): + name: str + values: list[float | None] + band_names: list[str] + band_descriptions: list[str] | None = None + +class Point(BaseModel): + coordinates: list[float] + assets: list[AssetPoint] +``` + +**Migration Example:** +```python +# Before (0.26) +response = { + "coordinates": [-122.5, 37.5], + "values": [ + ("asset1", [100.0, 200.0], ["B1", "B2"]), + ("asset2", [150.0, 250.0], ["B1", "B2"]) + ] +} + +# Now (1.0) +response = { + "coordinates": [-122.5, 37.5], + "assets": [ + { + "name": "asset1", + "values": [100.0, 200.0], + "band_names": ["B1", "B2"], + "band_descriptions": None + }, + { + "name": "asset2", + "values": [150.0, 250.0], + "band_names": ["B1", "B2"], + "band_descriptions": None + } + ] +} +``` + +**Action Required:** Update all client code that parses mosaic `/point` responses to use the new structure. + +#### MosaicJSON Dependency Now Optional + +**Impact:** Medium - Affects installations and imports + +The `cogeo-mosaic` package is now an optional dependency. + +```bash +# Before (0.26) +pip install titiler.mosaic + +# Now (1.0) - Install with MosaicJSON support +pip install "titiler.mosaic[mosaicjson]" +``` + +**Action Required:** Update your installation commands if you use MosaicJSON functionality. + +#### Backend Attribute No Longer Has Default + +**Impact:** High - Affects custom mosaic implementations + +The `MosaicTilerFactory.backend` attribute no longer has a default value and must be explicitly specified. + +```python +# Before (0.26) +# Backend had a default value +factory = MosaicTilerFactory() + +# Now (1.0) +# Must explicitly set backend +from titiler.mosaic.backends import MosaicBackend + +factory = MosaicTilerFactory( + backend=MosaicBackend +) +``` + +**Action Required:** Explicitly set the `backend` attribute when creating `MosaicTilerFactory` instances. + +#### MosaicJSON Endpoints Moved to Extension + +**Impact:** Medium - Affects mosaic endpoint structure + +The `/` and `/validate` endpoints are now provided by the `MosaicJSONExtension` instead of being included by default. + +```python +# Before (0.26) +# Endpoints available by default +factory = MosaicTilerFactory() + +# Now (1.0) +from titiler.mosaic.extensions import MosaicJSONExtension + +factory = MosaicTilerFactory( + extensions=[MosaicJSONExtension()] +) +``` + +**Action Required:** Add `MosaicJSONExtension` to your factory if you need the `/` and `/validate` endpoints. + +### titiler.extensions Changes + +#### rio-cogeo Version Update + +**Impact:** Low - Dependency version change + +The `rio-cogeo` requirement has been updated to `7.0,<8.0`. + +**Action Required:** Review the [rio-cogeo changelog](https://github.com/cogeotiff/rio-cogeo/blob/main/CHANGES.md) for any breaking changes that might affect your usage. + +## New Features + +### titiler.mosaic + +#### New Optional Endpoints + +Three new optional endpoints are available for mosaic operations: + +- `/feature` - Feature-based queries +- `/bbox` - Bounding box queries +- `/statistics` - Statistical analysis + +```python +# Enable in your factory +factory = MosaicTilerFactory( + add_feature=True, # Enables /feature endpoint + add_bbox=True, # Enables /bbox endpoint + add_statistics=True # Enables /statistics endpoint +) +``` + +#### WMTS Extension for Mosaics + +A dedicated WMTS extension is now available for mosaic factories: + +```python +from titiler.mosaic.extensions import wmtsExtension + +factory = MosaicTilerFactory( + extensions=[wmtsExtension()] +) +``` + +#### OGC Maps API Support + +Optional OGC Maps API `/map` endpoint is now available: + +```python +factory = MosaicTilerFactory( + add_map=True # Enables /map endpoint +) +``` + +## Dependency Updates + +### Core Dependencies + +- `rio-tiler`: Updated to `>=8.0,<9.0` +- `rio-cogeo`: Updated to `7.0,<8.0` + +**Action Required:** Test your application with the new versions and review their respective changelogs for any behavioral changes. + +## Migration Checklist + +Use this checklist to ensure a smooth migration: + +- [ ] Update WMTS usage to use the new `wmtsExtension` +- [ ] Update client code parsing `/point` responses (both core and mosaic) +- [ ] Explicitly set `backend` attribute for `MosaicTilerFactory` +- [ ] Add `MosaicJSONExtension` if using MosaicJSON endpoints +- [ ] Update installation to include `[mosaicjson]` extra if needed +- [ ] Review and test automatic image format outputs (UINT8 behavior) +- [ ] Update dependency versions: `rio-tiler` and `rio-cogeo` +- [ ] Test with new Python 3.11+ requirement (from 0.25) +- [ ] Update any hardcoded WMTS endpoint paths in client applications +- [ ] Consider adopting new optional endpoints (`/feature`, `/bbox`, `/statistics`) +- [ ] Review OGC Maps API support for your use cases + +## Getting Help + +If you encounter issues during migration: + +1. Check the [GitHub Issues](https://github.com/developmentseed/titiler/issues) +2. Review the [full CHANGELOG](https://github.com/developmentseed/titiler/blob/main/CHANGES.md) +3. Join the discussions in the [TiTiler repository](https://github.com/developmentseed/titiler) diff --git a/docs/src/migrations/v2_migration.md b/docs/src/migrations/v2_migration.md new file mode 100644 index 000000000..9cc460366 --- /dev/null +++ b/docs/src/migrations/v2_migration.md @@ -0,0 +1,243 @@ +# Migrating from TiTiler 1.x to 2.0 + +!!! note + + This migration guide was generated with the help of Claude AI + + +This document describes the breaking changes and migration steps required when upgrading from TiTiler 1.1.1 to 2.0.0. + +## Overview + +TiTiler 2.0.0 introduces several breaking changes primarily focused on: + +1. **Tile sizing**: Replaced `tile_scale` with direct `tilesize` parameter +2. **Dependency simplification**: changed per-asset band indexes (`bidx`) option for multi-asset readers +3. **rio-tiler 9.0 compatibility**: Removed `MultiBandTilerFactory` and related classes + +## Breaking Changes + +### 1. Tile Size Parameter Changes + +#### Removed: `tile_scale` and `@{scale}x` suffix + +The `tile_scale` parameter and `@{scale}x` URL suffix have been removed from all endpoints. + +**Before (1.x):** +``` +GET /tiles/WebMercatorQuad/10/512/384@2x.png?url=... +GET /tilejson.json?url=...&tile_scale=2 +GET /map.html?url=...&tile_scale=2 +GET /WMTSCapabilities.xml?url=...&tile_scale=2 +``` + +**After (2.0):** +``` +GET /tiles/WebMercatorQuad/10/512/384.png?url=...&tilesize=512 +GET /tilejson.json?url=...&tilesize=512 +GET /map.html?url=...&tilesize=256 +GET /WMTSCapabilities.xml?url=... # can't overwrite tilesize anymore +``` + +#### New: `tilesize` query parameter + +A new `tilesize` query parameter is available for tile and tilejson endpoints: + +| Endpoint | Old Default | New Default | +|----------|-------------|-------------| +| `/tiles/{tms}/{z}/{x}/{y}` | 256x256 (scale=1) | TileMatrix's `tileHeight x tileWidth` | +| `/tilejson.json` | 256 | 512 | +| `/map.html` | 256 | 256 | + +**Migration Steps:** +1. Replace `@2x` suffix with `tilesize=512` query parameter +2. Replace `@4x` suffix with `tilesize=1024` query parameter +3. Replace `tile_scale=N` with `tilesize=N*256` query parameter +4. Update any client code that constructs tile URLs with scale suffix + +### 2. Dependency Class Renames + +Several dependency classes have been renamed to reflect their simplified functionality. + +#### Renamed Classes + +| Old Name (1.x) | New Name (2.0) | +|----------------|----------------| +| `AssetsBidxExprParams` | `AssetsExprParams` | + +**Migration Steps:** +```python +# Before (1.x) +from titiler.core.dependencies import AssetsBidxExprParams, AssetsBidxExprParamsOptional + +# After (2.0) +from titiler.core.dependencies import AssetsExprParams +``` + +#### Removed Classes + +The following classes have been removed entirely: + +- `titiler.core.dependencies.AssetsBidxParams` +- `titiler.core.dependencies.BandsParams` +- `titiler.core.dependencies.BandsExprParamsOptional` +- `titiler.core.dependencies.BandsExprParams` + +**Migration Steps:** + +If you were using `BandsParams` or `BandsExprParams` with a custom `MultiBandReader`: +- Consider migrating to a `MultiBaseReader` approach with assets +- Or use expressions to select specific bands + +### 3. Removed Functions + +The following functions have been removed: + +- `titiler.core.dependencies.parse_asset_indexes()` +- `titiler.core.dependencies.parse_asset_expression()` + +These were used internally for parsing `asset_indexes` and `asset_expression` query parameters. + +### 4. Removed: `MultiBandTilerFactory` + +The `MultiBandTilerFactory` class has been completely removed because `MultiBandReader` is no longer available in rio-tiler 9.0. + +**Before (1.x):** +```python +from titiler.core.factory import MultiBandTilerFactory +from rio_tiler.io import MultiBandReader + +class MyBandReader(MultiBandReader): + ... + +tiler = MultiBandTilerFactory(reader=MyBandReader) +``` + +**Migration Steps:** + +1. Convert your `MultiBandReader` to a `MultiBaseReader` that exposes bands as assets +2. Use `MultiBaseTilerFactory` instead + +```python +# After (2.0) +from titiler.core.factory import MultiBaseTilerFactory +from rio_tiler.io import MultiBaseReader + +class MyAssetReader(MultiBaseReader): + # Expose bands as assets + ... + +tiler = MultiBaseTilerFactory(reader=MyAssetReader) +``` + +### 5. `assets` parameter is mandatory for MultiBaseReader + +##### Info and statistics + +`/info` and `/statistics` endpoints now require `assets=` parameter to be set. Users can use special notation `assets=:all:` to get info/statistics for all assets. + +**Before (1.x):** +``` +GET /stac/info?url=... +``` + +**After (2.0):** + +``` +GET /stac/info?url=...&assets=:all: +``` + +##### Expression + +The `expression` parameter cannot be used to define `assets` and thus `assets` parameter must be provided. + +**Before (1.x):** +``` +GET /stac/tiles/WebMercatorQuad/10/512/384.png?url=...&expression=B02_b1+B03_b1 +``` + +**After (2.0):** + +``` +GET /stac/tiles/WebMercatorQuad/10/512/384.png?url=...&assets=B02&assets=B03&expression=b1+b2 +``` + +### 6. `bidx` Parameter Ignored for MultiBaseReader + +The `bidx` (band index) parameter is now **ignored** by `MultiBaseTilerFactory` endpoints. Previously, you could use `bidx` to select bands across all assets. + +**Before (1.x):** +``` +GET /stac/tiles/WebMercatorQuad/10/512/384.png?url=...&assets=B02&assets=B03&bidx=1 +``` + +**After (2.0):** + +Use new `asset` notation: `assets={AssetName}|bidx=1,2,3` + +``` +GET /stac/tiles/WebMercatorQuad/10/512/384.png?url=...&assets=B02|bidx=1&assets=B03|bidx=1 +``` + +### 7. Removed: `asset_indexes` and `asset_expression` Options + +The `asset_indexes` and `asset_expression` query parameters have been removed from `dependencies.py`. + +**Before (1.x):** +``` +GET /stac/preview.png?url=...&asset_indexes=data|1,2,3 +GET /stac/preview.png?url=...&asset_expression=data|b1*b2 +``` + +**After (2.0):** + +Use new `asset` notation: `assets={AssetName}|bidx=1,2,3` or `assets={AssetName}|expression=b1*2` + +``` +GET /stac/preview.png?url=...&assets=B01|bidx=1,2,3 +GET /stac/preview.png?url=...&assets=B01|expression=b1*2 +``` + +### 8. Extension Changes + +#### COG and STAC Viewers + +The `cogViewerExtension` and `stacViewerExtension` now force `tilesize=256` in their tile requests. + +#### WMTS Capabilities + +The `tile_scale` parameter has been removed from `/WMTSCapabilities.xml` endpoints in both `titiler.extensions` and `titiler.mosaic`. + +## API Endpoint Changes Summary + +| Endpoint Pattern | Change | +|-----------------|--------| +| `/tiles/{tms}/{z}/{x}/{y}@{scale}x` | **Removed** - Use `tilesize` query param | +| `/tiles/{tms}/{z}/{x}/{y}@{scale}x.{format}` | **Removed** - Use `tilesize` query param | +| `/tilejson.json?tile_scale=N` | **Removed** - Use `tilesize=N*256` | +| `/map.html?tile_scale=N` | **Removed** - Use `tilesize=N*256` | +| `/WMTSCapabilities.xml?tile_scale=N` | **Removed** | + +## Code Migration Checklist + +- [ ] Replace all `@{scale}x` tile URL suffixes with `tilesize` parameter +- [ ] Replace `tile_scale` query parameter with `tilesize` +- [ ] Update imports: `AssetsBidxExprParams` → `AssetsExprParams` +- [ ] Remove usage of `AssetsBidxParams`, `BandsParams`, `BandsExprParams`, `BandsExprParamsOptional` +- [ ] Migrate `MultiBandTilerFactory` usage to `MultiBaseTilerFactory` +- [ ] Remove `bidx` parameter from multi-asset requests (use `expression` instead) +- [ ] Remove `asset_indexes` and `asset_expression` parameters +- [ ] Use `assets=:all:` for `/info` and `/statistics` endpoints for `MultiBaseTilerFactory` + +## Dependency Updates + +TiTiler 2.0 requires: +- rio-tiler >= 9.0 + +Make sure to update your `pyproject.toml` accordingly. + +## Need Help? + +If you encounter issues during migration: +- Check the [CHANGES.md](https://github.com/developmentseed/titiler/blob/main/CHANGES.md) for detailed release notes +- Open an issue at [https://github.com/developmentseed/titiler/issues](https://github.com/developmentseed/titiler/issues) diff --git a/docs/src/mosaics.md b/docs/src/mosaics.md deleted file mode 100644 index 1695bca95..000000000 --- a/docs/src/mosaics.md +++ /dev/null @@ -1,16 +0,0 @@ - -[Work in Progress] - -![](img/africa_mosaic.png) - -`Titiler` has native support for reading and creating web map tiles from **MosaicJSON**. - -> MosaicJSON is an open standard for representing metadata about a mosaic of Cloud-Optimized GeoTIFF (COG) files. - -Ref: https://github.com/developmentseed/mosaicjson-spec - - -### Links - -- https://medium.com/devseed/cog-talk-part-2-mosaics-bbbf474e66df -- https://github.com/developmentseed/cogeo-mosaic diff --git a/docs/src/overrides/partials/integrations/analytics/plausible.html b/docs/src/overrides/partials/integrations/analytics/plausible.html new file mode 100644 index 000000000..6be309101 --- /dev/null +++ b/docs/src/overrides/partials/integrations/analytics/plausible.html @@ -0,0 +1,46 @@ + + + + diff --git a/docs/src/packages/application.md b/docs/src/packages/application.md new file mode 120000 index 000000000..9f698a377 --- /dev/null +++ b/docs/src/packages/application.md @@ -0,0 +1 @@ +../../../src/titiler/application/README.md \ No newline at end of file diff --git a/docs/src/packages/core.md b/docs/src/packages/core.md new file mode 120000 index 000000000..fff7ecdd5 --- /dev/null +++ b/docs/src/packages/core.md @@ -0,0 +1 @@ +../../../src/titiler/core/README.md \ No newline at end of file diff --git a/docs/src/packages/extensions.md b/docs/src/packages/extensions.md new file mode 120000 index 000000000..6fcc9e3d1 --- /dev/null +++ b/docs/src/packages/extensions.md @@ -0,0 +1 @@ +../../../src/titiler/extensions/README.md \ No newline at end of file diff --git a/docs/src/packages/mosaic.md b/docs/src/packages/mosaic.md new file mode 120000 index 000000000..cf87cb31c --- /dev/null +++ b/docs/src/packages/mosaic.md @@ -0,0 +1 @@ +../../../src/titiler/mosaic/README.md \ No newline at end of file diff --git a/docs/src/packages/xarray.md b/docs/src/packages/xarray.md new file mode 120000 index 000000000..dc85e70b9 --- /dev/null +++ b/docs/src/packages/xarray.md @@ -0,0 +1 @@ +../../../src/titiler/xarray/README.md \ No newline at end of file diff --git a/docs/src/security.md b/docs/src/security.md new file mode 120000 index 000000000..42cce94fd --- /dev/null +++ b/docs/src/security.md @@ -0,0 +1 @@ +../../SECURITY.md \ No newline at end of file diff --git a/docs/src/advanced/Algorithms.md b/docs/src/user_guide/algorithms.md similarity index 76% rename from docs/src/advanced/Algorithms.md rename to docs/src/user_guide/algorithms.md index eb823e9fb..4d0ec8a7d 100644 --- a/docs/src/advanced/Algorithms.md +++ b/docs/src/user_guide/algorithms.md @@ -6,11 +6,25 @@ The algorithms are meant to overcome the limitation of `expression` (using [nume We added a set of custom algorithms: -- `hillshade`: Create hillshade from elevation dataset -- `contours`: Create contours lines (raster) from elevation dataset -- `terrarium`: Mapzen's format to encode elevation value in RGB values (https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium) -- `terrainrgb`: Mapbox's format to encode elevation value in RGB values (https://docs.mapbox.com/data/tilesets/guides/access-elevation-data/) +- `hillshade`: Create hillshade from elevation dataset (parameters: azimuth (45), angle_altitude(45)) +- `contours`: Create contours lines (raster) from elevation dataset (parameters: increment (35), thickness (1)) +- `slope`: Create degrees of slope from elevation dataset +- `terrarium`: [Mapzen's format](https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium) to encode elevation value in RGB values `elevation = (red * 256 + green + blue / 256) - 32768` +- `terrainrgb`: [Mapbox](https://docs.mapbox.com/data/tilesets/guides/access-elevation-data/)/[Maptiler](https://docs.maptiler.com/guides/map-tilling-hosting/data-hosting/rgb-terrain-by-maptiler/)'s format to encode elevation value in RGB values `elevation = -10000 + ((red * 256 * 256 + green * 256 + blue) * 0.1)` - `normalizedIndex`: Normalized Difference Index (e.g NDVI) +- `cast`: Cast data to integer +- `floor`: Round data to the smallest integer +- `ceil`: Round data to the largest integer + +- `min`: Return **Min** values along the `bands` axis. +- `max`: Return **Max** values along the `bands` axis. +- `median`: Return **Median** values along the `bands` axis. +- `mean`: Return **Mean** values along the `bands` axis. +- `std`: Return the **Standard Deviation** along the `bands` axis. +- `var`: Return **Variance** along the `bands` axis. +- `sum`: Return **Sum** along the `bands` axis. +- `grayscale`: Return a **grayscale** version of an image using ITU-R 601-2 luma transformation. +- `bitonal`: All values larger than 127 are set to 255 (white), all other values to 0 (black). ### Usage @@ -78,7 +92,7 @@ This base class defines that algorithm: - can have`parameters` (enabled by `extra = "allow"` pydantic config) -Here is a simple example of a custom Algorithm: +Here is a simple example of a custom algorithm: ```python from titiler.core.algorithm import BaseAlgorithm @@ -109,7 +123,7 @@ class Multiply(BaseAlgorithm): Using a Pydantic's `BaseModel` class to construct the custom algorithm enables two things **parametrization** and **type casting/validation**. -If we look at the `Multiply` algorithm, we can see it needs a `factor` parameter. In Titiler (in the post_process dependency) we will pass this parameter via query string (e.g `/preview.png?algo=multiply&algo_parameter={"factor":3}`) and pydantic will make sure we use the right types/values. +If we look at the `Multiply` algorithm, we can see it needs a `factor` parameter. In TiTiler (in the post_process dependency) we will pass this parameter via query string (e.g `/preview.png?algo=multiply&algo_parameter={"factor":3}`) and pydantic will make sure we use the right types/values. ```python # Available algorithm @@ -131,7 +145,7 @@ def post_process_dependency( ## Dependency -To be able to use your own algorithm in titiler's endpoint you need to create a `Dependency` to tell the application what algorithm are available. +To be able to use your own algorithm in TiTiler's endpoint, you need to create a `Dependency` to tell the application which algorithms are available. To ease the dependency creation, we added a `dependency` property in the `titiler.core.algorithm.Algorithms` class, which will return a FastAPI dependency to be added to the endpoints. @@ -154,7 +168,7 @@ endpoints = TilerFactory(process_dependency=PostProcessParams) ### Order of operation -When creating a map tile (or other images), we will fist apply the `algorithm` then the `rescaling` and finally the `color_formula`. +When creating a map tile (or other images), we will first apply the `algorithm`, then the `rescaling`, and finally the `color_formula`. ```python with reader(url as src_dst: diff --git a/docs/src/dynamic_tiling.md b/docs/src/user_guide/dynamic_tiling.md similarity index 82% rename from docs/src/dynamic_tiling.md rename to docs/src/user_guide/dynamic_tiling.md index 15b9ebaf6..7b96a25a1 100644 --- a/docs/src/dynamic_tiling.md +++ b/docs/src/user_guide/dynamic_tiling.md @@ -5,9 +5,9 @@ TiTiler's first goal is to create a lightweight but performant dynamic tile server... but what do we mean by this? -When you zoom/pan on a web map, you are visualizing either vector or raster data that is loaded by your web client (e.g Chrome). Vector Tiles are rendered **On the Fly**, meaning the map library (e.g MapboxGL) will apply styling on the vector it receives to create a visual representation on the map. This is possible because vector data can be encoded and compressed very efficiently and result in each tile being only couple of kilo octets. +When you zoom/pan on a web map, you are visualizing either vector or raster data that is loaded by your web client (e.g Chrome). Vector Tiles are rendered **On the Fly**, meaning the map library (e.g Mapbox GL-JS) will apply the styling on the vector it receives to create a visual representation on the map. This is possible because vector data can be encoded and compressed very efficiently and result in each tile being only a couple of kilo octets. -On the other side, raster data is a really dense format, a `256 x 256 x 3` tile (True color image) needs to encode `196 608` values, and depending on the data type (Integer, Float, Complex), a raster tile can be really heavy. Depending on the dataset data type, some operations might be needed in order to obtain a visual representation (e.g. rescaling, colormap, ... ). Map library will almost only accept Uint8 RGB(A) tile encoded as PNG, JPEG or Webp. +On the other side, raster data is a really dense format, a `256 x 256 x 3` tile (True color image) needs to encode `196 608` values, and depending on the data type (Integer, Float, Complex), a raster tile can be really heavy. Depending on the dataset data type, some operations might be needed in order to obtain a visual representation (e.g. rescaling, colormap, ... ). The map library will almost only accept Uint8 RGB(A) tile encoded as PNG, JPEG or Webp. ## **Static tiling** @@ -68,9 +68,9 @@ The goal of the `Dynamic Tiling` process is to get rid of all the pre-processing ## Summary -With `Static` tile generation you are often limited because you are visualizing data that is fixed and stored somewhere on a disk. With `Dynamic tiling`, users have the possibility to apply their own choice of processing (e.g rescaling, masking) before creating the `image`. +With `Static` tile generation, you are often limited because you are visualizing data that is fixed and stored somewhere on a disk. With `Dynamic tiling`, users have the possibility to apply their own choice of processing (e.g rescaling, masking) before creating the `image`. -Static tiling will always be faster than dynamic tiling, but a cache layer can be set up in front of the dynamic tiler, but using a dynamic tiler often means that same tile won't be serve twice (because users can set multiple options). +Static tiling will always be faster to load than dynamic tiling, but a cache layer can be set up in front of the dynamic tiler. Using a dynamic tiler often means that the same tile won't be served twice (because users can set multiple options). ## Links [https://medium.com/devseed/cog-talk-part-1-whats-new-941facbcd3d1](https://medium.com/devseed/cog-talk-part-1-whats-new-941facbcd3d1) diff --git a/docs/src/user_guide/getting_started.md b/docs/src/user_guide/getting_started.md new file mode 100644 index 000000000..29a1a2118 --- /dev/null +++ b/docs/src/user_guide/getting_started.md @@ -0,0 +1,382 @@ +[TiTiler](https://developmentseed.org/titiler) is a modern map tile server that helps developers quickly serve geospatial data on the web. Think of it as a specialized tool that takes large geographic files (like satellite imagery) and slices them into small, web-friendly map tiles that load efficiently in browser-based maps. + +Built on FastAPI, TiTiler makes working with Cloud-Optimized GeoTIFFs, Spatio Temporal Asset Catalog and other spatial data formats straightforward, even if you're not a GIS expert. It handles all the complex work of processing geographic data and serving it through simple API endpoints that any web developer can use. + +In the past, putting maps on websites was a real pain. Developers had to use bulky tools like GeoServer that were hard to set up, or spend hours making thousands of **static** tiny map images with tools like gdal2tiles that couldn't be changed later. TiTiler makes this so much easier. It creates **dynamic** map pieces right when you need them, instead of making them all beforehand. It works great with modern cloud data and doesn't need complicated setup. This means less headache and more time to focus on building cool map features that users will love. + +## Dynamic vs. Static Tiles: What's the Difference? + +Static tiles are like pre-printed map pieces stored in folders. Once created, they're locked—changing anything means starting over. They use lots of storage, but load quickly. + +TiTiler's dynamic tiles work like a chef cooking to order. When someone views your map, TiTiler grabs just the data needed and creates tiles on the spot. This lets you instantly change colors, adjust contrast, or highlight different features. Your map becomes flexible and responsive, adapting to what users need right now, rather than being stuck with choices made earlier. + +More on [Dynamic Tiling](dynamic_tiling.md) + +## Let's Get TiTiler Up and Running! + +Now that we understand the advantage of TiTiler's dynamic approach, let's get it running on your local machine. Follow these steps: + +### **1. Create Your Project Workspace** + +First, let's create a dedicated space for our TiTiler project. Open your terminal (Command Prompt or PowerShell on Windows, Terminal on macOS/Linux) and run: + +```bash +# Works on all operating systems +mkdir Titiler +cd Titiler +``` + +> 💡 **Pro Tip**: Keeping your TiTiler project in its own folder makes it easier to manage and prevents conflicts with other Python projects. + +### **2. Set Up a Python Virtual Environment** +a. Create the virtual environment: + ```bash + python -m venv titiler + ``` +b. Activate the virtual environment: + - **For Linux/macOS:** + ```bash + source titiler/bin/activate + ``` + - **For Windows:** + ```bash + titiler\Scripts\activate + ``` + +### **3. Install TiTiler and Its Dependencies** + +With your environment activated, install TiTiler and the web server it needs: + +```bash +pip install titiler.core uvicorn +``` + +> ⚠️ **Warning**: Previously, TiTiler was available via a `titiler` metapackage, but that's no longer the case. +> In late 2025, we [dropped support for this metapackage](https://github.com/developmentseed/titiler/issues/294). + +This command installs the core TiTiler package and Uvicorn, a lightning-fast ASGI server. + +> 💡 **What's happening**: TiTiler.core contains the essential functionality for serving map tiles. Uvicorn is the engine that will run our FastAPI application. + +### **4. Create Your TiTiler Application** + +Now for the fun part! Create a file named `main.py` with the following code: + +```python +from fastapi import FastAPI +from titiler.core.factory import TilerFactory + +from starlette.middleware.cors import CORSMiddleware + +app = FastAPI() + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allows all origins (for development - be more specific in production) + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Create a TilerFactory for Cloud-Optimized GeoTIFFs +cog = TilerFactory() + +# Register all the COG endpoints automatically +app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) + + +# Optional: Add a welcome message for the root endpoint +@app.get("/") +def read_index(): + return {"message": "Welcome to TiTiler"} +``` + +> 💡 **Code Breakdown**: +> +> - We create a FastAPI app and add CORS middleware to allow web maps to access our images +> - The `TilerFactory()` creates all the endpoints needed for serving COG tiles +> - We include those endpoints in our app with `app.include_router()` +> - A simple home endpoint provides a welcome message + +### **5. Launch Your TiTiler Server** + +Run the following command to start the server: +```bash +uvicorn main:app --reload +``` +You should see an output similar to this: + +![server logs](../img/server_logs.png) + +> 💡 **The `--reload` flag** automatically restarts the server whenever you change your code - perfect for development! + +### **6. Explore Your TiTiler API** + +Open your browser and go to: + +``` http://127.0.0.1:8000/ ``` - See your welcome message + +![browser](../img/browser.png) + +``` http://127.0.0.1:8000/docs ``` - Explore the interactive API documentation. The `/docs` page is your mission control center. It shows all the endpoints TiTiler created for you and lets you test them directly in your browser: + +![api docs](../img/api_docs.png) + +## Visualizing Your Geospatial Data + +Now that your server is running, let's see what it can do with real data! + +### **Quick Preview of Your Raster** + +To get a quick preview of any Cloud-Optimized GeoTIFF, use: + +```bash +http://127.0.0.1:8000/preview?url=file:///path_to_your_raster.tif +``` +> ⚠️ **Note**: Replace the path with the actual path to your COG file. Remember to use the full path for local files. + +## Visualizing a Specific Tile (Z, X, Y) + +When working with web maps, understanding tile coordinates is essential. Let's break down what Z, X, Y values mean: + +- **Z (zoom level)**: How far in/out you're zoomed. Lower numbers (0-5) show the whole world with less detail; higher numbers (15-22) show smaller areas with more detail. +- **X (column)**: Horizontal position, increasing eastward. +- **Y (row)**: Vertical position, increasing southward. + +At zoom level 0, there's just 1 tile for the whole world. Each zoom level increase splits each tile into 4 more detailed tiles. + +### **Why Visualize Specific Tiles?** + +- **Performance**: Load only what users can see +- **Debugging**: Inspect problematic tiles +- **Specific Analysis**: Extract data from exact locations + +### **Finding Z, X, Y for Your Image** + +The `rio_tiler` and `morecantile` library makes this straightforward: + +```python +from rio_tiler.io import Reader +import morecantile + +# Web Mercator is the default tiling scheme for most web map clients +WEB_MERCATOR_TMS = morecantile.tms.get("WebMercatorQuad") + +with Reader('/path/to/your/raster.tif', tms=WEB_MERCATOR_TMS) as src: + bbox = src.get_geographic_bounds("epsg:4326") + zoom = 15 + # Find all tiles covering the bounding box + tiles = list(src.tms.tiles(bbox[0], bbox[1], bbox[2], bbox[3], zoom)) + for t in tiles: + print("Tile coordinate (x, y, z):", t.x, t.y, t.z) +``` + +### **Viewing a Specific Tile in TiTiler** + +For example, if your tile has coordinates `x=5412, y=12463, z=15`, you would access the specific tile with: + +```bash +http://127.0.0.1:8000/tiles/WebMercatorQuad/15/5412/12463.png?url=raster.tif +``` + +URL components explained: + +- **`WebMercatorQuad/`**: The tiling scheme (this should match your raster's CRS - TiTiler will reproject on-the-fly if needed, but using the correct scheme improves performance and accuracy) +- **`{z}/{x}/{y}`**: Your tile coordinates +- **`.png`**: Output format (alternatives: `.jpg`, `.webp`, `.tif`) +- **`?url=raster.tif`**: Source raster file + +More on [Tiling Schemes](tile_matrix_sets.md) + +### **Creating a Web Map with Leaflet** + +[Leaflet](https://leafletjs.com/) is a lightweight, open-source JavaScript library for interactive maps. It lets you combine base maps (like OpenStreetMap) with overlays from custom tile servers such as TiTiler. + +The following code (in **map.html**) loads a base map, adds your TiTiler raster overlay, and automatically sets the map's view to the raster's bounds: + +--- + +
+ map.html Code + +```html + + + + Leaflet Basemap + TiTiler Raster Overlay + + + + + + +
+ + + +``` +
+ +--- + +## Troubleshooting Common Issues + +### **CORS Issues** + +If you encounter "Access to fetch at X has been blocked by CORS policy" errors in your browser console, make sure you: + +- Have included the CORS middleware in `main.py` as shown above +- Restart your TiTiler server after making changes + +### **File Not Found Errors** + +When using `file:///` URLs: +- Make sure to use the absolute path to your file with the correct format for your operating system: + + - Windows: `file:///C:/Users/username/data/image.tif` + - macOS: `file:///Users/username/data/image.tif` + - Linux: `file:///home/username/data/image.tif` + +### **No Tiles Showing** + +If your map loads but your tiles don't appear: + +- Check the browser console for errors +- Verify that your GeoTIFF is Cloud-Optimized (use `rio cogeo validate` from the rio-cogeo package) +- Try different zoom levels - your data might not be visible at all scales + +--- +*Created by [Dimple Jain](https://jaiindimple.github.io)* + + +## Default Application + +`TiTiler` comes with a default (complete) application with support for COG, STAC, and MosaicJSON. You can install and start the application locally by doing: + +```bash +# Update pip +python -m pip install -U pip + +# Install titiler packages +python -m pip install uvicorn titiler.application + +# Start application using uvicorn +uvicorn titiler.application.main:app + +> INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +See the default endpoints documentation pages: + +* [`/cog` - Cloud Optimized GeoTIFF](../endpoints/cog.md) +* [`/mosaicjson` - MosaicJSON](../endpoints/mosaic.md) +* [`/stac` - Spatio Temporal Asset Catalog](../endpoints/stac.md) +* [`/tileMatrixSets` - Tiling Schemes](../endpoints/tms.md) +* [`/algorithms` - Algorithms](../endpoints/algorithms.md) +* [`/colorMaps` - ColorMaps](../endpoints/colormaps.md) + +#### Settings + +The default application can be customized using environment variables defined in `titiler.application.settings.ApiSettings` class. Each variable needs to be prefixed with `TITILER_API_`. + +- `NAME` (str): name of the application. Defaults to `titiler`. +- `CORS_ORIGINS` (str, `,` delimited origins): allowed CORS origin. Defaults to `*`. +- `CORS_ALLOW_METHODS` (str, `,` delimited methods): allowed CORS methods. Defaults to `GET`. +- `CACHECONTROL` (str): Cache control header to add to responses. Defaults to `"public, max-age=3600"`. +- `ROOT_PATH` (str): path behind proxy. +- `DEBUG` (str): adds `LoggerMiddleware` and `TotalTimeMiddleware` in the middleware stack. +- `DISABLE_COG` (bool): disable `/cog` endpoints. +- `DISABLE_STAC` (bool): disable `/stac` endpoints. +- `DISABLE_MOSAIC` (bool): disable `/mosaic` endpoints. +- `LOWER_CASE_QUERY_PARAMETERS` (bool): transform all query-parameters to lower case (see https://github.com/developmentseed/titiler/pull/321). +- `GLOBAL_ACCESS_TOKEN` (str | None): a string which is required in the `?access_token=` query param with every request. + + +#### Extending TiTiler's app + +If you want to include all of Titiler's built-in endpoints, but also include +customized endpoints, you can import and extend the app directly. + +```bash +python -m pip install titiler.application uvicorn # also installs titiler.core and titiler.mosaic +``` + +These can then be used like: + +```py +# Add private COG endpoints requiring token validation +from fastapi import APIRouter, Depends, HTTPException, Security +from fastapi.security.api_key import APIKeyQuery + +from titiler.application.main import app +from titiler.core.factory import TilerFactory + +import uvicorn + +api_key_query = APIKeyQuery(name="access_token", auto_error=False) + + +def token_validation(access_token: str = Security(api_key_query)): + """stupid token validation.""" + if not access_token: + raise HTTPException(status_code=401, detail="Missing `access_token`") + + # if access_token == `token` then OK + if not access_token == "token": + raise HTTPException(status_code=401, detail="Invalid `access_token`") + + return True + + +# Custom router with token dependency +router = APIRouter(dependencies=[Depends(token_validation)]) +tiler = TilerFactory(router_prefix="private/cog", router=router) + +app.include_router(tiler.router, prefix="/private/cog", tags=["Private"]) + + +if __name__ == '__main__': + uvicorn.run(app=app, host="127.0.0.1", port=8080, log_level="info") +``` + +More on [customization](../advanced/customization.md) diff --git a/docs/src/output_format.md b/docs/src/user_guide/output_format.md similarity index 94% rename from docs/src/output_format.md rename to docs/src/user_guide/output_format.md index 8d32ad577..31882086d 100644 --- a/docs/src/output_format.md +++ b/docs/src/user_guide/output_format.md @@ -2,7 +2,7 @@ `TiTiler` supports the common output format for map tiles: JPEG, PNG and WEBP. While some formats (e.g PNG) are able to encode Uint16 or Float datatypes, most web browsers only supports 8 bit data (meaning that it has to be between 0 and 255). -It's on the user to know what datatype is the input source (COG), and what kind of `post processing` there is to do to create a valid web map tile. +It's on the user to know what datatype is the input source (COG), and what kind of `post processing` is required to create a valid web map tile. `TiTiler` also has support for more complex output data formats, such as JPEG2000 or GeoTIFF. While it might not be useful for FrontEnd display (most browsers can't decode GeoTIFF natively), some users could want to transmit the data as `raw` values to some applications (non-web display). @@ -47,7 +47,7 @@ print(data.shape) data, mask = data[0:-1], data[-1] ``` -Notebook: [Working_with_NumpyTile](examples/notebooks/Working_with_NumpyTile.ipynb) +Notebook: [Working_with_NumpyTile](../examples/notebooks/Working_with_NumpyTile.ipynb) ## JSONResponse diff --git a/docs/src/user_guide/rendering.md b/docs/src/user_guide/rendering.md new file mode 100644 index 000000000..b679ecd4e --- /dev/null +++ b/docs/src/user_guide/rendering.md @@ -0,0 +1,133 @@ +# Rendering Options + +When using Titiler to visualize imagery, there are some helper options that change how the data appears on the screen. You can: + +1. Adjust the band values using basic color-oriented image operations +2. Apply color maps to create heat maps, colorful terrain based on band value +3. Rescale images on a per-band basis + +## Color Map + +Color maps are arrays of colors, used to map pixel values to specific colors. For example, it is possible to map a single band DEM, where the pixel values denote height, to a color map which shows higher values as white: + +![color map example](../img/colormap.png) + +TiTiler supports both default colormaps (each with a name) and custom color maps. + +### Default Colormaps + +Default colormaps pre-made, each with a given name. These maps come from the `rio-tiler` library, which has taken colormaps packaged with Matplotlib and has added others that are commonly used with raster data. + +A list of available color maps can be found in Titiler's Swagger docs, or in the [rio-tiler documentation](https://cogeotiff.github.io/rio-tiler/colormap/#default-rio-tilers-colormaps). + +To use a default colormap, simply use the parameter `colormap_name`: + +```python +import httpx + +resp = httpx.get( + "https://titiler.xyz/cog/preview", + params={ + "url": "", + "colormap_name": "" # e.g. autumn_r + } +) +``` + +You can take any of the colormaps listed on `rio-tiler`, and add `_r` to reverse it. + +### Custom Colormaps + +If you'd like to specify your own colormap, you can specify your own using an encoded JSON: + +```python +import httpx + +response = httpx.get( + "https://titiler.xyz/cog/preview", + params={ + "url": "", + "bidx": "1", + "colormap": json.dumps({ + "0": "#e5f5f9", + "10": "#99d8c9", + "255": "#2ca25f", + }) + } +) +``` + +TiTiler supports colormaps that are both discrete (where pixels will be one of the colors that you specify) and linear (where pixel colors will blend between the given colors). + +For more information, please check out [rio-tiler's docs](https://cogeotiff.github.io/rio-tiler/colormap/). + +It is also possible to add a [colormap dependency](../examples/code/tiler_with_custom_colormap) to automatically apply +a default colormap. + +## Color Formula + +Color formulae are simple commands that apply color corrections to images. This is useful for reducing artefacts like atmospheric haze, dark shadows, or muted colors. + +Titiler supports color formulae as defined in [Mapbox's `rio-color` plugin](https://github.com/mapbox/rio-color). These include the operations ([taken from the `rio-color` docs](https://github.com/mapbox/rio-color#operations)): + +- **Gamma** adjustment: adjusts RGB values according to a power law, effectively brightening or darkening the midtones. It can be very effective in satellite imagery for reducing atmospheric haze in the blue and green bands. + +- **Sigmoidal** contrast adjustment: can alter the contrast and brightness of an image in a way that matches human's non-linear visual perception. It works well to increase contrast without blowing out the very dark shadows or already-bright parts of the image. + +- **Saturation**: can be thought of as the "colorfulness" of a pixel. Highly saturated colors are intense and almost cartoon-like, low saturation is more muted, closer to black and white. You can adjust saturation independently of brightness and hue, but the data must be transformed into a different color space. + +In TiTiler, color_formulae are applied through the `color_formula` parameter as a string. An example of this option in action: + +```python +import httpx + +response = httpx.get( + "https://titiler.xyz/cog/preview", + params={ + "url": "", + "color_formula": "gamma rg 1.3, sigmoidal rgb 22 0.1, saturation 1.5" + } +) +``` + +## Rescaling + +Rescaling is the act of adjusting the minimum and maximum values when rendering an image. In an image with a single band, the rescaled minimum value will be set to black, and the rescaled maximum value will be set to white. This is useful if you want to accentuate features that only appear at a certain pixel value (e.g. you have a DEM, but you want to highlight how the terrain changes between sea level and 100m). + +All TiTiler endpoinds returning *image* support `rescale` parameter. The parameter should be in form of `"rescale={min},{max}"`. + +```python +import httpx + +response = httpx.get( + "https;//titiler.xyz/cog/preview", + params={ + "url": "", + "rescale": "0,100", + }, +) +``` + +TiTiler supports rescaling on a per-band basis, using multiple `rescale` parameters. + +```python +import httpx + +response = httpx.get( + "https;//titiler.xyz/cog/preview", + params=( + ("url", ""), + ("rescale", "0,100"), + ("rescale", "0,1000"), + ("rescale", "0,10000"), + ), +) +``` + +By default, TiTiler will rescale the bands using the min/max values of the input datatype. For example, PNG images 8 or 16-bit unsigned pixels, giving a possible range of 0 to 255 or 0 to 65,536, so Titiler will use these ranges to rescale to the output format. + +For certain datasets (e.g. DEMs), this default behaviour can make the image seem washed out (or even entirely one color), +so if you see this happen look into rescaling your images to something that makes sense for your data. + +It is also possible to add a [rescaling dependency](../api/titiler/core/dependencies/#ImageRenderingParams) to automatically apply +a default rescale. diff --git a/docs/src/tile_matrix_sets.md b/docs/src/user_guide/tile_matrix_sets.md similarity index 91% rename from docs/src/tile_matrix_sets.md rename to docs/src/user_guide/tile_matrix_sets.md index eea52d992..3488ff54e 100644 --- a/docs/src/tile_matrix_sets.md +++ b/docs/src/user_guide/tile_matrix_sets.md @@ -26,7 +26,7 @@ $ curl http://127.0.0.1:8000/tileMatrixSets | jq '.tileMatrixSets[] | .id' "WebMercatorQuad" ``` -You can easily add more TileMatrixSet support, see [custom tms](advanced/customization.md#custom-tms). +You can easily add more TileMatrixSet support, see [custom tms](../advanced/customization.md#custom-tms). -Notebook: [Working_with_nonWebMercatorTMS](examples/notebooks/Working_with_nonWebMercatorTMS.ipynb) +Notebook: [Working_with_nonWebMercatorTMS](../examples/notebooks/Working_with_nonWebMercatorTMS.ipynb) diff --git a/docs/src/user_guide/titiler_with_stac.md b/docs/src/user_guide/titiler_with_stac.md new file mode 100644 index 000000000..fdd11cf1e --- /dev/null +++ b/docs/src/user_guide/titiler_with_stac.md @@ -0,0 +1,397 @@ +In our [previous post](https://developmentseed.org/titiler/user_guide/getting_started/), we set up TiTiler for serving Cloud-Optimized GeoTIFFs. Now let's explore how TiTiler works with STAC (SpatioTemporal Asset Catalog) - a standardized way to describe and organize geospatial data. + +> **Prerequisites**: For Python environment and TiTiler setup, check out the [Getting Started with TiTiler](https://developmentseed.org/titiler/user_guide/getting_started/#lets-get-titiler-up-and-running) post. + +## What is [STAC](https://stacspec.org/en)? + +STAC is like a library catalog for satellite imagery. Instead of searching through folders, you get structured JSON files that tell you: + +- **What**: Type of imagery, bands available, resolution +- **Where**: Geographic location (bounding box, geometry) +- **When**: Capture date and time +- **How**: Direct links to the actual data files (assets) + +## Our Example Dataset + +Throughout this tutorial, we'll use openly available Maxar satellite imagery from the Bay of Bengal Cyclone Mocha event: +```bash +https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json +``` + +This STAC item contains: + +| Asset | Description | Bands | +|-------|-------------|-------| +| `visual` | True color RGB | Red, Green, Blue | +| `ms_analytic` | Multispectral | Coastal, Blue, Green, Yellow, Red, RedEdge, NIR1, NIR2 | +| `pan_analytic` | Panchromatic | Single band (high resolution) | +| `data-mask` | Valid data mask | Single band | + + +> **Tip**: Explore other open datasets at [STAC Index](https://stacindex.org/catalogs) to practice with different imagery + +## Setting Up TiTiler for STAC + +Make sure your `main.py` includes the STAC router: + +```python +from fastapi import FastAPI +from titiler.core.factory import TilerFactory, MultiBaseTilerFactory +from starlette.middleware.cors import CORSMiddleware +from rio_tiler.io import STACReader + +app = FastAPI(title="TiTiler") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# COG endpoints +cog = TilerFactory() +app.include_router(cog.router, prefix="/cog", tags=["COG"]) + +# STAC endpoints +stac = MultiBaseTilerFactory(reader=STACReader) +app.include_router(stac.router, prefix="/stac", tags=["STAC"]) + +@app.get("/") +def read_index(): + return {"message": "Welcome to TiTiler!"} +``` + +**What's new for STAC**: We import `STACReader` from `rio_tiler.io` and use `MultiBaseTilerFactory` (instead of `TilerFactory`) to create STAC endpoints - this factory understands how to read multiple assets from a single STAC item. + +Start the server: +```bash +uvicorn main:app --reload +``` + +Open your browser and go to: +``` http://127.0.0.1:8000/docs ``` - Explore the interactive API documentation. + +![](../img/stac_api_docs.png) + +*** + +## 1. Getting STAC Item Info `/stac/info`: + +Before visualizing, let's understand what's in our STAC item. The `/stac/info` endpoint returns metadata about available assets and their properties. + +### List all available assets + +```bash +http://127.0.0.1:8000/stac/assets?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json +``` + +### Basic Info Request + +```bash +http://127.0.0.1:8000/stac/info?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=:all: +``` + +!!! note + + `assets=:all:` special notation tells titiler to fetch info for all available asset + +### Info for Specific Asset + +To get detailed information about a specific asset: + +```bash +http://127.0.0.1:8000/stac/info?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=visual +``` + +![](../img/stac_asset_info.png) + +*** + +## 2. Quick Preview: `/stac/preview` + +The `/stac/preview` endpoint generates a downsampled image of your data - perfect for quick visualization. + +### Preview the Visual Asset (RGB) + +```bash +http://127.0.0.1:8000/stac/preview?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=visual +``` + +True Color visual asset: +![](../img/stac_asset_preview.png) + +### Preview with Output Format + +You can specify the output format: + +```bash +# JPEG output (smaller file size) +http://127.0.0.1:8000/stac/preview.jpg?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=visual + +# WebP output (modern format, good compression) +http://127.0.0.1:8000/stac/preview.webp?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=visual +``` + +*** + +## 3. Map Tiles: `/stac/tiles/{tileMatrixSetId}/{z}/{x}/{y}` + +For web maps, you need tiles - small image pieces that load progressively as users pan and zoom. This is the most powerful endpoint for building interactive maps. + +### Finding Tile Coordinates (Z, X, Y) + +To request a specific tile, you need to know its coordinates. You can calculate these from the STAC item's bounding box using the `morecantile` library. + +**Important**: STAC items have two types of bounding boxes: + +- **`bbox`** - Always in WGS84 (lon/lat) - **use this for tile calculations** +- **`proj:bbox`** - In the asset's projection (e.g., UTM meters) - don't use this directly + +```python +import httpx +import morecantile + +# Fetch the STAC item +stac_url = "https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json" +item = httpx.get(stac_url).json() + +# Get the WGS84 bbox (not proj:bbox!) +bbox = item["bbox"] # [92.724, 20.481, 92.761, 20.530] + +# Calculate center point +lon = (bbox[0] + bbox[2]) / 2 # 92.743 +lat = (bbox[1] + bbox[3]) / 2 # 20.506 +zoom = 15 + +# Load the WebMercatorQuad TileMatrixSet +tms = morecantile.tms.get("WebMercatorQuad") + +# Get the tile containing this point +tile = tms.tile(lon, lat, zoom) +print(f"z={tile.z}, x={tile.x}, y={tile.y}") +# Output: z=15, x=24825, y=14476 +``` + +> **Tip**: Install morecantile with `python -m pip install morecantile` + +### Basic Tile Request + +```bash +http://127.0.0.1:8000/stac/tiles/WebMercatorQuad/15/24825/14476.png?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=visual +``` +![](../img/stac_tile_zxy.png) + + +**URL breakdown**: + +- `WebMercatorQuad`: Standard web map projection (EPSG:3857) +- `15`: Zoom level +- `24825`: X tile coordinate +- `14476`: Y tile coordinate +- `.png`: Output format + +### Using `assets` Parameter + +The `assets` parameter specifies which asset(s) to render: + +```bash +# Single asset (visual - RGB) +&assets=visual + +# Multiple assets (for band combinations from different assets) +&assets=ms_analytic&assets=pan_analytic +``` + +### Passing per Asset options using `{name}|{options}={...}` + +When working with multi-band asset, you can extend the **asse**t option to select specific bands or apply expression: + +!!! note + + Support of `options` in asset's name may depends on implementation. `rio-tiler`'s STACReader supports both `indexes=` and `expression=`: + + - Indexes: `assets=ms_analytic|indexes=1,2,3` + - Expression: `assets=ms_analytic|expression=b1+b2` + + +```bash +# Select band 3 (Green) from ms_analytic +http://127.0.0.1:8000/stac/tiles/WebMercatorQuad/15/24825/14476.png?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=ms_analytic|indexes=3 +``` + +**Band indices for `ms_analytic`**: + +| Index | Band Name | Common Name | +|-------|-----------|-------------| +| 1 | BAND_C | Coastal | +| 2 | BAND_B | Blue | +| 3 | BAND_G | Green | +| 4 | BAND_Y | Yellow | +| 5 | BAND_R | Red | +| 6 | BAND_RE | Red Edge | +| 7 | BAND_N | NIR (Near Infrared) | +| 8 | BAND_N2 | NIR2 | + +### Creating RGB Composites with `{asset}|indexes={}` + +Create false-color composites by specifying 3 bands: + +```bash +# Natural Color (Red, Green, Blue - bands 5,3,2) +&assets=ms_analytic|indexes=5,3,2 + +# False Color Infrared (NIR, Red, Green - bands 7,5,3) +&assets=ms_analytic|indexes=7,5,3 + +# Agriculture (NIR, Green, Blue - bands 7,3,2) +&assets=ms_analytic|indexes=7,3,2 +``` + +### Using `rescale` - Adjust Value Range + +**Important**: Raw satellite data often has values outside the 0-255 display range. Without rescaling, images may appear black or washed out. + +> **Tip**: If your image appears black or too dark, try adjusting the rescale max value. Adjust based on your data min-max. + +**Full example - False Color Infrared with rescale**: +```bash +http://127.0.0.1:8000/stac/tiles/WebMercatorQuad/15/24825/14476.png?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=ms_analytic&asset_bidx=ms_analytic|7,5,3&rescale=0,2000 +``` + +![](../img/false_color.png) + +### Using `expression` - Band Math + +The `expression` parameter lets you perform calculations across bands. This is powerful for creating vegetation indices, water indices, and more. + +**Syntax**: Bands are referenced as `b{index}` (e.g., `b7` for NIR band in the `ms_analytic` asset). + +!!! important + + For STAC, the band index `b{index}` within an expression corresponds to the index of the resulting image created using multiple assets. + If you combine two assets with 2 bands each, the resulting images will have 4 bands, thus an expression could accept `b1 -> b4`. + + ``` + # select bands 1 & 2 for two assets and apply an expression + assets=visual|indexes=1,2&assets=ms_analytic|indexes=1,2&expression=(b1+b2+b3+b4)/4 + ``` + +!!! warning + + The `+` sign in URLs is interpreted as a space! Use `%2B` instead of `+` in your expressions, otherwise you'll get a syntax error. + + +#### NDVI (Normalized Difference Vegetation Index) + +NDVI highlights vegetation: `(NIR - Red) / (NIR + Red)` + +```bash +http://127.0.0.1:8000/stac/tiles/WebMercatorQuad/15/24825/14476.png?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=ms_analytic&expression=(b7-b5)/(b7%2Bb5)&rescale=-1,1 +``` + +![](../img/ndvi_without_color.png) + +#### NDWI (Normalized Difference Water Index) + +NDWI highlights water bodies: `(Green - NIR) / (Green + NIR)` + +```bash +http://127.0.0.1:8000/stac/tiles/WebMercatorQuad/15/24825/14476.png?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=ms_analytic&expression=(b3-b7)/(b3%2Bb7)&rescale=-1,1 +``` + +#### Simple Band Ratio + +```bash +# NIR/Red ratio (vegetation vigor) +&assets=ms_analytic&expression=b7/b5 +``` + +### Using `colormap_name` - Apply Color Palettes + +When using expressions (which return single-band results), apply colormaps to make the data meaningful: + +**[List of Available colormaps](https://titiler.xyz/colorMaps)** + +#### NDVI with Colormap + +```bash +http://127.0.0.1:8000/stac/tiles/WebMercatorQuad/15/24825/14476.png?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=ms_analytic&expression=(b7-b5)/(b7%2Bb5)&colormap_name=rdylgn&rescale=-1,1 +``` + +- `colormap_name=rdylgn`: Red-Yellow-Green colormap (red=low NDVI, green=high NDVI) +- `rescale=-1,1`: NDVI values range from -1 to 1 + +#### Water Index with Blue Colormap + +```bash +http://127.0.0.1:8000/stac/tiles/WebMercatorQuad/15/24825/14476.png?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=ms_analytic&expression=(b3-b7)/(b3%2Bb7)&colormap_name=blues&rescale=-1,1 +``` + +### Visualize at the Image Extent or Crop to Custom Bounds + +**NDVI on Image Extent** +```bash +http://127.0.0.1:8000/stac/bbox/92.724,20.481,92.761,20.530.png?url=https://maxar-opendata.s3.amazonaws.com/events/BayofBengal-Cyclone-Mocha-May-23/ard/46/033111333030/2023-05-22/10300110E84B5A00.json&assets=ms_analytic&expression=(b7-b5)/(b7%2Bb5)&colormap_name=rdylgn&rescale=-1,1 +``` + +![](../img/full_ndvi.png) + +**URL breakdown:** + + - `/stac/bbox/92.724,20.481,92.761,20.530.png` - Bounding box: `minLon,minLat,maxLon,maxLat` + - Get these values from the STAC item's `bbox` field + +*** +### Using with Leaflet + +```html + + + + STAC + TiTiler Map + + + + +
+ + + +``` + +**To view the map:** + + 1. Save the code as `map.html` + 2. Make sure TiTiler server is running (`uvicorn main:app --reload`) + 3. Open `map.html` in your browser (double-click or drag into browser) + +![](../img/stac_preview_map.png) + +## Common Issues + +**Asset not found**: Check asset names in the STAC item JSON - they're case-sensitive. + +**Black/white tiles**: Your data values might be outside the default range. Use `rescale` to adjust. + +**Slow tiles**: Large files take time. Consider using overviews or lower zoom levels for previews. + +--- +*Created by [Dimple Jain](https://jaiindimple.github.io)* \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b691b6110..787a115dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "titiler" description = "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.11" license = {file = "LICENSE"} authors = [ {name = "Vincent Sarago", email = "vincent@developmentseed.com"}, @@ -21,31 +21,63 @@ classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: GIS", ] -version="0.11.7" +version="2.0.0b2" dependencies = [ - "titiler.core==0.11.7", - "titiler.extensions==0.11.7", - "titiler.mosaic==0.11.7", - "titiler.application==0.11.7", + "titiler-core", + "titiler-xarray", + "titiler-extensions", + "titiler-mosaic[mosaicjson]", + "titiler-application", ] -[project.optional-dependencies] +[dependency-groups] dev = [ + "pytest", + "pytest-cov", + "pytest-asyncio", + "httpx", + "obstore", + "zarr>=3,<4.0", + "h5netcdf", + "h5py", + "fsspec", + "s3fs>=2025.2.0", + "aiohttp", + "requests", + "pystac[validation]>=1.0.0,<2.0.0", + "brotlipy", + "boto3", + "owslib", "pre-commit", + "IPython", + "jupyter", + "matplotlib", + "folium", +] +telemetry = [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-instrumentation-fastapi", + "opentelemetry-instrumentation-logging", + "opentelemetry-exporter-otlp", ] docs = [ - "nbconvert", - "mkdocs", - "mkdocs-jupyter", - "mkdocs-material", - "pygments", - "pdocs", + "black>=23.10.1", + "mkdocs>=1.4.3", + "mkdocs-jupyter>=0.24.5", + "mkdocs-material[imaging]>=9.5", + "griffe-inherited-docstrings>=1.0.0", + "mkdocstrings[python]>=0.25.1", +] +server = [ + "uvicorn", ] [project.urls] @@ -55,33 +87,20 @@ Issues = "https://github.com/developmentseed/titiler/issues" Source = "https://github.com/developmentseed/titiler" Changelog = "https://developmentseed.org/titiler/release-notes/" -[tool.hatch.build.targets.sdist] -exclude = [ - ".binder", - ".pytest_cache", - ".ruff_cache", - ".vscode", - ".dockerignore", - "src/", - "deployment/", - "docs/", - "scripts/", - "dockerfiles/", - "docker-compose.yml", - ".github", - ".history", - ".bumpversion.cfg", - ".flake8", - ".gitignore", - ".pre-commit-config.yaml", - "AUTHORS.txt", - "CHANGES.md", - "CONTRIBUTING.md", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +[tool.uv] +package = false + +[tool.uv.sources] +titiler-core = { workspace = true } +titiler-extensions = { workspace = true } +titiler-mosaic = { workspace = true } +titiler-application = { workspace = true } +titiler-xarray = { workspace = true } + +[tool.uv.workspace] +members = [ + "src/titiler/*" +] [tool.coverage.run] branch = true @@ -107,7 +126,7 @@ known_third_party = [ ] default_section = "THIRDPARTY" -[tool.ruff] +[tool.ruff.lint] select = [ "D1", # pydocstyle errors "E", # pycodestyle errors @@ -121,9 +140,138 @@ ignore = [ "B008", # do not perform function calls in argument defaults "B905", # ignore zip() without an explicit strict= parameter, only support with python >3.10 ] +exclude = [ + "*.ipynb" +] [tool.mypy] no_implicit_optional = true strict_optional = true namespace_packages = true explicit_package_bases = true + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::rasterio.errors.NotGeoreferencedWarning", +] + +[tool.bumpversion] +current_version = "2.0.0b2" +parse = """(?x) + (?P\\d+)\\. + (?P\\d+)\\. + (?P\\d+) + (?: + (?Pa|b|rc|dev) # pre-release label + (?P\\d+) # pre-release version number + )? # pre-release section is optional + (?: + \\.post + (?P\\d+) # post-release version number + )? # post-release section is optional +""" +serialize = [ + "{major}.{minor}.{patch}.post{post_n}", + "{major}.{minor}.{patch}{pre_l}{pre_n}", + "{major}.{minor}.{patch}", +] + +search = "{current_version}" +replace = "{new_version}" +regex = false +tag = false +commit = false +allow_dirty = true +tag_name = "{new_version}" + +############################################################################### +# update titiler meta package +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'version="{current_version}"' +replace = 'version="{new_version}"' + +############################################################################### +# Update sub modules version +# titiler.core +[[tool.bumpversion.files]] +filename = "src/titiler/core/titiler/core/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +# titiler.xarray +[[tool.bumpversion.files]] +filename = "src/titiler/xarray/titiler/xarray/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +# titiler.extensions +[[tool.bumpversion.files]] +filename = "src/titiler/extensions/titiler/extensions/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +# titiler.mosaic +[[tool.bumpversion.files]] +filename = "src/titiler/mosaic/titiler/mosaic/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +# titiler.application +[[tool.bumpversion.files]] +filename = "src/titiler/application/titiler/application/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +############################################################################### +# Update sub modules dependencies +# titiler.xarray +[[tool.bumpversion.files]] +filename = "src/titiler/xarray/pyproject.toml" +search = 'titiler-core=={current_version}' +replace = 'titiler-core=={new_version}' + +# titiler.extensions +[[tool.bumpversion.files]] +filename = "src/titiler/extensions/pyproject.toml" +search = 'titiler-core=={current_version}' +replace = 'titiler-core=={new_version}' + +# titiler.mosaic +[[tool.bumpversion.files]] +filename = "src/titiler/mosaic/pyproject.toml" +search = 'titiler-core=={current_version}' +replace = 'titiler-core=={new_version}' + +# titiler.application +[[tool.bumpversion.files]] +filename = "src/titiler/application/pyproject.toml" +search = 'titiler-core[telemetry]=={current_version}' +replace = 'titiler-core[telemetry]=={new_version}' + +[[tool.bumpversion.files]] +filename = "src/titiler/application/pyproject.toml" +search = 'titiler-xarray=={current_version}' +replace = 'titiler-xarray=={new_version}' + +[[tool.bumpversion.files]] +filename = "src/titiler/application/pyproject.toml" +search = 'titiler-extensions[cogeo,stac]=={current_version}' +replace = 'titiler-extensions[cogeo,stac]=={new_version}' + +[[tool.bumpversion.files]] +filename = "src/titiler/application/pyproject.toml" +search = 'titiler-mosaic[mosaicjson]=={current_version}' +replace = 'titiler-mosaic[mosaicjson]=={new_version}' + +############################################################################### +# Others +[[tool.bumpversion.files]] +filename = "deployment/aws/lambda/Dockerfile" +search = 'titiler-application=={current_version}' +replace = 'titiler-application=={new_version}' + +[[tool.bumpversion.files]] +filename = "deployment/k8s/charts/Chart.yaml" +search = 'appVersion: {current_version}' +replace = 'appVersion: {new_version}' diff --git a/scripts/publish b/scripts/publish index 7e548c6ec..8689fbfe2 100755 --- a/scripts/publish +++ b/scripts/publish @@ -1,22 +1,17 @@ #! /usr/bin/env bash -SUBPACKAGE_DIRS=( +SUBPACKAGES=( "core" + "xarray" "mosaic" "application" "extensions" ) -for PACKAGE_DIR in "${SUBPACKAGE_DIRS[@]}" +for PACKAGE in "${SUBPACKAGES[@]}" do - echo "publishing titiler-${PACKAGE_DIR}" - pushd ./src/titiler/${PACKAGE_DIR} + echo "publishing titiler-${PACKAGE}" rm -rf dist - python -m build - twine upload dist/* - popd + uv build --package titiler-${PACKAGE} + uv publish done - -rm -rf dist -python -m build -twine upload dist/* diff --git a/scripts/test b/scripts/test new file mode 100755 index 000000000..beb1409d8 --- /dev/null +++ b/scripts/test @@ -0,0 +1,15 @@ +#! /usr/bin/env bash + +SUBPACKAGE_DIRS=( + "core" + "xarray" + "mosaic" + "application" + "extensions" +) + +for PACKAGE_DIR in "${SUBPACKAGE_DIRS[@]}" +do + echo "Running tests for titiler-${PACKAGE_DIR}" + uv run pytest src/titiler/${PACKAGE_DIR} +done diff --git a/src/titiler/application/LICENSE b/src/titiler/application/LICENSE new file mode 100644 index 000000000..eb4365951 --- /dev/null +++ b/src/titiler/application/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Development Seed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/titiler/application/README.md b/src/titiler/application/README.md index d8ca172eb..ee9dde7a7 100644 --- a/src/titiler/application/README.md +++ b/src/titiler/application/README.md @@ -6,19 +6,19 @@ ## Installation ```bash -$ pip install -U pip +python -m pip install -U pip # From Pypi -$ pip install titiler.application +python -m pip install titiler.application # Or from sources -$ git clone https://github.com/developmentseed/titiler.git -$ cd titiler && pip install -e titiler/application +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core -e src/titiler/extensions -e src/titiler/mosaic -e src/titiler/xarray -e src/titiler/application ``` Launch Application ```bash -$ pip install uvicorn +$ python -m pip install uvicorn $ uvicorn titiler.application.main:app --reload ``` @@ -30,9 +30,7 @@ titiler/ ├── tests/ - Tests suite └── titiler/application/ - `application` namespace package ├── templates/ - | ├── index.html - demo landing page - | ├── cog_index.html - demo viewer for `/cog` - | └── stac_index.html - demo viewer for `/stac` + | └── index.html - Landing page ├── main.py - Main FastAPI application └── settings.py - demo settings (cache, cors...) ``` diff --git a/src/titiler/application/pyproject.toml b/src/titiler/application/pyproject.toml index f88aa0750..f03411be5 100644 --- a/src/titiler/application/pyproject.toml +++ b/src/titiler/application/pyproject.toml @@ -1,8 +1,8 @@ [project] -name = "titiler.application" +name = "titiler-application" description = "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.11" authors = [ {name = "Vincent Sarago", email = "vincent@developmentseed.com"}, ] @@ -21,31 +21,36 @@ classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: GIS", ] dynamic = ["version"] dependencies = [ - "titiler.core==0.11.7", - "titiler.extensions[cogeo,stac]==0.11.7", - "titiler.mosaic==0.11.7", - "starlette-cramjam>=0.3,<0.4", - "python-dotenv", + "titiler-core[telemetry]==2.0.0b2", + "titiler-xarray==2.0.0b2", + "titiler-extensions[cogeo,stac]==2.0.0b2", + "titiler-mosaic[mosaicjson]==2.0.0b2", + "starlette-cramjam>=0.4,<0.6", + "pydantic-settings~=2.0", ] [project.optional-dependencies] +server = [ + "uvicorn[standard]>=0.12.0", +] + +[dependency-groups] test = [ "pytest", "pytest-cov", "pytest-asyncio", "httpx", "brotlipy", -] -server = [ - "uvicorn[standard]>=0.12.0,<0.19.0", + "boto3", ] [project.urls] @@ -55,14 +60,15 @@ Issues = "https://github.com/developmentseed/titiler/issues" Source = "https://github.com/developmentseed/titiler" Changelog = "https://developmentseed.org/titiler/release-notes/" -[build-system] -requires = ["pdm-pep517"] -build-backend = "pdm.pep517.api" - -[tool.pdm.version] -source = "file" +[tool.hatch.version] path = "titiler/application/__init__.py" -[tool.pdm.build] -includes = ["titiler/application"] -excludes = ["tests/", "**/.mypy_cache", "**/.DS_Store"] +[tool.hatch.build.targets.sdist] +only-include = ["titiler"] + +[tool.hatch.build.targets.wheel] +only-include = ["titiler"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/src/titiler/application/tests/conftest.py b/src/titiler/application/tests/conftest.py index 967990102..b883ab570 100644 --- a/src/titiler/application/tests/conftest.py +++ b/src/titiler/application/tests/conftest.py @@ -34,6 +34,7 @@ def app(set_env) -> TestClient: def mock_RequestGet(src_path): """Mock Requests.""" + # HTTP class MockResponse: def __init__(self, data): diff --git a/src/titiler/application/tests/fixtures/dataset_3d.zarr/.zattrs b/src/titiler/application/tests/fixtures/dataset_3d.zarr/.zattrs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/titiler/application/tests/fixtures/dataset_3d.zarr/.zattrs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/titiler/application/tests/fixtures/dataset_3d.zarr/.zgroup b/src/titiler/application/tests/fixtures/dataset_3d.zarr/.zgroup new file mode 100644 index 000000000..3b7daf227 --- /dev/null +++ b/src/titiler/application/tests/fixtures/dataset_3d.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/src/titiler/application/tests/fixtures/dataset_3d.zarr/.zmetadata b/src/titiler/application/tests/fixtures/dataset_3d.zarr/.zmetadata new file mode 100644 index 000000000..061ce5cb0 --- /dev/null +++ b/src/titiler/application/tests/fixtures/dataset_3d.zarr/.zmetadata @@ -0,0 +1,119 @@ +{ + "metadata": { + ".zattrs": {}, + ".zgroup": { + "zarr_format": 2 + }, + "dataset/.zarray": { + "chunks": [ + 1, + 250, + 500 + ], + "compressor": { + "blocksize": 0, + "clevel": 5, + "cname": "lz4", + "id": "blosc", + "shuffle": 1 + }, + "dtype": "cogeo" in response.content.decode() assert ( - "http://testserver/cog/tiles/WebMercatorQuad/{TileMatrix}/{TileCol}/{TileRow}@1x.png?url=https" + "http://testserver/cog/tiles/WebMercatorQuad/{TileMatrix}/{TileCol}/{TileRow}.png?url=https" + in response.content.decode() + ) + assert ( + "http://www.opengis.net/def/crs/EPSG/0/3857" in response.content.decode() ) response = app.get( - "/cog/WMTSCapabilities.xml?url=https://myurl.com/cog.tif&tile_scale=2&tile_format=jpg" + "/cog/WMTSCapabilities.xml?url=https://myurl.com/cog.tif&use_epsg=true" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/xml" - assert ( - "http://testserver/cog/tiles/WebMercatorQuad/{TileMatrix}/{TileCol}/{TileRow}@2x.jpg?url=https" - in response.content.decode() - ) + assert "EPSG:3857" in response.content.decode() @patch("rio_tiler.io.rasterio.rasterio") @@ -86,7 +74,7 @@ def test_tile(rio, app): # full tile response = app.get( - "/cog/tiles/8/87/48?url=https://myurl.com/cog.tif&rescale=0,1000" + "/cog/tiles/WebMercatorQuad/8/87/48?url=https://myurl.com/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" @@ -96,7 +84,7 @@ def test_tile(rio, app): assert meta["height"] == 256 response = app.get( - "/cog/tiles/8/87/48@2x?url=https://myurl.com/cog.tif&rescale=0,1000&color_formula=Gamma R 3" + "/cog/tiles/WebMercatorQuad/8/87/48?url=https://myurl.com/cog.tif&rescale=0,1000&tilesize=512&color_formula=Gamma R 3" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" @@ -105,25 +93,19 @@ def test_tile(rio, app): assert meta["height"] == 512 response = app.get( - "/cog/tiles/8/87/48.jpg?url=https://myurl.com/cog.tif&rescale=0,1000" + "/cog/tiles/WebMercatorQuad/8/87/48.jpg?url=https://myurl.com/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpg" response = app.get( - "/cog/tiles/8/87/48.jpeg?url=https://myurl.com/cog.tif&rescale=0,1000" + "/cog/tiles/WebMercatorQuad/8/87/48.jpeg?url=https://myurl.com/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" response = app.get( - "/cog/tiles/8/87/48@2x.jpg?url=https://myurl.com/cog.tif&rescale=0,1000" - ) - assert response.status_code == 200 - assert response.headers["content-type"] == "image/jpg" - - response = app.get( - "/cog/tiles/8/87/48@2x.tif?url=https://myurl.com/cog.tif&nodata=0&bidx=1" + "/cog/tiles/WebMercatorQuad/8/87/48.tif?url=https://myurl.com/cog.tif&nodata=0&bidx=1&tilesize=512" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" @@ -133,14 +115,16 @@ def test_tile(rio, app): assert meta["width"] == 512 assert meta["height"] == 512 - response = app.get("/cog/tiles/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0") + response = app.get( + "/cog/tiles/WebMercatorQuad/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" data = numpy.load(BytesIO(response.content)) assert data.shape == (2, 256, 256) response = app.get( - "/cog/tiles/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false" + "/cog/tiles/WebMercatorQuad/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -150,7 +134,7 @@ def test_tile(rio, app): # Test brotli compression headers = {"Accept-Encoding": "br"} response = app.get( - "/cog/tiles/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", + "/cog/tiles/WebMercatorQuad/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", headers=headers, ) assert response.status_code == 200 @@ -159,7 +143,7 @@ def test_tile(rio, app): # Exclude png from compression middleware headers = {"Accept-Encoding": "br"} response = app.get( - "/cog/tiles/8/87/48.png?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", + "/cog/tiles/WebMercatorQuad/8/87/48.png?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", headers=headers, ) assert response.status_code == 200 @@ -168,7 +152,7 @@ def test_tile(rio, app): # Test gzip fallback headers = {"Accept-Encoding": "gzip"} response = app.get( - "/cog/tiles/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", + "/cog/tiles/WebMercatorQuad/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", headers=headers, ) assert response.status_code == 200 @@ -176,19 +160,23 @@ def test_tile(rio, app): # partial response = app.get( - "/cog/tiles/8/84/47?url=https://myurl.com/cog.tif&rescale=0,1000" + "/cog/tiles/WebMercatorQuad/8/84/47?url=https://myurl.com/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" response = app.get( - "/cog/tiles/8/84/47?url=https://myurl.com/cog.tif&nodata=0&rescale=0,1000&colormap_name=viridis" + "/cog/tiles/WebMercatorQuad/8/84/47?url=https://myurl.com/cog.tif&nodata=0&rescale=0,1000&colormap_name=viridis" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" - cmap = urlencode( - { + # valid colormap + response = app.get( + "/cog/tiles/WebMercatorQuad/8/53/50.png", + params={ + "url": "https://myurl.com/above_cog.tif", + "bidx": 1, "colormap": json.dumps( { "1": [58, 102, 24, 255], @@ -196,34 +184,31 @@ def test_tile(rio, app): "3": "#b1b129", "4": "#ddcb9aFF", } - ) - } - ) - response = app.get( - f"/cog/tiles/8/53/50.png?url=https://myurl.com/above_cog.tif&bidx=1&{cmap}" + ), + }, ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" - cmap = urlencode({"colormap": json.dumps({"1": [58, 102]})}) + # invalid colormap shape response = app.get( - f"/cog/tiles/8/53/50.png?url=https://myurl.com/above_cog.tif&bidx=1&{cmap}" + "/cog/tiles/WebMercatorQuad/8/53/50.png", + params={ + "url": "https://myurl.com/above_cog.tif", + "bidx": 1, + "colormap": json.dumps({"1": [58, 102]}), + }, ) assert response.status_code == 400 - cmap = urlencode({"colormap": {"1": "#ddcb9aFF"}}) + # bad resampling response = app.get( - f"/cog/tiles/8/53/50.png?url=https://myurl.com/above_cog.tif&bidx=1&{cmap}" - ) - assert response.status_code == 400 - - response = app.get( - "/cog/tiles/8/53/50.png?url=https://myurl.com/above_cog.tif&bidx=1&resampling=somethingwrong" + "/cog/tiles/WebMercatorQuad/8/53/50.png?url=https://myurl.com/above_cog.tif&bidx=1&resampling=somethingwrong" ) assert response.status_code == 422 response = app.get( - "/cog/tiles/8/87/48@2x.tif?url=https://myurl.com/cog.tif&nodata=0&bidx=1&return_mask=false" + "/cog/tiles/WebMercatorQuad/8/87/48.tif?url=https://myurl.com/cog.tif&nodata=0&bidx=1&return_mask=false&tilesize=512" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" @@ -239,44 +224,41 @@ def test_tilejson(rio, app): """test /tilejson endpoint.""" rio.open = mock_rasterio_open - response = app.get("/cog/tilejson.json?url=https://myurl.com/cog.tif") + response = app.get( + "/cog/WebMercatorQuad/tilejson.json?url=https://myurl.com/cog.tif" + ) assert response.status_code == 200 body = response.json() - assert body["tilejson"] == "2.2.0" + assert body["tilejson"] == "3.0.0" assert body["version"] == "1.0.0" assert body["scheme"] == "xyz" assert len(body["tiles"]) == 1 assert body["tiles"][0].startswith( - "http://testserver/cog/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=https" + "http://testserver/cog/tiles/WebMercatorQuad/{z}/{x}/{y}?url=https" ) assert body["minzoom"] == 5 assert body["maxzoom"] == 9 assert body["bounds"] assert body["center"] - response = app.get( - "/cog/tilejson.json?url=https://myurl.com/cog.tif&tile_format=png&tile_scale=2" - ) - assert response.status_code == 200 - body = response.json() - assert body["tiles"][0].startswith( - "http://testserver/cog/tiles/WebMercatorQuad/{z}/{x}/{y}@2x.png?url=https" - ) - cmap_dict = { "1": [58, 102, 24, 255], "2": [100, 177, 41], "3": "#b1b129", "4": "#ddcb9aFF", } - cmap = urlencode({"colormap": json.dumps(cmap_dict)}) response = app.get( - f"/cog/tilejson.json?url=https://myurl.com/above_cog.tif&bidx=1&{cmap}" + "/cog/WebMercatorQuad/tilejson.json", + params={ + "url": "https://myurl.com/above_cog.tif", + "bidx": 1, + "colormap": json.dumps(cmap_dict), + }, ) assert response.status_code == 200 body = response.json() assert body["tiles"][0].startswith( - "http://testserver/cog/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=https" + "http://testserver/cog/tiles/WebMercatorQuad/{z}/{x}/{y}?url=https" ) query = dict(parse_qsl(urlparse(body["tiles"][0]).query)) assert json.loads(query["colormap"]) == cmap_dict @@ -340,11 +322,11 @@ def test_preview(rio, app): @patch("rio_tiler.io.rasterio.rasterio") def test_part(rio, app): - """test /crop endpoint.""" + """test /bbox endpoint.""" rio.open = mock_rasterio_open response = app.get( - "/cog/crop/-56.228,72.715,-54.547,73.188.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" + "/cog/bbox/-56.228,72.715,-54.547,73.188.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -355,7 +337,7 @@ def test_part(rio, app): assert meta["driver"] == "PNG" response = app.get( - "/cog/crop/-56.228,72.715,-54.547,73.188.jpg?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256&return_mask=false" + "/cog/bbox/-56.228,72.715,-54.547,73.188.jpg?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256&return_mask=false" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpg" @@ -366,7 +348,7 @@ def test_part(rio, app): assert meta["driver"] == "JPEG" response = app.get( - "/cog/crop/-56.228,72.715,-54.547,73.188/128x128.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" + "/cog/bbox/-56.228,72.715,-54.547,73.188/128x128.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -376,7 +358,7 @@ def test_part(rio, app): assert meta["driver"] == "PNG" response = app.get( - "/cog/crop/-56.228,72.715,-54.547,73.188.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256&width=512&height=512" + "/cog/bbox/-56.228,72.715,-54.547,73.188.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256&width=512&height=512" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -386,7 +368,7 @@ def test_part(rio, app): assert meta["driver"] == "PNG" response = app.get( - "/cog/crop/-56.228,72.715,-54.547,73.188.npy?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" + "/cog/bbox/-56.228,72.715,-54.547,73.188.npy?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -417,7 +399,9 @@ def test_tile_outside_bounds_error(rio, app): """raise 404 when tile is not found.""" rio.open = mock_rasterio_open - response = app.get("/cog/tiles/15/0/0?url=https://myurl.com/cog.tif&rescale=0,1000") + response = app.get( + "/cog/tiles/WebMercatorQuad/15/0/0?url=https://myurl.com/cog.tif&rescale=0,1000" + ) assert response.status_code == 404 assert response.headers["Cache-Control"] == "private, max-age=3600" diff --git a/src/titiler/application/tests/routes/test_mosaic.py b/src/titiler/application/tests/routes/test_mosaic.py index 3e092329d..efcd01c8c 100644 --- a/src/titiler/application/tests/routes/test_mosaic.py +++ b/src/titiler/application/tests/routes/test_mosaic.py @@ -4,7 +4,7 @@ from typing import Any, Callable from unittest.mock import patch -import mercantile +import morecantile from cogeo_mosaic.backends import FileBackend from cogeo_mosaic.mosaic import MosaicJSON @@ -42,53 +42,44 @@ def test_read_mosaic(app): MosaicJSON(**response.json()) -def test_bounds(app): - """test GET /mosaicjson/bounds endpoint""" - response = app.get("/mosaicjson/bounds", params={"url": MOSAICJSON_FILE}) - assert response.status_code == 200 - body = response.json() - assert len(body["bounds"]) == 4 - assert body["bounds"][0] < body["bounds"][2] - assert body["bounds"][1] < body["bounds"][3] - - def test_info(app): """test GET /mosaicjson/info endpoint""" response = app.get("/mosaicjson/info", params={"url": MOSAICJSON_FILE}) assert response.status_code == 200 body = response.json() - assert body["minzoom"] == 7 - assert body["maxzoom"] == 9 - assert body["name"] == "mosaic" # mosaic.name is not set assert body["quadkeys"] == [] + assert body["mosaic_minzoom"] == 7 + assert body["mosaic_maxzoom"] == 9 + assert body["mosaic_tilematrixset"] response = app.get("/mosaicjson/info.geojson", params={"url": MOSAICJSON_FILE}) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" body = response.json() assert body["geometry"] - assert body["properties"]["minzoom"] == 7 - assert body["properties"]["maxzoom"] == 9 - assert body["properties"]["name"] == "mosaic" # mosaic.name is not set assert body["properties"]["quadkeys"] == [] + assert body["properties"]["mosaic_minzoom"] == 7 + assert body["properties"]["mosaic_maxzoom"] == 9 + assert body["properties"]["mosaic_tilematrixset"] def test_tilejson(app): - """test GET /mosaicjson/tilejson.json endpoint""" + """test GET /mosaicjson/WebMercatorQuad/tilejson.json endpoint""" mosaicjson = read_json_fixture(MOSAICJSON_FILE) - response = app.get("/mosaicjson/tilejson.json", params={"url": MOSAICJSON_FILE}) + response = app.get( + "/mosaicjson/WebMercatorQuad/tilejson.json", params={"url": MOSAICJSON_FILE} + ) assert response.status_code == 200 body = response.json() TileJSON(**body) assert ( - "http://testserver/mosaicjson/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=" + "http://testserver/mosaicjson/tiles/WebMercatorQuad/{z}/{x}/{y}?url=" in body["tiles"][0] ) assert body["minzoom"] == mosaicjson["minzoom"] assert body["maxzoom"] == mosaicjson["maxzoom"] assert body["bounds"] == mosaicjson["bounds"] - assert body["center"] == mosaicjson["center"] def test_point(app): @@ -102,22 +93,23 @@ def test_point(app): ) assert response.status_code == 200 body = response.json() - assert len(body["values"]) == 1 - assert body["values"][0][0].endswith(".tif") - assert body["values"][0][1] == [9943, 9127, 9603] + assert len(body["assets"]) == 1 + assert body["assets"][0]["name"].endswith(".tif") + assert body["assets"][0]["values"] == [9943, 9127, 9603] def test_tile(app): """Test GET /mosaicjson/tiles endpoint""" mosaicjson = read_json_fixture(MOSAICJSON_FILE) bounds = mosaicjson["bounds"] - tile = mercantile.tile(*mosaicjson["center"]) - partial_tile = mercantile.tile(bounds[0], bounds[1], mosaicjson["minzoom"]) + tms = morecantile.tms.get("WebMercatorQuad") + tile = tms.tile(*mosaicjson["center"]) + partial_tile = tms.tile(bounds[0], bounds[1], mosaicjson["minzoom"]) with patch.object(FileBackend, "_read", mosaic_read_factory(MOSAICJSON_FILE)): # full tile response = app.get( - f"/mosaicjson/tiles/{tile.z}/{tile.x}/{tile.y}", + f"/mosaicjson/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}", params={"url": MOSAICJSON_FILE}, ) assert response.status_code == 200 @@ -126,8 +118,8 @@ def test_tile(app): assert meta["width"] == meta["height"] == 256 response = app.get( - f"/mosaicjson/tiles/{tile.z}/{tile.x}/{tile.y}@2x", - params={"url": MOSAICJSON_FILE}, + f"/mosaicjson/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}", + params={"url": MOSAICJSON_FILE, "tilesize": 512}, ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -135,7 +127,7 @@ def test_tile(app): assert meta["width"] == meta["height"] == 512 response = app.get( - f"/mosaicjson/tiles/{tile.z}/{tile.x}/{tile.y}.tif", + f"/mosaicjson/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}.tif", params={"url": MOSAICJSON_FILE}, ) assert response.status_code == 200 @@ -145,8 +137,8 @@ def test_tile(app): assert meta["crs"] == 3857 response = app.get( - f"/mosaicjson/tiles/{tile.z}/{tile.x}/{tile.y}@2x.tif", - params={"url": MOSAICJSON_FILE, "nodata": 0, "bidx": 1}, + f"/mosaicjson/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}.tif", + params={"url": MOSAICJSON_FILE, "nodata": 0, "bidx": 1, "tilesize": 512}, ) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" @@ -157,7 +149,7 @@ def test_tile(app): assert meta["height"] == 512 response = app.get( - f"/mosaicjson/tiles/{tile.z}/{tile.x}/{tile.y}@2x.jpg", + f"/mosaicjson/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}.jpg", params={ "url": MOSAICJSON_FILE, "rescale": "0,1000", @@ -170,14 +162,14 @@ def test_tile(app): # partial tile response = app.get( - f"/mosaicjson/tiles/{partial_tile.z}/{partial_tile.x}/{partial_tile.y}", + f"/mosaicjson/tiles/WebMercatorQuad/{partial_tile.z}/{partial_tile.x}/{partial_tile.y}", params={"url": MOSAICJSON_FILE}, ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" response = app.get( - f"/mosaicjson/tiles/{partial_tile.z}/{partial_tile.x}/{partial_tile.y}.tif", + f"/mosaicjson/tiles/WebMercatorQuad/{partial_tile.z}/{partial_tile.x}/{partial_tile.y}.tif", params={"url": MOSAICJSON_FILE, "resampling": "bilinear"}, ) assert response.status_code == 200 @@ -185,26 +177,16 @@ def test_tile(app): def test_wmts(app): - """test GET /mosaicjson/WMTSCapabilities.xml endpoint""" + """test GET /mosaicjson/WebMercatorQuad/WMTSCapabilities.xml endpoint""" with patch.object(FileBackend, "_read", mosaic_read_factory(MOSAICJSON_FILE)): - response = app.get( - "/mosaicjson/WMTSCapabilities.xml", params={"url": MOSAICJSON_FILE} - ) - assert response.status_code == 200 - assert response.headers["content-type"] == "application/xml" - assert ( - "http://testserver/mosaicjson/tiles/WebMercatorQuad/{TileMatrix}/{TileCol}/{TileRow}@1x.png?url=" - in response.content.decode() - ) - response = app.get( "/mosaicjson/WMTSCapabilities.xml", - params={"url": MOSAICJSON_FILE, "tile_scale": 2}, + params={"url": MOSAICJSON_FILE}, ) assert response.status_code == 200 assert response.headers["content-type"] == "application/xml" assert ( - "http://testserver/mosaicjson/tiles/WebMercatorQuad/{TileMatrix}/{TileCol}/{TileRow}@2x.png?url=" + "http://testserver/mosaicjson/tiles/WebMercatorQuad/{TileMatrix}/{TileCol}/{TileRow}.png?url=" in response.content.decode() ) diff --git a/src/titiler/application/tests/routes/test_stac.py b/src/titiler/application/tests/routes/test_stac.py index 2ced6f0dd..70adf9a2c 100644 --- a/src/titiler/application/tests/routes/test_stac.py +++ b/src/titiler/application/tests/routes/test_stac.py @@ -1,5 +1,4 @@ -"""test /COG endpoints.""" - +"""test /stac endpoints.""" from typing import Dict from unittest.mock import patch @@ -9,17 +8,6 @@ from ..conftest import mock_rasterio_open, mock_RequestGet -@patch("rio_tiler.io.stac.httpx") -def test_bounds(httpx, app): - """test /bounds endpoint.""" - httpx.get = mock_RequestGet - - response = app.get("/stac/bounds?url=https://myurl.com/item.json") - assert response.status_code == 200 - body = response.json() - assert len(body["bounds"]) == 4 - - @patch("rio_tiler.io.rasterio.rasterio") @patch("rio_tiler.io.stac.httpx") def test_info(httpx, rio, app): @@ -37,7 +25,11 @@ def test_info(httpx, rio, app): body = response.json() assert body["B01"] + # no assets response = app.get("/stac/info?url=https://myurl.com/item.json") + assert response.status_code == 422 + + response = app.get("/stac/info?url=https://myurl.com/item.json&assets=:all:") assert response.status_code == 200 body = response.json() assert body["B01"] @@ -75,20 +67,13 @@ def test_tile(httpx, rio, app): rio.open = mock_rasterio_open # Missing assets - response = app.get("/stac/tiles/9/289/207?url=https://myurl.com/item.json") - assert response.status_code == 400 - response = app.get( - "/stac/tiles/9/289/207?url=https://myurl.com/item.json&assets=B01&rescale=0,1000" + "/stac/tiles/WebMercatorQuad/9/289/207?url=https://myurl.com/item.json" ) - assert response.status_code == 200 - assert response.headers["content-type"] == "image/png" - meta = parse_img(response.content) - assert meta["width"] == 256 - assert meta["height"] == 256 + assert response.status_code == 422 response = app.get( - "/stac/tiles/9/289/207?url=https://myurl.com/item.json&expression=B01_b1&rescale=0,1000" + "/stac/tiles/WebMercatorQuad/9/289/207?url=https://myurl.com/item.json&assets=B01&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -104,20 +89,22 @@ def test_tilejson(httpx, rio, app): httpx.get = mock_RequestGet rio.open = mock_rasterio_open - response = app.get("/stac/tilejson.json?url=https://myurl.com/item.json") - assert response.status_code == 400 + response = app.get( + "/stac/WebMercatorQuad/tilejson.json?url=https://myurl.com/item.json" + ) + assert response.status_code == 422 response = app.get( - "/stac/tilejson.json?url=https://myurl.com/item.json&assets=B01&minzoom=5&maxzoom=10" + "/stac/WebMercatorQuad/tilejson.json?url=https://myurl.com/item.json&assets=B01&minzoom=5&maxzoom=10" ) assert response.status_code == 200 body = response.json() - assert body["tilejson"] == "2.2.0" + assert body["tilejson"] == "3.0.0" assert body["version"] == "1.0.0" assert body["scheme"] == "xyz" assert len(body["tiles"]) == 1 assert body["tiles"][0].startswith( - "http://testserver/stac/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=" + "http://testserver/stac/tiles/WebMercatorQuad/{z}/{x}/{y}?url=" ) assert body["minzoom"] == 5 assert body["maxzoom"] == 10 @@ -125,13 +112,14 @@ def test_tilejson(httpx, rio, app): assert body["center"] response = app.get( - "/stac/tilejson.json?url=https://myurl.com/item.json&assets=B01&tile_format=png&tile_scale=2" + "/stac/WebMercatorQuad/tilejson.json?url=https://myurl.com/item.json&assets=B01&tile_format=png&tilesize=512" ) assert response.status_code == 200 body = response.json() assert body["tiles"][0].startswith( - "http://testserver/stac/tiles/WebMercatorQuad/{z}/{x}/{y}@2x.png?url=" + "http://testserver/stac/tiles/WebMercatorQuad/{z}/{x}/{y}.png?url=" ) + assert "tilesize=512" in body["tiles"][0] @patch("rio_tiler.io.rasterio.rasterio") @@ -141,9 +129,9 @@ def test_preview(httpx, rio, app): httpx.get = mock_RequestGet rio.open = mock_rasterio_open - # Missing Assets or Expression + # Missing Assets response = app.get("/stac/preview?url=https://myurl.com/item.json") - assert response.status_code == 400 + assert response.status_code == 422 response = app.get( "/stac/preview?url=https://myurl.com/item.json&assets=B01&rescale=0,1000&max_size=64" @@ -164,13 +152,25 @@ def test_preview(httpx, rio, app): assert meta["height"] == 128 response = app.get( - "/stac/preview?url=https://myurl.com/item.json&expression=B01_b1&rescale=0,1000&max_size=64" + "/stac/preview.tiff?url=https://myurl.com/items_bands.json&assets=rgb&return_mask=False" ) assert response.status_code == 200 - assert response.headers["content-type"] == "image/png" meta = parse_img(response.content) - assert meta["width"] == 64 - assert meta["height"] == 64 + assert meta["count"] == 3 + + response = app.get( + "/stac/preview.tiff?url=https://myurl.com/items_bands.json&assets=rgb|bands=red&return_mask=False" + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["count"] == 1 + + response = app.get( + "/stac/preview.tiff?url=https://myurl.com/items_bands.json&assets=rgb|bidx=1&return_mask=False" + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["count"] == 1 @patch("rio_tiler.io.rasterio.rasterio") @@ -180,14 +180,14 @@ def test_part(httpx, rio, app): httpx.get = mock_RequestGet rio.open = mock_rasterio_open - # Missing Assets or Expression + # Missing Assets response = app.get( - "/stac/crop/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json" + "/stac/bbox/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json" ) - assert response.status_code == 400 + assert response.status_code == 422 response = app.get( - "/stac/crop/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&assets=B01&rescale=0,1000&max_size=64" + "/stac/bbox/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&assets=B01&rescale=0,1000&max_size=64" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -196,7 +196,7 @@ def test_part(httpx, rio, app): assert meta["height"] == 14 response = app.get( - "/stac/crop/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&assets=B01&rescale=0,1000&max_size=64&width=128&height=128" + "/stac/bbox/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&assets=B01&rescale=0,1000&max_size=64&width=128&height=128" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -205,7 +205,7 @@ def test_part(httpx, rio, app): assert meta["height"] == 128 response = app.get( - "/stac/crop/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&expression=B01_b1&rescale=0,1000&max_size=64" + "/stac/bbox/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&assets=B01&rescale=0,1000&max_size=64" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -217,13 +217,13 @@ def test_part(httpx, rio, app): @patch("rio_tiler.io.rasterio.rasterio") @patch("rio_tiler.io.stac.httpx") def test_point(httpx, rio, app): - """test crop endpoints.""" + """test point endpoints.""" httpx.get = mock_RequestGet rio.open = mock_rasterio_open - # Missing Assets or Expression + # Missing Assets response = app.get("/stac/point/23.878,32.063?url=https://myurl.com/item.json") - assert response.status_code == 400 + assert response.status_code == 422 response = app.get( "/stac/point/23.878,32.063?url=https://myurl.com/item.json&assets=B01" @@ -232,24 +232,46 @@ def test_point(httpx, rio, app): body = response.json() assert body["coordinates"] == [23.878, 32.063] assert body["values"] == [3565] - assert body["band_names"] == ["B01_b1"] + assert body["band_names"] == ["b1"] + assert body["band_descriptions"] == ["B01_b1"] + + response = app.get( + "/stac/point/23.878,32.063?url=https://myurl.com/item.json&assets=B01&expression=b1*2" + ) + assert response.status_code == 200 + body = response.json() + assert body["coordinates"] == [23.878, 32.063] + assert body["values"] == [7130] + assert body["band_names"] == ["b1"] + assert body["band_descriptions"] == ["B01_b1*2"] response = app.get( - "/stac/point/23.878,32.063?url=https://myurl.com/item.json&expression=B01_b1*2" + "/stac/point/23.878,32.063?url=https://myurl.com/item.json&assets=B01&expression=b1*2&asset_as_band=true" ) assert response.status_code == 200 body = response.json() assert body["coordinates"] == [23.878, 32.063] assert body["values"] == [7130] - assert body["band_names"] == ["B01_b1*2"] + assert body["band_names"] == ["b1"] + assert body["band_descriptions"] == ["B01*2"] + + response = app.get( + "/stac/point/23.878,32.063?url=https://myurl.com/item.json&assets=B01&assets=B09&expression=b1/b2" + ) + assert response.status_code == 200 + body = response.json() + assert body["coordinates"] == [23.878, 32.063] + assert round(body["values"][0], 2) == 0.49 + assert body["band_descriptions"] == ["B01_b1/B09_b1"] response = app.get( - "/stac/point/23.878,32.063?url=https://myurl.com/item.json&expression=B01_b1/B09_b1" + "/stac/point/23.878,32.063?url=https://myurl.com/item.json&assets=B01&assets=B09&expression=b1/b2&asset_as_band=true" ) assert response.status_code == 200 body = response.json() assert body["coordinates"] == [23.878, 32.063] assert round(body["values"][0], 2) == 0.49 + assert body["band_descriptions"] == ["B01/B09"] @patch("rio_tiler.io.rasterio.rasterio") diff --git a/src/titiler/application/tests/routes/test_zarr.py b/src/titiler/application/tests/routes/test_zarr.py new file mode 100644 index 000000000..6080a18ae --- /dev/null +++ b/src/titiler/application/tests/routes/test_zarr.py @@ -0,0 +1,37 @@ +"""test /COG endpoints.""" + +import os + +from ..conftest import DATA_DIR + +dataset_3d_zarr = os.path.join(DATA_DIR, "dataset_3d.zarr") + + +def test_zarr_endpoints(app): + """test zarr endpoint.""" + resp = app.get("/zarr/validate", params={"url": dataset_3d_zarr}) + assert resp.status_code == 200 + assert resp.headers["content-type"] == "application/json" + assert list(resp.json()) == ["dataset"] + assert resp.json()["dataset"]["compatible_with_titiler"] + + resp = app.get("/zarr/dataset/keys", params={"url": dataset_3d_zarr}) + assert resp.status_code == 200 + assert resp.headers["content-type"] == "application/json" + assert resp.json() == ["dataset"] + + resp = app.get("/zarr/info", params={"url": dataset_3d_zarr, "variable": "dataset"}) + assert resp.status_code == 200 + assert resp.headers["content-type"] == "application/json" + + resp = app.get( + "/zarr/tiles/WebMercatorQuad/0/0/0", + params={ + "url": dataset_3d_zarr, + "variable": "dataset", + "rescale": "0,500", + "bidx": 1, + }, + ) + assert resp.status_code == 200 + assert resp.headers["content-type"] == "image/png" diff --git a/src/titiler/application/tests/test_main.py b/src/titiler/application/tests/test_main.py index 81ddba727..038b40400 100644 --- a/src/titiler/application/tests/test_main.py +++ b/src/titiler/application/tests/test_main.py @@ -5,4 +5,19 @@ def test_health(app): """Test /healthz endpoint.""" response = app.get("/healthz") assert response.status_code == 200 - assert response.json() == {"ping": "pong!"} + resp = response.json() + assert set(resp["versions"].keys()) == { + "titiler", + "gdal", + "geos", + "proj", + "rasterio", + "zarr", + "xarray", + } + + response = app.get("/api") + assert response.status_code == 200 + + response = app.get("/api.html") + assert response.status_code == 200 diff --git a/src/titiler/application/titiler/application/__init__.py b/src/titiler/application/titiler/application/__init__.py index f59030372..3d2fc4ed6 100644 --- a/src/titiler/application/titiler/application/__init__.py +++ b/src/titiler/application/titiler/application/__init__.py @@ -1,3 +1,3 @@ """titiler.application""" -__version__ = "0.11.7" +__version__ = "2.0.0b2" diff --git a/src/titiler/application/titiler/application/main.py b/src/titiler/application/titiler/application/main.py index d6366d8bd..4f9b844eb 100644 --- a/src/titiler/application/titiler/application/main.py +++ b/src/titiler/application/titiler/application/main.py @@ -1,13 +1,20 @@ """titiler app.""" +import json import logging +from logging import config as log_config +from typing import Annotated, Literal import jinja2 -from fastapi import FastAPI -from rio_tiler.io import STACReader +import rasterio +from cogeo_mosaic.backends import MosaicBackend as MosaicJSONBackend +from cogeo_mosaic.errors import MosaicAuthError, MosaicError, MosaicNotFoundError +from fastapi import Depends, FastAPI, HTTPException, Query, Security +from fastapi.security.api_key import APIKeyQuery +from rio_tiler.io import Reader, STACReader +from starlette import status from starlette.middleware.cors import CORSMiddleware from starlette.requests import Request -from starlette.responses import HTMLResponse from starlette.templating import Jinja2Templates from starlette_cramjam.middleware import CompressionMiddleware @@ -16,6 +23,7 @@ from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers from titiler.core.factory import ( AlgorithmFactory, + ColorMapFactory, MultiBaseTilerFactory, TilerFactory, TMSFactory, @@ -26,63 +34,116 @@ LowerCaseQueryStringMiddleware, TotalTimeMiddleware, ) +from titiler.core.models.OGC import Conformance, Landing +from titiler.core.resources.enums import MediaType +from titiler.core.utils import accept_media_type, create_html_response, update_openapi from titiler.extensions import ( cogValidateExtension, cogViewerExtension, stacExtension, + stacRenderExtension, stacViewerExtension, + wmtsExtension, ) -from titiler.mosaic.errors import MOSAIC_STATUS_CODES -from titiler.mosaic.factory import MosaicTilerFactory - -try: - pass # type: ignore -except ImportError: - # Try backported to PY<39 `importlib_resources`. - pass # type: ignore logging.getLogger("botocore.credentials").disabled = True logging.getLogger("botocore.utils").disabled = True +logging.getLogger("rasterio.session").setLevel(logging.ERROR) logging.getLogger("rio-tiler").setLevel(logging.ERROR) -templates = Jinja2Templates( - directory="", - loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), -) # type:ignore +api_settings = ApiSettings() +# custom template directory +templates_location: list[jinja2.BaseLoader] = ( + [jinja2.FileSystemLoader(api_settings.template_directory)] + if api_settings.template_directory + else [] +) +# default template directory +templates_location.extend( + [ + jinja2.PackageLoader("titiler.application", "templates"), + jinja2.PackageLoader("titiler.core", "templates"), + ] +) -api_settings = ApiSettings() +jinja2_env = jinja2.Environment( + autoescape=jinja2.select_autoescape(["html"]), + loader=jinja2.ChoiceLoader(templates_location), +) +titiler_templates = Jinja2Templates(env=jinja2_env) -app = FastAPI( - title=api_settings.name, - description="""A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL. +app_dependencies = [] +if api_settings.global_access_token: + ############################################################################### + # Setup a global API access key, if configured + api_key_query = APIKeyQuery(name="access_token", auto_error=False) + + def validate_access_token(access_token: str = Security(api_key_query)): + """Validates API key access token, set as the `api_settings.global_access_token` value. + Returns True if no access token is required, or if the access token is valid. + Raises an HTTPException (401) if the access token is required but invalid/missing. + """ + if not access_token: + raise HTTPException(status_code=401, detail="Missing `access_token`") + + # if access_token == `token` then OK + if access_token != api_settings.global_access_token: + raise HTTPException(status_code=401, detail="Invalid `access_token`") ---- + return True -**Documentation**: https://developmentseed.org/titiler/ + app_dependencies.append(Depends(validate_access_token)) -**Source Code**: https://github.com/developmentseed/titiler ---- - """, +############################################################################### + +app = FastAPI( + title=api_settings.name, + openapi_url="/api", + docs_url="/api.html", + description=api_settings.description, version=titiler_version, root_path=api_settings.root_path, + dependencies=app_dependencies, ) +# Fix OpenAPI response header for OGC Common compatibility +update_openapi(app) + +TITILER_CONFORMS_TO = { + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/landing-page", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/oas30", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/html", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json", +} + + ############################################################################### # Simple Dataset endpoints (e.g Cloud Optimized GeoTIFF) if not api_settings.disable_cog: cog = TilerFactory( + reader=Reader, router_prefix="/cog", + add_ogc_maps=True, extensions=[ cogValidateExtension(), cogViewerExtension(), stacExtension(), + wmtsExtension(), ], + enable_telemetry=api_settings.telemetry_enabled, + templates=titiler_templates, ) - app.include_router(cog.router, prefix="/cog", tags=["Cloud Optimized GeoTIFF"]) + app.include_router( + cog.router, + prefix="/cog", + tags=["Cloud Optimized GeoTIFF"], + ) + TITILER_CONFORMS_TO.update(cog.conforms_to) ############################################################################### # STAC endpoints @@ -90,33 +151,110 @@ stac = MultiBaseTilerFactory( reader=STACReader, router_prefix="/stac", - extensions=[ - stacViewerExtension(), - ], + add_ogc_maps=True, + extensions=[stacViewerExtension(), stacRenderExtension(), wmtsExtension()], + enable_telemetry=api_settings.telemetry_enabled, + templates=titiler_templates, ) app.include_router( - stac.router, prefix="/stac", tags=["SpatioTemporal Asset Catalog"] + stac.router, + prefix="/stac", + tags=["SpatioTemporal Asset Catalog"], ) + TITILER_CONFORMS_TO.update(stac.conforms_to) + ############################################################################### # Mosaic endpoints if not api_settings.disable_mosaic: - mosaic = MosaicTilerFactory(router_prefix="/mosaicjson") - app.include_router(mosaic.router, prefix="/mosaicjson", tags=["MosaicJSON"]) + from titiler.mosaic.errors import MOSAIC_STATUS_CODES + from titiler.mosaic.extensions.mosaicjson import MosaicJSONExtension + from titiler.mosaic.extensions.wmts import wmtsExtension as mosaic_wmtsExtension + from titiler.mosaic.factory import MosaicTilerFactory + + mosaic = MosaicTilerFactory( + backend=MosaicJSONBackend, # type: ignore + router_prefix="/mosaicjson", + extensions=[ + MosaicJSONExtension(), + mosaic_wmtsExtension(), + ], + enable_telemetry=api_settings.telemetry_enabled, + templates=titiler_templates, + ) + app.include_router( + mosaic.router, + prefix="/mosaicjson", + tags=["MosaicJSON"], + ) + + # Add Mosaic specific error handlers + MOSAIC_STATUS_CODES.update( + { + MosaicAuthError: status.HTTP_401_UNAUTHORIZED, + MosaicError: status.HTTP_424_FAILED_DEPENDENCY, + MosaicNotFoundError: status.HTTP_404_NOT_FOUND, + } + ) + add_exception_handlers(app, MOSAIC_STATUS_CODES) + + TITILER_CONFORMS_TO.update(mosaic.conforms_to) + + +############################################################################### +# Zarr endpoints +if not api_settings.disable_zarr: + from titiler.xarray.extensions import DatasetMetadataExtension, ValidateExtension + from titiler.xarray.factory import TilerFactory as XarrayTilerFactory + + md = XarrayTilerFactory( + router_prefix="/zarr", + extensions=[ + DatasetMetadataExtension(), + ValidateExtension(), + ], + enable_telemetry=api_settings.telemetry_enabled, + templates=titiler_templates, + ) + app.include_router( + md.router, + prefix="/zarr", + tags=["Zarr"], + ) + + TITILER_CONFORMS_TO.update(md.conforms_to) + ############################################################################### # TileMatrixSets endpoints -tms = TMSFactory() -app.include_router(tms.router, tags=["Tiling Schemes"]) +tms = TMSFactory(templates=titiler_templates) +app.include_router( + tms.router, + tags=["Tiling Schemes"], +) +TITILER_CONFORMS_TO.update(tms.conforms_to) ############################################################################### # Algorithms endpoints -algorithms = AlgorithmFactory() -app.include_router(algorithms.router, tags=["Algorithms"]) +algorithms = AlgorithmFactory(templates=titiler_templates) +app.include_router( + algorithms.router, + tags=["Algorithms"], +) +TITILER_CONFORMS_TO.update(algorithms.conforms_to) +############################################################################### +# Colormaps endpoints +cmaps = ColorMapFactory(templates=titiler_templates) +app.include_router( + cmaps.router, + tags=["ColorMaps"], +) +TITILER_CONFORMS_TO.update(cmaps.conforms_to) + +############################################################################### add_exception_handlers(app, DEFAULT_STATUS_CODES) -add_exception_handlers(app, MOSAIC_STATUS_CODES) # Set all CORS enabled origins if api_settings.cors_origins: @@ -124,7 +262,7 @@ CORSMiddleware, allow_origins=api_settings.cors_origins, allow_credentials=True, - allow_methods=["GET"], + allow_methods=api_settings.cors_allow_methods, allow_headers=["*"], ) @@ -138,6 +276,7 @@ "image/jp2", "image/webp", }, + compression_level=6, ) app.add_middleware( @@ -147,9 +286,70 @@ ) if api_settings.debug: - app.add_middleware(LoggerMiddleware, headers=True, querystrings=True) + app.add_middleware(LoggerMiddleware) app.add_middleware(TotalTimeMiddleware) + log_config.dictConfig( + { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "detailed": { + "format": "%(asctime)s - %(levelname)s - %(name)s - %(message)s" + }, + "request": { + "format": ( + "%(asctime)s - %(levelname)s - %(name)s - %(message)s " + + json.dumps( + { + k: f"%({k})s" + for k in [ + "http.method", + "http.referer", + "http.request.header.origin", + "http.route", + "http.target", + "http.request.header.content-length", + "http.request.header.accept-encoding", + "http.request.header.origin", + "titiler.path_params", + "titiler.query_params", + ] + } + ) + ), + }, + }, + "handlers": { + "console_detailed": { + "class": "logging.StreamHandler", + "level": "WARNING", + "formatter": "detailed", + "stream": "ext://sys.stdout", + }, + "console_request": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "request", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "titiler": { + "level": "INFO", + "handlers": ["console_detailed"], + "propagate": False, + }, + "titiler.requests": { + "level": "INFO", + "handlers": ["console_request"], + "propagate": False, + }, + }, + } + ) + + if api_settings.lower_case_query_parameters: app.add_middleware(LowerCaseQueryStringMiddleware) @@ -161,16 +361,207 @@ operation_id="healthCheck", tags=["Health Check"], ) -def ping(): +def application_health_check(): """Health check.""" - return {"ping": "pong!"} + versions = { + "titiler": titiler_version, + "rasterio": rasterio.__version__, + "gdal": rasterio.__gdal_version__, + "proj": rasterio.__proj_version__, + "geos": rasterio.__geos_version__, + } + if not api_settings.disable_zarr: + import xarray + import zarr + + versions.update({"zarr": zarr.__version__, "xarray": xarray.__version__}) + + return {"versions": versions} + + +@app.get( + "/", + response_model=Landing, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "text/html": {}, + "application/json": {}, + } + }, + }, + tags=["OGC Common"], +) +def landing( + request: Request, + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, +): + """TiTiler landing page.""" + data = { + "title": "TiTiler", + "description": "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL.", + "links": [ + { + "title": "Landing page", + "href": str(request.url_for("landing")), + "type": "text/html", + "rel": "self", + }, + { + "title": "The API definition (JSON)", + "href": str(request.url_for("openapi")), + "type": "application/vnd.oai.openapi+json;version=3.0", + "rel": "service-desc", + }, + { + "title": "The API documentation", + "href": str(request.url_for("swagger_ui_html")), + "type": "text/html", + "rel": "service-doc", + }, + { + "title": "Conformance Declaration", + "href": str(request.url_for("conformance")), + "type": "text/html", + "rel": "http://www.opengis.net/def/rel/ogc/1.0/conformance", + }, + { + "title": "List of Available TileMatrixSets", + "href": str(request.url_for("tilematrixsets")), + "type": "application/json", + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes", + }, + { + "title": "List of Available Algorithms", + "href": str(request.url_for("available_algorithms")), + "type": "application/json", + "rel": "data", + }, + { + "title": "List of Available ColorMaps", + "href": str(request.url_for("available_colormaps")), + "type": "application/json", + "rel": "data", + }, + { + "title": "TiTiler Documentation (external link)", + "href": "https://developmentseed.org/titiler/", + "type": "text/html", + "rel": "doc", + }, + { + "title": "TiTiler source code (external link)", + "href": "https://github.com/developmentseed/titiler", + "type": "text/html", + "rel": "doc", + }, + ], + } + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) -@app.get("/", response_class=HTMLResponse, include_in_schema=False) -def landing(request: Request): - """TiTiler Landing page""" - return templates.TemplateResponse( - name="index.html", - context={"request": request}, - media_type="text/html", + if output_type == MediaType.html: + return create_html_response( + request, + data, + title="TiTiler", + template_name="landing", + templates=titiler_templates, + ) + + return data + + +@app.get( + "/conformance", + response_model=Conformance, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "text/html": {}, + "application/json": {}, + } + }, + }, + tags=["OGC Common"], +) +def conformance( + request: Request, + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, +): + """Conformance classes. + + Called with `GET /conformance`. + + Returns: + Conformance classes which the server conforms to. + + """ + data = {"conformsTo": sorted(TITILER_CONFORMS_TO)} + + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) + + if output_type == MediaType.html: + return create_html_response( + request, + data, + title="Conformance", + template_name="conformance", + templates=titiler_templates, + ) + + return data + + +if api_settings.telemetry_enabled: + from opentelemetry import trace + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor + from opentelemetry.instrumentation.logging import LoggingInstrumentor + from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION, Resource + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + LoggingInstrumentor().instrument(set_logging_format=True) + FastAPIInstrumentor.instrument_app(app) + + resource = Resource.create( + { + SERVICE_NAME: "titiler", + SERVICE_VERSION: titiler_version, + } ) + + provider = TracerProvider(resource=resource) + + # uses the OTEL_EXPORTER_OTLP_ENDPOINT env var + processor = BatchSpanProcessor(OTLPSpanExporter()) + provider.add_span_processor(processor) + + trace.set_tracer_provider(provider) diff --git a/src/titiler/application/titiler/application/py.typed b/src/titiler/application/titiler/application/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/titiler/application/titiler/application/settings.py b/src/titiler/application/titiler/application/settings.py index 09fe63175..d4833656a 100644 --- a/src/titiler/application/titiler/application/settings.py +++ b/src/titiler/application/titiler/application/settings.py @@ -1,30 +1,54 @@ """Titiler API settings.""" -import pydantic +from pydantic import field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict -class ApiSettings(pydantic.BaseSettings): +class ApiSettings(BaseSettings): """FASTAPI application settings.""" name: str = "TiTiler" + description: str = """A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL. + +--- + +**Documentation**: https://developmentseed.org/titiler/ + +**Source Code**: https://github.com/developmentseed/titiler + +--- + """ + cors_origins: str = "*" + cors_allow_methods: str = "GET" cachecontrol: str = "public, max-age=3600" root_path: str = "" debug: bool = False + template_directory: str | None = None + disable_cog: bool = False disable_stac: bool = False disable_mosaic: bool = False + disable_zarr: bool = False lower_case_query_parameters: bool = False - @pydantic.validator("cors_origins") + telemetry_enabled: bool = False + + # an API key required to access any endpoint, passed via the ?access_token= query parameter + global_access_token: str | None = None + + model_config = SettingsConfigDict( + env_prefix="TITILER_API_", env_file=".env", extra="ignore" + ) + + @field_validator("cors_origins") def parse_cors_origin(cls, v): """Parse CORS origins.""" return [origin.strip() for origin in v.split(",")] - class Config: - """model config""" - - env_file = ".env" - env_prefix = "TITILER_API_" + @field_validator("cors_allow_methods") + def parse_cors_allow_methods(cls, v): + """Parse CORS allowed methods.""" + return [method.strip().upper() for method in v.split(",")] diff --git a/src/titiler/application/titiler/application/templates/conformance.html b/src/titiler/application/titiler/application/templates/conformance.html new file mode 100644 index 000000000..0471b3683 --- /dev/null +++ b/src/titiler/application/titiler/application/templates/conformance.html @@ -0,0 +1,32 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

{{ template.title }}

+ +

This API implements the conformance classes from standards and community specifications that are listed below.

+ +

Links

+
    +{% for url in response.conformsTo %} +
  • {{ url }}
  • +{% endfor %} +
+ +{% include "footer.html" %} diff --git a/src/titiler/application/titiler/application/templates/header.html b/src/titiler/application/titiler/application/templates/header.html new file mode 100644 index 000000000..a60001b62 --- /dev/null +++ b/src/titiler/application/titiler/application/templates/header.html @@ -0,0 +1,44 @@ + + + + {{ template.title }} + + + + + + + + + + + + +
+
diff --git a/src/titiler/application/titiler/application/templates/index.html b/src/titiler/application/titiler/application/templates/index.html deleted file mode 100644 index 70f9c7ea3..000000000 --- a/src/titiler/application/titiler/application/templates/index.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - TiTiler - - - - -
-
- ______   __     ______   __     __         ______     ______
-/\__  _\ /\ \   /\__  _\ /\ \   /\ \       /\  ___\   /\  == \
-\/_/\ \/ \ \ \  \/_/\ \/ \ \ \  \ \ \____  \ \  __\   \ \  __<
-   \ \_\  \ \_\    \ \_\  \ \_\  \ \_____\  \ \_____\  \ \_\ \_\
-    \/_/   \/_/     \/_/   \/_/   \/_____/   \/_____/   \/_/ /_/
-            
- -

API documentations: /docs -

-

TiTiler Online documentations: https://developmentseed.org/titiler/ -

-

- -
- Created by - - Development Seed - -
- - diff --git a/src/titiler/application/titiler/application/templates/landing.html b/src/titiler/application/titiler/application/templates/landing.html new file mode 100644 index 000000000..715d91d5b --- /dev/null +++ b/src/titiler/application/titiler/application/templates/landing.html @@ -0,0 +1,42 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

{{ response.title }}

+

+ {{ response.description }} +

+ +
+  ______   __     ______   __     __         ______     ______
+/\__  _\ /\ \   /\__  _\ /\ \   /\ \       /\  ___\   /\  == \
+\/_/\ \/ \ \ \  \/_/\ \/ \ \ \  \ \ \____  \ \  __\   \ \  __<
+    \ \_\  \ \_\    \ \_\  \ \_\  \ \_____\  \ \_____\  \ \_\ \_\
+    \/_/   \/_/     \/_/   \/_/   \/_____/   \/_____/   \/_/ /_/
+
+  
+ +

Links

+ + +{% include "footer.html" %} diff --git a/src/titiler/core/LICENSE b/src/titiler/core/LICENSE new file mode 100644 index 000000000..eb4365951 --- /dev/null +++ b/src/titiler/core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Development Seed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/titiler/core/README.md b/src/titiler/core/README.md index 4a8fa87c1..ae5804fa7 100644 --- a/src/titiler/core/README.md +++ b/src/titiler/core/README.md @@ -5,14 +5,14 @@ Core of Titiler's application. Contains blocks to create dynamic tile servers. ## Installation ```bash -$ pip install -U pip +python -m pip install -U pip # From Pypi -$ pip install titiler.core +python -m pip install titiler.core # Or from sources -$ git clone https://github.com/developmentseed/titiler.git -$ cd titiler && pip install -e titiler/core +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core ``` ## How To @@ -33,8 +33,6 @@ cog = TilerFactory() app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) ``` -See [titiler.application](../application) for a full example. - ## Package structure ``` @@ -42,6 +40,10 @@ titiler/ └── core/ ├── tests/ - Tests suite └── titiler/core/ - `core` namespace package + ├── algorithm/ + | ├── base.py - ABC Base Class for custom algorithms + | ├── dem.py - Elevation data related algorithms + | └── index.py - Simple band index algorithms ├── models/ | ├── response.py - Titiler's response models | ├── mapbox.py - Mapbox TileJSON pydantic model @@ -50,10 +52,13 @@ titiler/ | ├── enums.py - Titiler's enumerations (e.g MediaType) | └── responses.py - Custom Starlette's responses ├── templates/ - | └── wmts.xml - OGC WMTS template + | ├── map.html - Simple Map viewer (built with leaflet) + | └── wmts.xml - OGC WMTS document template ├── dependencies.py - Titiler FastAPI's dependencies ├── errors.py - Errors handler factory + ├── middleware.py - Starlette middlewares ├── factory.py - Dynamic tiler endpoints factories ├── routing.py - Custom APIRoute class + ├── telemetry.py - OpenTelemetry tracing functions └── utils.py - Titiler utility functions ``` diff --git a/src/titiler/core/pyproject.toml b/src/titiler/core/pyproject.toml index f964c014a..e7a3213d2 100644 --- a/src/titiler/core/pyproject.toml +++ b/src/titiler/core/pyproject.toml @@ -1,8 +1,8 @@ [project] -name = "titiler.core" +name = "titiler-core" description = "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.11" authors = [ {name = "Vincent Sarago", email = "vincent@developmentseed.com"}, ] @@ -21,26 +21,36 @@ classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: GIS", ] dynamic = ["version"] dependencies = [ - "fastapi>=0.87.0,<0.95", - "geojson-pydantic", + "fastapi>=0.108.0", + "geojson-pydantic>=1.1.2,<3.0", "jinja2>=2.11.2,<4.0.0", "numpy", - "pydantic", + "pydantic~=2.0", "rasterio", - "rio-tiler>=4.1.6,<4.2", + "rio-tiler>=9.0.0rc2,<10.0", + "morecantile", "simplejson", - "typing_extensions;python_version<'3.8'", ] [project.optional-dependencies] +telemetry = [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-instrumentation-fastapi", + "opentelemetry-instrumentation-logging", + "opentelemetry-exporter-otlp", +] + +[dependency-groups] test = [ "pytest", "pytest-cov", @@ -55,14 +65,15 @@ Issues = "https://github.com/developmentseed/titiler/issues" Source = "https://github.com/developmentseed/titiler" Changelog = "https://developmentseed.org/titiler/release-notes/" -[build-system] -requires = ["pdm-pep517"] -build-backend = "pdm.pep517.api" - -[tool.pdm.version] -source = "file" +[tool.hatch.version] path = "titiler/core/__init__.py" -[tool.pdm.build] -includes = ["titiler/core"] -excludes = ["tests/", "**/.mypy_cache", "**/.DS_Store"] +[tool.hatch.build.targets.sdist] +only-include = ["titiler"] + +[tool.hatch.build.targets.wheel] +only-include = ["titiler"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/src/titiler/core/tests/conftest.py b/src/titiler/core/tests/conftest.py index d2d299b5b..194e1c085 100644 --- a/src/titiler/core/tests/conftest.py +++ b/src/titiler/core/tests/conftest.py @@ -1,7 +1,7 @@ """``pytest`` configuration.""" import os -from typing import Any, Dict +from typing import Any import pytest import rasterio @@ -21,7 +21,7 @@ def set_env(monkeypatch): monkeypatch.setenv("AWS_CONFIG_FILE", "/tmp/noconfigheere") -def parse_img(content: bytes) -> Dict[Any, Any]: +def parse_img(content: bytes) -> dict[Any, Any]: """Read tile image and return metadata.""" with MemoryFile(content) as mem: with mem.open() as dst: diff --git a/src/titiler/core/tests/fixtures/cog_dateline.tif b/src/titiler/core/tests/fixtures/cog_dateline.tif new file mode 100644 index 000000000..279183018 Binary files /dev/null and b/src/titiler/core/tests/fixtures/cog_dateline.tif differ diff --git a/src/titiler/core/tests/fixtures/item.json b/src/titiler/core/tests/fixtures/item.json index f8e447265..8a20a9ed5 100644 --- a/src/titiler/core/tests/fixtures/item.json +++ b/src/titiler/core/tests/fixtures/item.json @@ -1,18 +1,10 @@ { "type": "Feature", - "stac_version": "1.0.0-beta.1", - "stac_extensions": [ - "eo", - "view", - "proj" - ], + "stac_version": "1.0.0", "id": "S2A_34SGA_20200318_0_L2A", - "bbox": [ - 23.293255090449595, - 31.505183020453355, - 24.296453548295318, - 32.51147809805106 - ], + "properties": { + "datetime": "2020-03-18T09:11:33Z" + }, "geometry": { "type": "Polygon", "coordinates": [ @@ -40,91 +32,31 @@ ] ] }, - "properties": { - "datetime": "2020-03-18T09:11:33Z", - "platform": "sentinel-2a", - "constellation": "sentinel-2", - "instruments": [ - "msi" - ], - "gsd": 10, - "data_coverage": 73.85, - "view:off_nadir": 0, - "eo:cloud_cover": 89.84, - "proj:epsg": 32634, - "sentinel:latitude_band": "S", - "sentinel:grid_square": "GA", - "sentinel:sequence": "0", - "sentinel:product_id": "S2A_MSIL2A_20200318T085701_N0214_R007_T34SGA_20200318T115254", - "created": "2020-05-12T21:03:26.671Z", - "updated": "2020-05-12T21:03:26.671Z" - }, - "collection": "sentinel-s2-l2a-cogs", + "links": [ + { + "rel": "self", + "href": "https://myurl.com/item.json", + "type": "application/json" + } + ], "assets": { "B01": { - "title": "Band 1 (coastal)", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", "href": "https://myurl.com/B01.tif", - "proj:shape": [ - 1830, - 1830 - ], - "proj:transform": [ - 60, - 0, - 699960, - 0, - -60, - 3600000, - 0, - 0, - 1 - ] + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Band 1 (coastal)" }, "B09": { - "title": "Band 9", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", "href": "https://myurl.com/B09.tif", - "proj:shape": [ - 1830, - 1830 - ], - "proj:transform": [ - 60, - 0, - 699960, - 0, - -60, - 3600000, - 0, - 0, - 1 - ] + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Band 9" } }, - "links": [ - { - "rel": "self", - "href": "s3://sentinel-cogs/sentinel-s2-l2a-cogs/2020/S2A_34SGA_20200318_0_L2A/S2A_34SGA_20200318_0_L2A.json", - "type": "application/json" - }, - { - "rel": "parent", - "href": "https://myurl.com/v0/collections/sentinel-s2-l2a" - }, - { - "rel": "collection", - "href": "https://myurl.com/v0/collections/sentinel-s2-l2a" - }, - { - "rel": "root", - "href": "https://myurl.com/v0/" - }, - { - "title": "Source STAC Item", - "rel": "derived_from", - "href": "https://myurl.com/collections/sentinel-s2-l2a/items/S2A_34SGA_20200318_0_L2A", - "type": "application/json" - } - ] + "bbox": [ + 23.293255090449595, + 31.505183020453355, + 24.296453548295318, + 32.51147809805106 + ], + "stac_extensions": [], + "collection": "sentinel-s2-l2a-cogs" } diff --git a/src/titiler/core/tests/test_CustomCmap.py b/src/titiler/core/tests/test_CustomCmap.py index 0c189247a..8b396aaf7 100644 --- a/src/titiler/core/tests/test_CustomCmap.py +++ b/src/titiler/core/tests/test_CustomCmap.py @@ -1,14 +1,13 @@ """Test TiTiler Custom Colormap Params.""" -from enum import Enum from io import BytesIO -from typing import Dict, Optional import numpy -from fastapi import FastAPI, Query +from fastapi import FastAPI from rio_tiler.colormap import ColorMaps from starlette.testclient import TestClient +from titiler.core.dependencies import create_colormap_dependency from titiler.core.factory import TilerFactory from .conftest import DATA_DIR @@ -17,19 +16,8 @@ "cmap1": {6: (4, 5, 6, 255)}, } cmap = ColorMaps(data=cmap_values) -ColorMapName = Enum( # type: ignore - "ColorMapName", [(a, a) for a in sorted(cmap.list())] -) - -def ColorMapParams( - colormap_name: ColorMapName = Query(None, description="Colormap name"), -) -> Optional[Dict]: - """Colormap Dependency.""" - if colormap_name: - return cmap.get(colormap_name.value) - - return None +ColorMapParams = create_colormap_dependency(cmap) def test_CustomCmap(): diff --git a/src/titiler/core/tests/test_CustomPath.py b/src/titiler/core/tests/test_CustomPath.py index cb1bd3298..91589fc9e 100644 --- a/src/titiler/core/tests/test_CustomPath.py +++ b/src/titiler/core/tests/test_CustomPath.py @@ -2,6 +2,7 @@ import os import re +from typing import Annotated from fastapi import FastAPI, HTTPException, Query from starlette.testclient import TestClient @@ -12,11 +13,13 @@ def CustomPathParams( - name: str = Query( - ..., - alias="file", - description="Give me a url.", - ) + name: Annotated[ + str, + Query( + alias="file", + description="Give me a url.", + ), + ], ) -> str: """Custom path Dependency.""" if not re.match(".+tif$", name): diff --git a/src/titiler/core/tests/test_CustomRender.py b/src/titiler/core/tests/test_CustomRender.py index 2d9661e2a..51b9e7e58 100644 --- a/src/titiler/core/tests/test_CustomRender.py +++ b/src/titiler/core/tests/test_CustomRender.py @@ -1,7 +1,6 @@ """Test TiTiler Custom Render Params.""" from dataclasses import dataclass -from typing import Optional, Union import numpy from fastapi import FastAPI, Query @@ -17,12 +16,12 @@ class CustomRenderParams(ImageRenderingParams): """Custom renderparams class.""" - nodata: Optional[Union[str, int, float]] = Query( + nodata: str | int | float | None = Query( None, title="Tiff Ouptut Nodata value", alias="output_nodata", ) - compress: Optional[str] = Query( + compress: str | None = Query( None, title="Tiff compression schema", alias="output_compression", @@ -30,6 +29,7 @@ class CustomRenderParams(ImageRenderingParams): def __post_init__(self): """post init.""" + super().__post_init__() if self.nodata is not None: self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata) @@ -41,7 +41,7 @@ def test_CustomRender(): app.include_router(cog.router) client = TestClient(app) - response = client.get(f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif") + response = client.get(f"/tiles/WebMercatorQuad/8/87/48.tif?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" meta = parse_img(response.content) @@ -51,7 +51,7 @@ def test_CustomRender(): assert not meta.get("compress") response = client.get( - f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif&return_mask=false&output_nodata=0&output_compression=deflate" + f"/tiles/WebMercatorQuad/8/87/48.tif?url={DATA_DIR}/cog.tif&return_mask=false&output_nodata=0&output_compression=deflate" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" @@ -62,7 +62,7 @@ def test_CustomRender(): assert meta["compress"] == "deflate" response = client.get( - f"/tiles/9/289/207?url={DATA_DIR}/TCI.tif&rescale=0,1000&rescale=0,2000&rescale=0,3000" + f"/tiles/WebMercatorQuad/9/289/207?url={DATA_DIR}/TCI.tif&rescale=0,1000&rescale=0,2000&rescale=0,3000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" diff --git a/src/titiler/core/tests/test_algorithms.py b/src/titiler/core/tests/test_algorithms.py index ca4e95a59..fe2c90c97 100644 --- a/src/titiler/core/tests/test_algorithms.py +++ b/src/titiler/core/tests/test_algorithms.py @@ -3,6 +3,7 @@ import json import numpy +import pytest from fastapi import Depends, FastAPI from rasterio.io import MemoryFile from rio_tiler.models import ImageData @@ -22,12 +23,11 @@ class Multiply(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Apply Multiplication factor.""" # Multiply image data bcy factor - data = img.data * self.factor + data = img.array * self.factor # Create output ImageData return ImageData( data, - img.mask, assets=img.assets, crs=img.crs, bounds=img.bounds, @@ -88,7 +88,6 @@ def main(algorithm=Depends(default_algorithms.dependency)): # MAPBOX Terrain RGB response = client.get("/", params={"algorithm": "terrainrgb"}) assert response.status_code == 200 - with MemoryFile(response.content) as mem: with mem.open() as dst: data = dst.read().astype(numpy.float64) @@ -100,7 +99,6 @@ def main(algorithm=Depends(default_algorithms.dependency)): # TILEZEN Terrarium response = client.get("/", params={"algorithm": "terrarium"}) assert response.status_code == 200 - with MemoryFile(response.content) as mem: with mem.open() as dst: data = dst.read().astype(numpy.float64) @@ -108,3 +106,315 @@ def main(algorithm=Depends(default_algorithms.dependency)): # https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium elevation = (data[0] * 256 + data[1] + data[2] / 256) - 32768 numpy.testing.assert_array_equal(elevation, arr[0]) + + +def test_normalized_index(): + """test ndi.""" + algo = default_algorithms.get("normalizedIndex")() + + arr = numpy.zeros((2, 256, 256), dtype="uint16") + arr[0, :, :] = 1 + arr[1, :, :] = 2 + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "float32" + assert len(numpy.unique(out.array).tolist()) == 1 + numpy.testing.assert_almost_equal(out.array[0, 0, 0], 0.3333, decimal=3) + + # with mixed 0 and masked + arr = numpy.ma.MaskedArray( + numpy.zeros((2, 256, 256), dtype="uint16"), + mask=numpy.zeros((2, 256, 256), dtype="bool"), + ) + arr.data[0, :, :] = 1 + arr.data[0, 0:10, 0:10] = 0 + arr.mask[0, 0:5, 0:5] = True + + arr.data[1, :, :] = 2 + arr.data[1, 0:10, 0:10] = 0 + arr.mask[1, 0:5, 0:5] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "float32" + assert len(numpy.unique(out.array).tolist()) == 2 # 0.33 and None + assert out.array[0, 0, 0] is numpy.ma.masked + assert out.array[0, 6, 6] is numpy.ma.masked + numpy.testing.assert_almost_equal(out.array[0, 10, 10], 0.3333, decimal=3) + + +def test_hillshade(): + """test hillshade.""" + algo = default_algorithms.get("hillshade")() + + arr = numpy.random.randint(0, 5000, (1, 262, 262), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 262, 262), dtype="uint16"), + mask=numpy.zeros((1, 262, 262), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked + + +def test_slope(): + """test slope.""" + algo = default_algorithms.get("slope")() + + arr = numpy.random.randint(0, 5000, (1, 262, 262), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "float32" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 262, 262), dtype="uint16"), + mask=numpy.zeros((1, 262, 262), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "float32" + assert out.array[0, 0, 0] is numpy.ma.masked + + +def test_contours(): + """test contours.""" + algo = default_algorithms.get("contours")() + + arr = numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked + + +def test_terrarium(): + """test terrarium.""" + algo = default_algorithms.get("terrarium")() + + arr = numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked + + # works on the above masked array img, with algo which was passed nodata_height + nodata_height = 10.0 + algo = default_algorithms.get("terrarium")(nodata_height=nodata_height) + out = algo(img) + masked = out.array[:, arr.mask[0, :, :]] + masked_height = (masked[0] * 256 + masked[1] + masked[2] / 256) - 32768 + numpy.testing.assert_array_equal( + masked_height, nodata_height * numpy.ones((100 * 100), dtype="bool") + ) + + +def test_terrainrgb(): + """test terrainrgb.""" + algo = default_algorithms.get("terrainrgb")() + + arr = numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked + + # works on the above masked array img, with algo which was passed nodata_height + nodata_height = 10.0 + algo = default_algorithms.get("terrainrgb")(nodata_height=nodata_height) + out = algo(img) + masked = out.array[:, arr.mask[0, :, :]] + masked_height = -10000 + ( + ((masked[0] * 256 * 256) + (masked[1] * 256) + masked[2]) * 0.1 + ) + numpy.testing.assert_array_equal( + masked_height, nodata_height * numpy.ones((100 * 100), dtype="bool") + ) + + +def test_ops(): + """test ops: cast, ceil and floor.""" + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256)).astype("float32"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.data[0, 0, 0] = 1.6 + arr.mask[0, 1:100, 1:100] = True + + img = ImageData(arr) + assert img.array.dtype == numpy.float32 + + algo = default_algorithms.get("cast")() + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] == 1 + assert out.array[0, 1, 1] is numpy.ma.masked + + assert img.array.dtype == numpy.float32 + algo = default_algorithms.get("floor")() + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] == 1 + assert out.array[0, 1, 1] is numpy.ma.masked + + assert img.array.dtype == numpy.float32 + algo = default_algorithms.get("ceil")() + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] == 2 + assert out.array[0, 1, 1] is numpy.ma.masked + + +@pytest.mark.parametrize( + "name,numpy_method,options", + [ + ("min", numpy.ma.min, {}), + ("max", numpy.ma.max, {}), + ("median", numpy.ma.median, {}), + ("mean", numpy.ma.mean, {}), + ("std", numpy.ma.std, {"ddof": 1}), + ("var", numpy.ma.var, {"ddof": 1}), + ("sum", numpy.ma.sum, {}), + ], +) +def test_math_algorithm(name, numpy_method, options): + """test math algos.""" + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256)).astype("float32"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.data[0, 0, 0] = 1.6 + arr.mask[0, 1:100, 1:100] = True + + img = ImageData(arr) + assert img.array.dtype == numpy.float32 + + algo = default_algorithms.get(name)() + out = algo(img) + + numpy.testing.assert_array_equal( + out.array, numpy_method(img.array, axis=0, keepdims=True, **options) + ) + assert out.array[0, 1, 1] is numpy.ma.masked + + +def test_bitonal_algorithm(): + """Test bitonal algorithm.""" + algo = default_algorithms.get("bitonal")() + + arr = numpy.ma.MaskedArray( + numpy.zeros((1, 256, 256), dtype="uint8"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.data[0, 100:200, 100:200] = 200 + arr.mask[0, 120:130, 120:130] = True + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 125, 125] is numpy.ma.masked + assert out.array[0, 150, 150] == 255 + assert out.array[0, 50, 50] == 0 + + arr = numpy.ma.MaskedArray( + numpy.zeros((3, 256, 256), dtype="uint8"), + mask=numpy.zeros((3, 256, 256), dtype="bool"), + ) + arr.data[:, 100:200, 100:200] = 200 + arr.mask[0, 120:130, 120:130] = True + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 125, 125] is numpy.ma.masked + assert out.array[0, 150, 150] == 255 + assert out.array[0, 50, 50] == 0 + + arr = numpy.ma.MaskedArray( + numpy.zeros((4, 256, 256), dtype="uint8"), + mask=numpy.zeros((4, 256, 256), dtype="bool"), + ) + img = ImageData(arr) + with pytest.raises(ValueError): + out = algo(img) + + +def test_grayscale_algorithm(): + """Test grayscale algorithm.""" + algo = default_algorithms.get("grayscale")() + + arr = numpy.ma.MaskedArray( + numpy.zeros((3, 256, 256), dtype="uint8"), + mask=numpy.zeros((3, 256, 256), dtype="bool"), + ) + arr.data[:, 100:200, 100:200] = 200 + arr.mask[0, 120:130, 120:130] = True + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 125, 125] is numpy.ma.masked + assert out.array[0, 150, 150] != 0 + assert out.array[0, 50, 50] == 0 + + arr = numpy.ma.MaskedArray( + numpy.zeros((2, 256, 256), dtype="uint8"), + mask=numpy.zeros((2, 256, 256), dtype="bool"), + ) + img = ImageData(arr) + with pytest.raises(ValueError): + out = algo(img) diff --git a/src/titiler/core/tests/test_cache_middleware.py b/src/titiler/core/tests/test_cache_middleware.py index b1331edd9..1dfee53d8 100644 --- a/src/titiler/core/tests/test_cache_middleware.py +++ b/src/titiler/core/tests/test_cache_middleware.py @@ -1,5 +1,6 @@ """Test titiler.core.CacheControlMiddleware.""" +from typing import Annotated from fastapi import FastAPI, Path from starlette.responses import Response @@ -29,18 +30,18 @@ async def route3(): @app.get("/tiles/{z}/{x}/{y}") async def tiles( - z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), - x: int = Path(..., description="Mercator tiles's column"), - y: int = Path(..., description="Mercator tiles's row"), + z: Annotated[int, Path(ge=0, le=30, description="Mercator tiles's zoom level")], + x: Annotated[int, Path(description="Mercator tiles's column")], + y: Annotated[int, Path(description="Mercator tiles's row")], ): """tiles.""" return "yeah" @app.get("/emptytiles/{z}/{x}/{y}") async def emptytiles( - z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), - x: int = Path(..., description="Mercator tiles's column"), - y: int = Path(..., description="Mercator tiles's row"), + z: Annotated[int, Path(ge=0, le=30, description="Mercator tiles's zoom level")], + x: Annotated[int, Path(description="Mercator tiles's column")], + y: Annotated[int, Path(description="Mercator tiles's row")], ): """tiles.""" return Response(status_code=404) diff --git a/src/titiler/core/tests/test_case_middleware.py b/src/titiler/core/tests/test_case_middleware.py index dc46ab5ae..b2f4b758b 100644 --- a/src/titiler/core/tests/test_case_middleware.py +++ b/src/titiler/core/tests/test_case_middleware.py @@ -1,6 +1,6 @@ """Test titiler.core.middleware.LowerCaseQueryStringMiddleware.""" -from typing import List +from typing import Annotated from fastapi import FastAPI, Query from starlette.testclient import TestClient @@ -13,7 +13,7 @@ def test_lowercase_middleware(): app = FastAPI() @app.get("/route1") - async def route1(value: str = Query(...)): + async def route1(value: Annotated[str, Query()]): """route1.""" return {"value": value} @@ -33,7 +33,7 @@ def test_lowercase_middleware_multiple_values(): app = FastAPI() @app.get("/route1") - async def route1(value: List[str] = Query(...)): + async def route1(value: Annotated[list[str], Query()]): """route1.""" return {"value": value} @@ -46,3 +46,26 @@ async def route1(value: List[str] = Query(...)): response = client.get("/route1?VALUE=lorenzori&VALUE=dogs&value=trucks") assert response.json() == {"value": ["lorenzori", "dogs", "trucks"]} + + +def test_lowercase_middleware_url_with_query_parameters(): + """Make sure all query parameters return.""" + app = FastAPI() + + @app.get("/route1") + async def route1(url: list[str] = Query(...)): + """route1.""" + return {"url": url} + + app.add_middleware(LowerCaseQueryStringMiddleware) + + client = TestClient(app) + + url = "https://developmentseed.org?solutions=geospatial&planet=better" + url_encoded = ( + "https%3A%2F%2Fdevelopmentseed.org%3Fsolutions%3Dgeospatial%26planet%3Dbetter" + ) + + response = client.get(f"/route1?url={url_encoded}") + + assert response.json() == {"url": [url]} diff --git a/src/titiler/core/tests/test_dependencies.py b/src/titiler/core/tests/test_dependencies.py index 6254aa471..e61e078ba 100644 --- a/src/titiler/core/tests/test_dependencies.py +++ b/src/titiler/core/tests/test_dependencies.py @@ -2,15 +2,15 @@ import json from dataclasses import dataclass -from typing import Literal +from typing import Annotated, Literal -import pytest -from fastapi import Depends, FastAPI, Query +from fastapi import Depends, FastAPI, Path from morecantile import tms +from rasterio.crs import CRS from rio_tiler.types import ColorMapType from starlette.testclient import TestClient -from titiler.core import dependencies, errors +from titiler.core import dependencies from titiler.core.resources.responses import JSONResponse @@ -18,15 +18,20 @@ def test_tms(): """Create App.""" app = FastAPI() - @app.get("/web/{TileMatrixSetId}") - def web(TileMatrixSetId: Literal["WebMercatorQuad"] = Query(...)): + @app.get("/web/{tileMatrixSetId}") + def web( + tileMatrixSetId: Annotated[ + Literal["WebMercatorQuad"], + Path(), + ], + ): """return tms id.""" - return TileMatrixSetId + return tileMatrixSetId - @app.get("/all/{TileMatrixSetId}") - def all(TileMatrixSetId: Literal[tuple(tms.list())] = Query(...)): + @app.get("/all/{tileMatrixSetId}") + def all(tileMatrixSetId: Annotated[Literal[tuple(tms.list())], Path()]): """return tms id.""" - return TileMatrixSetId + return tileMatrixSetId client = TestClient(app) response = client.get("/web/WebMercatorQuad") @@ -34,7 +39,7 @@ def all(TileMatrixSetId: Literal[tuple(tms.list())] = Query(...)): response = client.get("/web/WorldCRS84Quad") assert response.status_code == 422 - assert "permitted: 'WebMercatorQuad'" in response.json()["detail"][0]["msg"] + assert "Input should be 'WebMercatorQuad'" in response.json()["detail"][0]["msg"] response = client.get("/all/WebMercatorQuad") assert response.json() == "WebMercatorQuad" @@ -60,7 +65,7 @@ def main(cm=Depends(dependencies.ColorMapParams)): assert response.json()["1"] == [68, 2, 85, 255] cmap = json.dumps({1: [68, 1, 84, 255]}) - response = client.get(f"/?colormap={cmap}") + response = client.get("/", params={"colormap": cmap}) assert response.json()["1"] == [68, 1, 84, 255] cmap = json.dumps({0: "#000000", 255: "#ffffff"}) @@ -75,7 +80,7 @@ def main(cm=Depends(dependencies.ColorMapParams)): ([3, 1000], [255, 0, 0, 255]), ] cmap = json.dumps(intervals) - response = client.get(f"/?colormap={cmap}") + response = client.get("/", params={"colormap": cmap}) assert response.json()[0] == [[1, 2], [0, 0, 0, 255]] assert response.json()[1] == [[2, 3], [255, 255, 255, 255]] assert response.json()[2] == [[3, 1000], [255, 0, 0, 255]] @@ -135,17 +140,20 @@ def main(cm=Depends(dependencies.ColorMapParams)): assert response.json()[0][0] == [0.0, 0.05263157894736842] assert response.json()[0][1] == [247, 252, 240, 255] + response = client.get("/", params={"colormap": "invalid json"}) + assert response.status_code == 400 # this can only be validated via exception + def test_default(): """test default dep behavior.""" @dataclass class dep(dependencies.DefaultDependency): + v: int | None = None - v: int - - # make sure we can unpack the class - assert dict(**dep(v=1)) == {"v": 1} + assert dep(v=1).as_dict() == {"v": 1} + assert dep().as_dict() == {} + assert dep().as_dict(exclude_none=False) == {"v": None} assert dep(v=1).v == 1 @@ -204,131 +212,103 @@ def _assets(params=Depends(dependencies.AssetsParams)): return params.assets @app.get("/second") - def _assets_expr(params=Depends(dependencies.AssetsBidxExprParams)): - """return params.""" - return params - - @app.get("/third") - def _assets_bidx(params=Depends(dependencies.AssetsBidxParams)): + def _assets_expr(params=Depends(dependencies.AssetsExprParams)): """return params.""" return params client = TestClient(app) + response = client.get("/first") + assert response.status_code == 422 + response = client.get("/first?assets=data&assets=image") assert response.json() == ["data", "image"] - response = client.get("/first") - assert not response.json() + response = client.get("/first?assets=data|bidx=1,2|expression=b1*b2") + assert response.json()[0]["name"] == "data" + assert response.json()[0]["indexes"] == [1, 2] + assert response.json()[0]["expression"] == "b1*b2" response = client.get("/second?assets=data&assets=image") assert response.json()["assets"] == ["data", "image"] assert not response.json()["expression"] - response = client.get("/second?expression=data*image") - assert response.json()["expression"] == "data*image" - assert not response.json()["assets"] - - with pytest.raises(errors.MissingAssets): - response = client.get("/second") - - response = client.get( - "/second?assets=data&assets=image&asset_bidx=data|1,2,3&asset_bidx=image|1" - ) + response = client.get("/second?assets=data&assets=image&expression=b1*b2") + assert response.json()["expression"] == "b1*b2" assert response.json()["assets"] == ["data", "image"] - assert response.json()["asset_indexes"] == {"data": [1, 2, 3], "image": [1]} - response = client.get("/third?assets=data&assets=image") - assert response.json()["assets"] == ["data", "image"] - - response = client.get("/third") - assert not response.json()["assets"] - - response = client.get( - "/third?assets=data&assets=image&asset_bidx=data|1,2,3&asset_bidx=image|1" - ) - assert response.json()["assets"] == ["data", "image"] - assert response.json()["asset_indexes"] == {"data": [1, 2, 3], "image": [1]} + response = client.get("/second?assets=data|bidx=1,2|expression=b1*b2") + assert response.json()["assets"][0]["name"] == "data" + assert response.json()["assets"][0]["indexes"] == [1, 2] + assert response.json()["assets"][0]["expression"] == "b1*b2" - response = client.get( - "/third?assets=data&assets=image&asset_expression=data|b1/b2&asset_expression=image|b1*b2" - ) - assert response.json()["assets"] == ["data", "image"] - assert response.json()["asset_expression"] == {"data": "b1/b2", "image": "b1*b2"} + response = client.get("/second") + assert response.status_code == 422 -def test_bands(): - """test bands deps.""" +def test_preview_part_params(): + """test preview/part deps.""" app = FastAPI() - @app.get("/first") - def _bands(params=Depends(dependencies.BandsParams)): - """return bands.""" - return params.bands - - @app.get("/second") - def _bands_expr(params=Depends(dependencies.BandsExprParams)): + @app.get("/preview") + @app.get("/preview/{width}x{height}") + def _endpoint(params=Depends(dependencies.PreviewParams)): """return params.""" return params - @app.get("/third") - def _bands_expr_opt(params=Depends(dependencies.BandsExprParamsOptional)): + @app.get("/part") + @app.get("/part/{width}x{height}") + def _endpoint(params=Depends(dependencies.PartFeatureParams)): """return params.""" return params client = TestClient(app) - response = client.get("/first?bands=b1&bands=b2") - assert response.json() == ["b1", "b2"] - - response = client.get("/first") - assert not response.json() - - response = client.get("/second?bands=b1&bands=b2") - assert response.json()["bands"] == ["b1", "b2"] - - response = client.get("/second", params={"expression": "b1;b2"}) - assert response.json()["expression"] == "b1;b2" - - with pytest.raises(errors.MissingBands): - response = client.get("/second") - - response = client.get("/third?bands=b1&bands=b2") - assert response.json()["bands"] == ["b1", "b2"] - - response = client.get("/third", params={"expression": "b1;b2"}) - assert response.json()["expression"] == "b1;b2" - - response = client.get("/third") - assert not response.json()["bands"] + response = client.get("/preview") + assert response.json()["max_size"] == 1024 + assert not response.json()["height"] + assert not response.json()["width"] + response = client.get("/preview?max_size=2048") + assert response.json()["max_size"] == 2048 + assert not response.json()["height"] + assert not response.json()["width"] -def test_image(): - """test image deps.""" + response = client.get("/preview?width=128") + assert not response.json()["max_size"] + assert not response.json()["height"] + assert response.json()["width"] == 128 - app = FastAPI() + response = client.get("/preview/128x128") + assert not response.json()["max_size"] + assert response.json()["height"] == 128 + assert response.json()["width"] == 128 - @app.get("/") - def _endpoint(params=Depends(dependencies.ImageParams)): - """return params.""" - return params + response = client.get("/preview?width=128&height=128") + assert not response.json()["max_size"] + assert response.json()["height"] == 128 + assert response.json()["width"] == 128 - client = TestClient(app) - response = client.get("/") - assert response.json()["max_size"] == 1024 + response = client.get("/part") + assert not response.json()["max_size"] assert not response.json()["height"] assert not response.json()["width"] - response = client.get("/?max_size=2048") + response = client.get("/part?max_size=2048") assert response.json()["max_size"] == 2048 assert not response.json()["height"] assert not response.json()["width"] - response = client.get("/?width=128") - assert response.json()["max_size"] == 1024 + response = client.get("/part?width=128") + assert not response.json()["max_size"] assert not response.json()["height"] assert response.json()["width"] == 128 - response = client.get("/?width=128&height=128") + response = client.get("/part?width=128&height=128") + assert not response.json()["max_size"] + assert response.json()["height"] == 128 + assert response.json()["width"] == 128 + + response = client.get("/part/128x128") assert not response.json()["max_size"] assert response.json()["height"] == 128 assert response.json()["width"] == 128 @@ -353,7 +333,7 @@ def is_nan(params=Depends(dependencies.DatasetParams)): response = client.get("/") assert not response.json()["nodata"] assert not response.json()["unscale"] - assert response.json()["resampling_method"] == "nearest" + assert not response.json()["resampling_method"] response = client.get("/?resampling=cubic") assert not response.json()["nodata"] @@ -366,6 +346,9 @@ def is_nan(params=Depends(dependencies.DatasetParams)): response = client.get("/nan?nodata=nan") assert response.json() == "nan" + response = client.get("/?nodata=invalid-value") + assert response.status_code == 422 + def test_render(): """test render deps.""" @@ -379,7 +362,7 @@ def _endpoint(params=Depends(dependencies.ImageRenderingParams)): client = TestClient(app) response = client.get("/") - assert response.json()["add_mask"] is True + assert not response.json()["add_mask"] response = client.get("/?return_mask=False") assert response.json()["add_mask"] is False @@ -401,7 +384,7 @@ def test_algo(): def _endpoint(algorithm=Depends(PostProcessParams)): """return params.""" if algorithm: - return algorithm.dict() + return algorithm.model_dump() return {} client = TestClient(app) @@ -412,7 +395,7 @@ def _endpoint(algorithm=Depends(PostProcessParams)): assert response.status_code == 422 response = client.get("/?algorithm=hillshade") - assert response.json()["azimuth"] == 90 + assert response.json()["azimuth"] == 45 assert response.json()["buffer"] == 3 assert response.json()["input_nbands"] == 1 @@ -426,3 +409,254 @@ def _endpoint(algorithm=Depends(PostProcessParams)): assert response.json()["azimuth"] == 30 assert response.json()["buffer"] == 4 assert response.json()["input_nbands"] == 1 + + response = client.get( + "/", + params={ + "algorithm_params": "invalid json", + }, + ) + assert response.status_code == 422 + + +def test_rescale_params(): + """test RescalingParams dependency.""" + app = FastAPI() + + @app.get("/") + def main(params=Depends(dependencies.ImageRenderingParams)): + """return rescale.""" + return params.rescale + + client = TestClient(app) + + response = client.get("/", params={"rescale": "0,1"}) + assert response.status_code == 200 + assert response.json() == [[0, 1]] + + response = client.get("/?rescale=0,1") + assert response.status_code == 200 + assert response.json() == [[0, 1]] + + response = client.get("/?rescale=0,1&rescale=2,3") + assert response.status_code == 200 + assert response.json() == [[0, 1], [2, 3]] + + response = client.get("/", params={"rescale": [0, 1]}) + assert response.status_code == 422 + + response = client.get("/", params={"rescale": [[0, 1]]}) + assert response.status_code == 200 + assert response.json() == [[0, 1]] + + response = client.get( + "/", + params=( + ("rescale", [0, 1]), + ("rescale", [0, 1]), + ), + ) + assert response.status_code == 200 + assert response.json() == [[0, 1], [0, 1]] + + response = client.get( + "/", + params=( + ("rescale", "0,1"), + ("rescale", "0,1"), + ), + ) + assert response.status_code == 200 + assert response.json() == [[0, 1], [0, 1]] + + response = client.get("/", params={"rescale": [[0, 1], [2, 3]]}) + assert response.status_code == 200 + assert response.json() == [[0, 1], [2, 3]] + + response = client.get("/", params={"rescale": "not a number"}) + assert response.status_code == 422 + + response = client.get( + "/", params={"rescale": ["not a number", "also not a number"]} + ) + assert response.status_code == 422 + + +def test_histogram_params(): + """Test HistogramParams dependency.""" + app = FastAPI() + + @app.get("/") + def main(params=Depends(dependencies.HistogramParams)): + """return rescale.""" + return params + + client = TestClient(app) + + response = client.get( + "/", + params={"histogram_bins": "8"}, + ) + assert response.status_code == 200 + assert response.json()["bins"] == 8 + + response = client.get( + "/", + params={"histogram_bins": "8,9"}, + ) + assert response.status_code == 200 + assert response.json()["bins"] == [8.0, 9.0] + + response = client.get( + "/", + params={"histogram_bins": "invalid value"}, + ) + assert response.status_code == 422 + + response = client.get( + "/", + ) + assert response.status_code == 200 + assert response.json()["bins"] == 10 + + response = client.get( + "/", + params={"histogram_range": "8,9"}, + ) + assert response.status_code == 200 + assert response.json()["range"] == [8.0, 9.0] + + response = client.get( + "/", + params={"histogram_range": "8"}, + ) + assert response.status_code == 422 + + response = client.get( + "/", + params={"histogram_range": "invalid value"}, + ) + assert response.status_code == 422 + + +def test_crs_params(): + """Test various crs dependencies.""" + for alias, dependency in { + "crs": dependencies.CRSParams, + "coord_crs": dependencies.CoordCRSParams, + "dst_crs": dependencies.DstCRSParams, + }.items(): + app = FastAPI() + + @app.get("/") + def main(params=Depends(dependency)): + """return crs params.""" + # special handling required for CRS object as it cannot be JSON-serialized by default + return params.to_dict() + + client = TestClient(app) + + response = client.get( + "/", + params={ + alias: """ + GEOGCS["WGS 84", + DATUM["WGS_1984", + SPHEROID["WGS 84",6378137,298.257223563, + AUTHORITY["EPSG","7030"]], + AUTHORITY["EPSG","6326"]], + PRIMEM["Greenwich",0, + AUTHORITY["EPSG","8901"]], + UNIT["degree",0.0174532925199433, + AUTHORITY["EPSG","9122"]], + AUTHORITY["EPSG","4326"]] + """ + }, + ) + assert response.status_code == 200 + + response = client.get("/", params={alias: "epsg:4326"}) + assert response.status_code == 200 + + response = client.get( + "/", params={alias: "+proj=longlat +datum=WGS84 +no_defs +type=crs"} + ) + assert response.status_code == 200 + + response = client.get("/", params={alias: "invalid crs"}) + assert response.status_code == 422 + + +def test_ogc_maps_params_crs(): + """Test OGCMapsParams crs.""" + app = FastAPI() + + @app.get("/") + def main(params=Depends(dependencies.OGCMapsParams)): + """return crs params.""" + # special handling required for CRS object as it cannot be JSON-serialized by default + # exclude request due to recursion during serialization + return { + key: value.to_dict() if isinstance(value, CRS) else value + for key, value in params.as_dict().items() + if key not in ["request"] + } + + client = TestClient(app) + + for field in ["crs", "bbox-crs"]: + response = client.get( + "/", + params={ + field: """ + GEOGCS["WGS 84", + DATUM["WGS_1984", + SPHEROID["WGS 84",6378137,298.257223563, + AUTHORITY["EPSG","7030"]], + AUTHORITY["EPSG","6326"]], + PRIMEM["Greenwich",0, + AUTHORITY["EPSG","8901"]], + UNIT["degree",0.0174532925199433, + AUTHORITY["EPSG","9122"]], + AUTHORITY["EPSG","4326"]] + """ + }, + ) + assert response.status_code == 200 + + response = client.get("/", params={field: "epsg:4326"}) + assert response.status_code == 200 + + response = client.get( + "/", params={field: "+proj=longlat +datum=WGS84 +no_defs +type=crs"} + ) + assert response.status_code == 200 + + response = client.get("/", params={field: "invalid crs"}) + assert response.status_code == 422 + + +def test_ogc_maps_params_bbox(): + """Test OGCMapsParams bbox.""" + app = FastAPI() + + @app.get("/") + def main(params=Depends(dependencies.OGCMapsParams)): + """return bbox params.""" + return params.bbox + + client = TestClient(app) + + response = client.get("/", params={"bbox": "-2,-1,2,1"}) + assert response.status_code == 200 + assert response.json() == [-2, -1, 2, 1] + + response = client.get("/", params={"bbox": "-2,-1,-5,2,1,5"}) + assert response.status_code == 200 + assert response.json() == [-2, -1, 2, 1] + + response = client.get("/", params={"bbox": "0"}) + assert response.status_code == 422 + + response = client.get("/", params={"bbox": "invalid bbox"}) + assert response.status_code == 422 diff --git a/src/titiler/core/tests/test_factories.py b/src/titiler/core/tests/test_factories.py index 408cf96a9..ad939db92 100644 --- a/src/titiler/core/tests/test_factories.py +++ b/src/titiler/core/tests/test_factories.py @@ -1,33 +1,39 @@ """Test TiTiler Tiler Factories.""" import json +import math import os import pathlib +import warnings from dataclasses import dataclass from enum import Enum from io import BytesIO -from typing import Dict, Optional, Type +from typing import Annotated, Dict, Optional, Sequence, Type from unittest.mock import patch -from urllib.parse import urlencode +from urllib.parse import quote, urlencode import attr import httpx import morecantile import numpy +import pytest +from attrs import define from fastapi import Depends, FastAPI, HTTPException, Path, Query, security, status from morecantile.defaults import TileMatrixSets from rasterio.crs import CRS from rasterio.io import MemoryFile +from rio_tiler.colormap import cmap as default_cmap +from rio_tiler.errors import InvalidDatatypeWarning, NoOverviewWarning from rio_tiler.io import BaseReader, MultiBandReader, Reader, STACReader from starlette.requests import Request from starlette.testclient import TestClient -from titiler.core.dependencies import RescaleType +from titiler.core import dependencies from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers from titiler.core.factory import ( AlgorithmFactory, - BaseTilerFactory, - MultiBandTilerFactory, + BaseFactory, + ColorMapFactory, MultiBaseTilerFactory, TilerFactory, TMSFactory, @@ -43,7 +49,7 @@ def test_TilerFactory(): """Test TilerFactory class.""" cog = TilerFactory() - assert len(cog.router.routes) == 27 + assert len(cog.router.routes) == 19 assert len(cog.supported_tms.list()) == NB_DEFAULT_TMS cog = TilerFactory(router_prefix="something", supported_tms=WEB_TMS) @@ -53,7 +59,15 @@ def test_TilerFactory(): app.include_router(cog.router, prefix="/something") client = TestClient(app) - response = client.get(f"/something/tilejson.json?url={DATA_DIR}/cog.tif") + response = client.get("/openapi.json") + assert response.status_code == 200 + + response = client.get("/docs") + assert response.status_code == 200 + + response = client.get( + f"/something/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] @@ -62,7 +76,7 @@ def test_TilerFactory(): assert response.status_code == 422 cog = TilerFactory(add_preview=False, add_part=False, add_viewer=False) - assert len(cog.router.routes) == 18 + assert len(cog.router.routes) == 10 app = FastAPI() cog = TilerFactory() @@ -72,17 +86,19 @@ def test_TilerFactory(): client = TestClient(app) - response = client.get(f"/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000") + response = client.get( + f"/tiles/WebMercatorQuad/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000" + ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" response = client.get( - f"/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=-3.4028235e+38,3.4028235e+38" + f"/tiles/WebMercatorQuad/8/87/48?url={DATA_DIR}/cog.tif&rescale={quote('-3.4028235e+38,3.4028235e+38')}" # + symbols are interpreted as spaces without quoting ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" response = client.get( - f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1&return_mask=false" + f"/tiles/WebMercatorQuad/8/87/48.tif?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1&return_mask=false" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" @@ -93,7 +109,7 @@ def test_TilerFactory(): assert meta["height"] == 256 response = client.get( - "/tiles/8/87/48.tif", + "/tiles/WebMercatorQuad/8/87/48.tif", params={ "url": f"{DATA_DIR}/cog.tif", "expression": "b1;b1;b1", @@ -109,14 +125,17 @@ def test_TilerFactory(): assert meta["height"] == 256 response = client.get( - f"/tiles/8/84/47?url={DATA_DIR}/cog.tif&bidx=1&rescale=0,1000&colormap_name=viridis" + f"/tiles/WebMercatorQuad/8/84/47?url={DATA_DIR}/cog.tif&bidx=1&rescale=0,1000&colormap_name=viridis" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" # Dict - cmap = urlencode( - { + response = client.get( + "/tiles/WebMercatorQuad/8/84/47.png", + params={ + "url": f"{DATA_DIR}/cog.tif", + "bidx": 1, "colormap": json.dumps( { "1": [58, 102, 24, 255], @@ -124,16 +143,18 @@ def test_TilerFactory(): "3": "#b1b129", "4": "#ddcb9aFF", } - ) - } + ), + }, ) - response = client.get(f"/tiles/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}") assert response.status_code == 200 assert response.headers["content-type"] == "image/png" # Intervals - cmap = urlencode( - { + response = client.get( + "/tiles/WebMercatorQuad/8/84/47.png", + params={ + "url": f"{DATA_DIR}/cog.tif", + "bidx": 1, "colormap": json.dumps( [ # ([min, max], [r, g, b, a]) @@ -141,37 +162,50 @@ def test_TilerFactory(): ([2, 3], [255, 255, 255, 255]), ([3, 1000], [255, 0, 0, 255]), ] - ) - } + ), + }, ) - response = client.get(f"/tiles/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}") assert response.status_code == 200 assert response.headers["content-type"] == "image/png" # Bad colormap format cmap = urlencode({"colormap": json.dumps({"1": [58, 102]})}) - response = client.get(f"/tiles/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}") + response = client.get( + f"/tiles/WebMercatorQuad/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}" + ) assert response.status_code == 400 # no json encoding cmap = urlencode({"colormap": {"1": [58, 102]}}) - response = client.get(f"/tiles/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}") + response = client.get( + f"/tiles/WebMercatorQuad/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}" + ) assert response.status_code == 400 # Test NumpyTile - response = client.get(f"/tiles/8/87/48.npy?url={DATA_DIR}/cog.tif") + response = client.get(f"/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" npy_tile = numpy.load(BytesIO(response.content)) assert npy_tile.shape == (2, 256, 256) # mask + data # Test Buffer - response = client.get(f"/tiles/8/87/48.npy?url={DATA_DIR}/cog.tif&buffer=10") + response = client.get( + f"/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif&buffer=10" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" npy_tile = numpy.load(BytesIO(response.content)) assert npy_tile.shape == (2, 276, 276) # mask + data + response = client.get( + f"/tiles/WebMercatorQuad/8/87/48.png?url={DATA_DIR}/cog.tif&tilesize=512" + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["width"] == 512 + assert meta["height"] == 512 + response = client.get( f"/preview?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=256" ) @@ -179,10 +213,21 @@ def test_TilerFactory(): assert response.headers["content-type"] == "image/jpeg" response = client.get( - f"/crop/-56.228,72.715,-54.547,73.188.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=256" + f"/bbox/-56.228,72.715,-54.547,73.188.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=256" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + + response = client.get( + f"/bbox/-56.228,72.715,-54.547,73.188/100x100.png?url={DATA_DIR}/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" + meta = parse_img(response.content) + assert meta["driver"] == "PNG" + assert meta["count"] == 2 + assert meta["width"] == 100 + assert meta["height"] == 100 response = client.get(f"/point/-56.228,72.715?url={DATA_DIR}/cog.tif") assert response.status_code == 200 @@ -190,8 +235,15 @@ def test_TilerFactory(): assert len(response.json()["values"]) == 1 assert response.json()["band_names"] == ["b1"] + # Masked values + response = client.get(f"/point/-59.337,73.9898?url={DATA_DIR}/cog.tif&nodata=1") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + assert response.json()["values"] == [None] + assert response.json()["band_names"] == ["b1"] + response = client.get( - f"/point/-6259272.328324187,12015838.020930404?url={DATA_DIR}/cog.tif&coord-crs=EPSG:3857" + f"/point/-6259272.328324187,12015838.020930404?url={DATA_DIR}/cog.tif&coord_crs=EPSG:3857" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" @@ -200,64 +252,52 @@ def test_TilerFactory(): response = client.get(f"/point/-56.228,72.715?url={DATA_DIR}/cog.tif&bidx=1&bidx=1") assert len(response.json()["values"]) == 2 - assert response.json()["band_names"] == ["b1", "b1"] + assert response.json()["band_names"] == ["b1", "b2"] + assert response.json()["band_descriptions"] == ["b1", "b1"] response = client.get( f"/point/-56.228,72.715?url={DATA_DIR}/cog.tif&expression=b1*2" ) assert len(response.json()["values"]) == 1 - assert response.json()["band_names"] == ["b1*2"] - - response = client.get(f"/tilejson.json?url={DATA_DIR}/cog.tif") - assert response.status_code == 200 - assert response.headers["content-type"] == "application/json" - assert response.json()["tilejson"] + assert response.json()["band_descriptions"] == ["b1*2"] - response = client.get(f"/WorldCRS84Quad/tilejson.json?url={DATA_DIR}/cog.tif") + # NOTE: tilejson tilesize default to 512x512 + # because Mapbox and Maplibre expect tiles to be 512x512 + response = client.get(f"/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] + assert "tilesize=512" in response.json()["tiles"][0] - response_qs = client.get( - f"/tilejson.json?url={DATA_DIR}/cog.tif&TileMatrixSetId=WorldCRS84Quad" + response = client.get( + f"/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif&tilesize=256" ) - assert response.json()["tiles"] == response_qs.json()["tiles"] - - response = client.get(f"/tilejson.json?url={DATA_DIR}/cog.tif&tile_format=png") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] - assert "png" in response.json()["tiles"][0] + assert "tilesize=256" in response.json()["tiles"][0] - response = client.get(f"/tilejson.json?url={DATA_DIR}/cog.tif&minzoom=5&maxzoom=12") + response = client.get(f"/WorldCRS84Quad/tilejson.json?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] - assert response.json()["minzoom"] == 5 - assert response.json()["maxzoom"] == 12 response = client.get( - f"/WMTSCapabilities.xml?url={DATA_DIR}/cog.tif&minzoom=5&maxzoom=12" + f"/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif&tile_format=png" ) assert response.status_code == 200 - assert response.headers["content-type"] == "application/xml" - meta = parse_img(response.content) - assert meta["driver"] == "WMTS" - assert meta["crs"] == "EPSG:3857" + assert response.headers["content-type"] == "application/json" + assert response.json()["tilejson"] + assert "png" in response.json()["tiles"][0] response = client.get( - f"/WorldCRS84Quad/WMTSCapabilities.xml?url={DATA_DIR}/cog.tif&minzoom=5&maxzoom=12" + f"/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif&minzoom=5&maxzoom=12" ) assert response.status_code == 200 - assert response.headers["content-type"] == "application/xml" - meta = parse_img(response.content) - assert meta["driver"] == "WMTS" - assert str(meta["crs"]) == "OGC:CRS84" - - response = client.get(f"/bounds?url={DATA_DIR}/cog.tif") - assert response.status_code == 200 assert response.headers["content-type"] == "application/json" - assert response.json()["bounds"] + assert response.json()["tilejson"] + assert response.json()["minzoom"] == 5 + assert response.json()["maxzoom"] == 12 response = client.get(f"/info?url={DATA_DIR}/cog.tif") assert response.status_code == 200 @@ -268,6 +308,17 @@ def test_TilerFactory(): assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" assert response.json()["type"] == "Feature" + assert "bbox" in response.json() + assert response.json()["geometry"]["type"] == "Polygon" + + # BBOX crossing the Antimeridian + with pytest.warns(UserWarning): + response = client.get(f"/info.geojson?url={DATA_DIR}/cog_dateline.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/geo+json" + assert response.json()["type"] == "Feature" + assert "bbox" in response.json() + assert response.json()["geometry"]["type"] == "MultiPolygon" response = client.get( f"/preview.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=256" @@ -286,14 +337,12 @@ def test_TilerFactory(): assert meta["width"] == 512 assert meta["height"] == 512 - response = client.get( - f"/preview.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=0&nodata=0" - ) + response = client.get(f"/preview/512x512.png?url={DATA_DIR}/cog.tif&rescale=0,1000") assert response.status_code == 200 assert response.headers["content-type"] == "image/png" meta = parse_img(response.content) - assert meta["width"] == 2658 - assert meta["height"] == 2667 + assert meta["width"] == 512 + assert meta["height"] == 512 response = client.get( f"/preview.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=0&nodata=0" @@ -345,18 +394,31 @@ def test_TilerFactory(): feature_collection = {"type": "FeatureCollection", "features": [feature]} - response = client.post(f"/crop?url={DATA_DIR}/cog.tif", json=feature) + response = client.post(f"/feature?url={DATA_DIR}/cog.tif", json=feature) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + + response = client.post( + f"/feature/100x100.png?url={DATA_DIR}/cog.tif&rescale=0,1000", json=feature + ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" + meta = parse_img(response.content) + assert meta["driver"] == "PNG" + assert meta["width"] == 100 + assert meta["height"] == 100 - response = client.post(f"/crop.tif?url={DATA_DIR}/cog.tif", json=feature) + response = client.post(f"/feature.tif?url={DATA_DIR}/cog.tif", json=feature) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" meta = parse_img(response.content) assert meta["dtype"] == "uint16" assert meta["count"] == 2 - response = client.post(f"/crop/100x100.jpeg?url={DATA_DIR}/cog.tif", json=feature) + with pytest.warns(InvalidDatatypeWarning): + response = client.post( + f"/feature/100x100.jpeg?url={DATA_DIR}/cog.tif", json=feature + ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" meta = parse_img(response.content) @@ -364,12 +426,7 @@ def test_TilerFactory(): assert meta["height"] == 100 # GET - statistics - response = client.get(f"/statistics?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1") - assert response.status_code == 200 - assert response.headers["content-type"] == "application/json" - resp = response.json() - assert len(resp) == 1 - assert set(resp["b1"].keys()) == { + stats_keys = [ "min", "max", "mean", @@ -384,34 +441,33 @@ def test_TilerFactory(): "valid_percent", "masked_pixels", "valid_pixels", + "description", + ] + + response = client.get(f"/statistics?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp) == 3 + assert set(resp["b1"].keys()) == { + *stats_keys, "percentile_2", "percentile_98", } assert len(resp["b1"]["histogram"][0]) == 10 + assert resp["b1"]["description"] == "b1" response = client.get(f"/statistics?url={DATA_DIR}/cog.tif&expression=b1*2") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" resp = response.json() assert len(resp) == 1 - assert set(resp["b1*2"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", + assert set(resp["b1"].keys()) == { + *stats_keys, "percentile_2", "percentile_98", } + assert resp["b1"]["description"] == "b1*2" response = client.get( f"/statistics?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1&p=4&p=5" @@ -419,25 +475,13 @@ def test_TilerFactory(): assert response.status_code == 200 assert response.headers["content-type"] == "application/json" resp = response.json() - assert len(resp) == 1 + assert len(resp) == 3 assert set(resp["b1"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", + *stats_keys, "percentile_4", "percentile_5", } + assert resp["b1"]["description"] == "b1" response = client.get(f"/statistics?url={DATA_DIR}/cog.tif&categorical=true") assert response.status_code == 200 @@ -445,20 +489,7 @@ def test_TilerFactory(): resp = response.json() assert len(resp) == 1 assert set(resp["b1"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", + *stats_keys, "percentile_2", "percentile_98", } @@ -473,20 +504,7 @@ def test_TilerFactory(): resp = response.json() assert len(resp) == 1 assert set(resp["b1"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", + *stats_keys, "percentile_2", "percentile_98", } @@ -510,6 +528,16 @@ def test_TilerFactory(): assert min(resp["b1"]["histogram"][1]) == 5.0 assert max(resp["b1"]["histogram"][1]) == 10.0 + # Stats with Algorithm + response = client.get( + f"/statistics?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&algorithm=normalizedIndex" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp) == 1 + assert resp["b1"]["description"] == "(b1 - b1) / (b1 + b1)" + # POST - statistics response = client.post( f"/statistics?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1", json=feature @@ -518,25 +546,14 @@ def test_TilerFactory(): assert response.headers["content-type"] == "application/geo+json" resp = response.json() assert resp["type"] == "Feature" - assert len(resp["properties"]["statistics"]) == 1 + assert len(resp["properties"]["statistics"]) == 3 assert set(resp["properties"]["statistics"]["b1"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", + *stats_keys, "percentile_2", "percentile_98", - "valid_pixels", - "masked_pixels", - "valid_percent", } + assert resp["properties"]["statistics"]["b1"]["description"] == "b1" + assert resp["properties"]["statistics"]["b2"]["description"] == "b1" response = client.post( f"/statistics?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1", @@ -546,28 +563,19 @@ def test_TilerFactory(): assert response.headers["content-type"] == "application/geo+json" resp = response.json() assert resp["type"] == "FeatureCollection" - assert len(resp["features"][0]["properties"]["statistics"]) == 1 + assert len(resp["features"][0]["properties"]["statistics"]) == 3 assert set(resp["features"][0]["properties"]["statistics"]["b1"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", + *stats_keys, "percentile_2", "percentile_98", - "valid_pixels", - "masked_pixels", - "valid_percent", } + assert resp["features"][0]["properties"]["statistics"]["b1"]["description"] == "b1" + assert resp["features"][0]["properties"]["statistics"]["b2"]["description"] == "b1" response = client.post( - f"/statistics?url={DATA_DIR}/cog.tif&categorical=true", json=feature + "/statistics", + json=feature, + params={"categorical": True, "max_size": 1024, "url": f"{DATA_DIR}/cog.tif"}, ) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" @@ -575,28 +583,25 @@ def test_TilerFactory(): assert resp["type"] == "Feature" assert len(resp["properties"]["statistics"]) == 1 assert set(resp["properties"]["statistics"]["b1"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", + *stats_keys, "percentile_2", "percentile_98", - "valid_pixels", - "masked_pixels", - "valid_percent", } - assert len(resp["properties"]["statistics"]["b1"]["histogram"][1]) == 12 + assert len(resp["properties"]["statistics"]["b1"]["histogram"][1]) == 13 + assert resp["properties"]["statistics"]["b1"]["description"] == "b1" response = client.post( - f"/statistics?url={DATA_DIR}/cog.tif&categorical=true&c=1&c=2&c=3&c=4", + "/statistics", json=feature, + params=( + ("categorical", True), + ("c", 1), + ("c", 2), + ("c", 3), + ("c", 4), + ("max_size", 1024), + ("url", f"{DATA_DIR}/cog.tif"), + ), ) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" @@ -604,26 +609,27 @@ def test_TilerFactory(): assert resp["type"] == "Feature" assert len(resp["properties"]["statistics"]) == 1 assert set(resp["properties"]["statistics"]["b1"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", + *stats_keys, "percentile_2", "percentile_98", - "valid_pixels", - "masked_pixels", - "valid_percent", } assert len(resp["properties"]["statistics"]["b1"]["histogram"][0]) == 4 assert resp["properties"]["statistics"]["b1"]["histogram"][0][3] == 0 + # Stats with Algorithm + response = client.post( + f"/statistics?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&algorithm=normalizedIndex", + json=feature, + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/geo+json" + resp = response.json() + assert resp["type"] == "Feature" + assert len(resp["properties"]["statistics"]) == 1 + assert ( + resp["properties"]["statistics"]["b1"]["description"] == "(b1 - b1) / (b1 + b1)" + ) + # Test with Algorithm response = client.get(f"/preview.tif?url={DATA_DIR}/dem.tif&return_mask=False") assert response.status_code == 200 @@ -641,6 +647,25 @@ def test_TilerFactory(): assert meta["dtype"] == "uint8" assert meta["count"] == 3 + # OGC Tileset + response = client.get(f"/tiles?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp["tilesets"]) == NB_DEFAULT_TMS + + first_tms = resp["tilesets"][0] + first_id = DEFAULT_TMS.list()[0] + assert first_id in first_tms["title"] + assert len(first_tms["links"]) == 2 # no link to the tms definition + + response = client.get(f"/tiles/WebMercatorQuad?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + # covers only 5 zoom levels + assert len(resp["tileMatrixSetLimits"]) == 5 + @patch("rio_tiler.io.rasterio.rasterio") def test_MultiBaseTilerFactory(rio): @@ -648,7 +673,7 @@ def test_MultiBaseTilerFactory(rio): rio.open = mock_rasterio_open stac = MultiBaseTilerFactory(reader=STACReader) - assert len(stac.router.routes) == 29 + assert len(stac.router.routes) == 21 app = FastAPI() app.include_router(stac.router) @@ -657,15 +682,22 @@ def test_MultiBaseTilerFactory(rio): client = TestClient(app) - response = client.get(f"/assets?url={DATA_DIR}/item.json") + response = client.get("/openapi.json") + assert response.status_code == 200 + + response = client.get("/docs") assert response.status_code == 200 - assert len(response.json()) == 2 - response = client.get(f"/bounds?url={DATA_DIR}/item.json") + response = client.get(f"/assets?url={DATA_DIR}/item.json") assert response.status_code == 200 - assert len(response.json()["bounds"]) == 4 + assert len(response.json()) == 2 + # no assets response = client.get(f"/info?url={DATA_DIR}/item.json") + assert response.status_code == 422 + + # :all: assets + response = client.get(f"/info?url={DATA_DIR}/item.json&assets=:all:") assert response.status_code == 200 assert len(response.json()) == 2 @@ -679,8 +711,9 @@ def test_MultiBaseTilerFactory(rio): assert response.headers["content-type"] == "application/geo+json" assert response.json()["type"] == "Feature" + # missing assets response = client.get(f"/preview.tif?url={DATA_DIR}/item.json") - assert response.status_code == 400 + assert response.status_code == 422 response = client.get( f"/preview.tif?url={DATA_DIR}/item.json&assets=B01&assets=B09&return_mask=false" @@ -695,30 +728,36 @@ def test_MultiBaseTilerFactory(rio): "/preview.tif", params={ "url": f"{DATA_DIR}/item.json", - "expression": "B01_b1;B01_b1;B01_b1", + "assets": "B01|bidx=1,1,1", "return_mask": False, }, ) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" meta = parse_img(response.content) - assert meta["dtype"] == "int32" + assert meta["dtype"] == "uint16" assert meta["count"] == 3 response = client.get( - f"/preview.tif?url={DATA_DIR}/item.json&assets=B01&asset_bidx=B01|1,1,1&return_mask=false" + "/preview.tif", + params={ + "url": f"{DATA_DIR}/item.json", + "assets": "B01", + "expression": "b1;b1;b1", + "return_mask": False, + }, ) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" meta = parse_img(response.content) - assert meta["dtype"] == "uint16" + assert meta["dtype"] == "int32" assert meta["count"] == 3 response = client.get( "/preview.tif", params={ "url": f"{DATA_DIR}/item.json", - "expression": "B01_b1;B01_b1;B01_b1", + "assets": "B01|expression=b1;b1;b1", "return_mask": False, }, ) @@ -733,7 +772,7 @@ def test_MultiBaseTilerFactory(rio): "/preview.tif", params={ "url": f"{DATA_DIR}/item.json", - "expression": "B01;B01;B01", + "assets": "B01", "asset_as_band": True, "return_mask": False, }, @@ -741,18 +780,11 @@ def test_MultiBaseTilerFactory(rio): assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" meta = parse_img(response.content) - assert meta["dtype"] == "int32" - assert meta["count"] == 3 + assert meta["dtype"] == "uint16" + assert meta["count"] == 1 # GET - statistics - response = client.get( - f"/asset_statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09" - ) - assert response.status_code == 200 - assert response.headers["content-type"] == "application/json" - resp = response.json() - assert len(resp) == 2 - assert set(resp["B01"]["b1"].keys()) == { + stats_keys = [ "min", "max", "mean", @@ -767,326 +799,87 @@ def test_MultiBaseTilerFactory(rio): "valid_percent", "masked_pixels", "valid_pixels", - "percentile_2", - "percentile_98", - } + "description", + ] + response = client.get( - f"/asset_statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&asset_bidx=B01|1&asset_bidx=B09|1" + f"/asset_statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" resp = response.json() assert len(resp) == 2 - assert resp["B01"]["b1"] - assert resp["B09"]["b1"] - - response = client.get(f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09") - assert response.status_code == 200 - assert response.headers["content-type"] == "application/json" - resp = response.json() - assert list(resp) == ["B01_b1", "B09_b1"] - assert set(resp["B01_b1"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", + assert set(resp["B01"]["b1"].keys()) == { + *stats_keys, "percentile_2", "percentile_98", } + assert resp["B09"]["b1"]["description"] == "b1" response = client.get( - f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&asset_bidx=B01|1&asset_bidx=B09|1" + f"/asset_statistics?url={DATA_DIR}/item.json&assets=B09|bidx=1,1" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" resp = response.json() - assert len(resp) == 2 - assert resp["B01_b1"] - assert resp["B09_b1"] - - stac_feature = { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "properties": {}, - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [23.62060546875, 31.834399275715842], - [23.838958740234375, 31.834399275715842], - [23.838958740234375, 32.072101858328686], - [23.62060546875, 32.072101858328686], - [23.62060546875, 31.834399275715842], - ] - ], - }, - } - ], - } + assert len(resp) == 1 + assert resp["B09|indexes=[1,1]"]["b1"] + assert resp["B09|indexes=[1,1]"]["b2"] + assert resp["B09|indexes=[1,1]"]["b1"]["description"] == "b1" + assert resp["B09|indexes=[1,1]"]["b2"]["description"] == "b1" - # POST - statistics - response = client.post( - f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09", - json=stac_feature["features"][0], - ) - assert response.status_code == 200 - assert response.headers["content-type"] == "application/geo+json" - resp = response.json() - props = resp["properties"]["statistics"] - assert len(props) == 2 - assert set(props["B01_b1"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", - "percentile_2", - "percentile_98", - } - assert props["B09_b1"] + # missing assets + response = client.get(f"/statistics?url={DATA_DIR}/item.json") + assert response.status_code == 422 - response = client.post( - f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09", json=stac_feature - ) + # all assets + response = client.get(f"/statistics?url={DATA_DIR}/item.json&assets=:all:") assert response.status_code == 200 - assert response.headers["content-type"] == "application/geo+json" + assert response.headers["content-type"] == "application/json" resp = response.json() - props = resp["features"][0]["properties"]["statistics"] - assert len(props) == 2 - assert set(props["B01_b1"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", - "percentile_2", - "percentile_98", - } - assert props["B09_b1"] + assert len(resp) == 2 - response = client.post( - f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&asset_bidx=B01|1&asset_bidx=B09|1", - json=stac_feature["features"][0], - ) + response = client.get(f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09") assert response.status_code == 200 - assert response.headers["content-type"] == "application/geo+json" + assert response.headers["content-type"] == "application/json" resp = response.json() - props = resp["properties"]["statistics"] - assert len(props) == 2 - assert set(props["B01_b1"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", + assert list(resp) == ["b1", "b2"] + assert set(resp["b1"].keys()) == { + *stats_keys, "percentile_2", "percentile_98", } - assert props["B09_b1"] - - -@attr.s -class BandFileReader(MultiBandReader): - """Test MultiBand""" - - input: str = attr.ib() - tms: morecantile.TileMatrixSet = attr.ib( - default=morecantile.tms.get("WebMercatorQuad") - ) - reader_options: Dict = attr.ib(factory=dict) - - reader: Type[BaseReader] = attr.ib(default=Reader) - - minzoom: int = attr.ib() - maxzoom: int = attr.ib() - - @minzoom.default - def _minzoom(self): - return self.tms.minzoom - - @maxzoom.default - def _maxzoom(self): - return self.tms.maxzoom - - def __attrs_post_init__(self): - """Parse Sceneid and get grid bounds.""" - self.bands = sorted([p.stem for p in pathlib.Path(self.input).glob("B0*.tif")]) - with self.reader(self._get_band_url(self.bands[0])) as cog: - self.bounds = cog.bounds - self.crs = cog.crs - self.minzoom = cog.minzoom - self.maxzoom = cog.maxzoom - - def _get_band_url(self, band: str) -> str: - """Validate band's name and return band's url.""" - return os.path.join(self.input, f"{band}.tif") - - -def CustomPathParams(directory: str = Query(..., description="Give me a url.")) -> str: - """Custom path Dependency.""" - return directory - - -def test_MultiBandTilerFactory(): - """test MultiBandTilerFactory.""" - - bands = MultiBandTilerFactory( - reader=BandFileReader, path_dependency=CustomPathParams - ) - assert len(bands.router.routes) == 28 - - app = FastAPI() - app.include_router(bands.router) - - add_exception_handlers(app, DEFAULT_STATUS_CODES) - - client = TestClient(app) - - response = client.get(f"/bands?directory={DATA_DIR}") - assert response.status_code == 200 - assert response.json() == ["B01", "B09"] - - # default bands - response = client.get(f"/info?directory={DATA_DIR}") - assert response.json()["band_metadata"] == [["B01", {}], ["B09", {}]] - - response = client.get(f"/info?directory={DATA_DIR}&bands=B01") - assert response.status_code == 200 - assert response.json()["band_metadata"] == [["B01", {}]] - - response = client.get(f"/info.geojson?directory={DATA_DIR}&bands=B01") - assert response.status_code == 200 - assert response.headers["content-type"] == "application/geo+json" - assert response.json()["properties"]["band_metadata"] == [["B01", {}]] - - # need bands or expression - response = client.get(f"/preview.tif?directory={DATA_DIR}&return_mask=false") - assert response.status_code == 400 - - response = client.get( - f"/preview.tif?directory={DATA_DIR}&bands=B01&bands=B09&bands=B01&return_mask=false" - ) - assert response.status_code == 200 - assert response.headers["content-type"] == "image/tiff; application=geotiff" - meta = parse_img(response.content) - assert meta["dtype"] == "uint16" - assert meta["count"] == 3 - - response = client.get( - "/preview.tif", - params={ - "directory": DATA_DIR, - "expression": "B01;B09;B01", - "return_mask": False, - }, - ) - assert response.status_code == 200 - assert response.headers["content-type"] == "image/tiff; application=geotiff" - meta = parse_img(response.content) - assert ( - meta["dtype"] == "int32" - ) # when using expression, numexpr will change the datatype - assert meta["count"] == 3 + assert resp["b1"]["description"] == "B01_b1" + assert resp["b2"]["description"] == "B09_b1" - # GET - statistics - response = client.get(f"/statistics?directory={DATA_DIR}") + response = client.get(f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" resp = response.json() assert len(resp) == 2 - assert resp["B01"] - assert resp["B09"] + assert resp["b1"]["description"] == "B01_b1" + assert resp["b2"]["description"] == "B09_b1" - response = client.get(f"/statistics?directory={DATA_DIR}&bands=B01&bands=B09") + response = client.get( + f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&asset_as_band=True" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" resp = response.json() assert len(resp) == 2 - assert set(resp["B01"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", - "percentile_2", - "percentile_98", - } - assert resp["B09"] + assert resp["b1"]["description"] == "B01" + assert resp["b2"]["description"] == "B09" - response = client.get(f"/statistics?directory={DATA_DIR}&expression=B01/B09") + # with Algorithm + response = client.get( + f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&algorithm=normalizedIndex&asset_as_band=True" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" resp = response.json() - assert len(resp) == 1 - assert set(resp["B01/B09"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", - "percentile_2", - "percentile_98", - } + assert resp["b1"]["description"] == "(B09 - B01) / (B09 + B01)" - # POST - statistics - band_feature = { + stac_feature = { "type": "FeatureCollection", "features": [ { @@ -1108,137 +901,154 @@ def test_MultiBandTilerFactory(): ], } + # POST - statistics response = client.post( - f"/statistics?directory={DATA_DIR}&bands=B01&bands=B09", - json=band_feature["features"][0], + f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09", + json=stac_feature["features"][0], ) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" resp = response.json() props = resp["properties"]["statistics"] assert len(props) == 2 - assert set(props["B01"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", + assert set(props["b1"].keys()) == { + *stats_keys, "percentile_2", "percentile_98", } - assert props["B09"] + assert props["b1"]["description"] == "B01_b1" + assert props["b2"]["description"] == "B09_b1" + # missing assets response = client.post( - f"/statistics?directory={DATA_DIR}&expression=B01/B09", - json=band_feature["features"][0], + f"/statistics?url={DATA_DIR}/item.json", + json=stac_feature["features"][0], + ) + assert response.status_code == 422 + + # all assets + response = client.post( + f"/statistics?url={DATA_DIR}/item.json&assets=:all:", + json=stac_feature["features"][0], ) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" resp = response.json() - props = resp["properties"]["statistics"] - assert len(props) == 1 - assert set(props["B01/B09"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", - "percentile_2", - "percentile_98", - } + assert len(resp["properties"]["statistics"]) == 2 response = client.post( - f"/statistics?directory={DATA_DIR}&bands=B01&bands=B09", json=band_feature + f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09", json=stac_feature ) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" resp = response.json() props = resp["features"][0]["properties"]["statistics"] assert len(props) == 2 - assert set(props["B01"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", + assert set(props["b1"].keys()) == { + *stats_keys, "percentile_2", "percentile_98", } - assert props["B09"] + assert props["b1"]["description"] == "B01_b1" response = client.post( - f"/statistics?directory={DATA_DIR}&expression=B01/B09", json=band_feature + f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&asset_as_band=True", + json=stac_feature["features"][0], ) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" resp = response.json() - props = resp["features"][0]["properties"]["statistics"] - assert len(props) == 1 - assert set(props["B01/B09"].keys()) == { - "min", - "max", - "mean", - "count", - "sum", - "std", - "median", - "majority", - "minority", - "unique", - "histogram", - "valid_percent", - "masked_pixels", - "valid_pixels", + props = resp["properties"]["statistics"] + assert len(props) == 2 + assert set(props["b1"].keys()) == { + *stats_keys, "percentile_2", "percentile_98", } + assert props["b1"]["description"] == "B01" + assert props["b2"]["description"] == "B09" - # default bands - response = client.post(f"/statistics?directory={DATA_DIR}", json=band_feature) + response = client.post( + f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&expression=b1/b2&asset_as_band=True", + json=stac_feature["features"][0], + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" resp = response.json() - props = resp["features"][0]["properties"]["statistics"] - assert props["B01"] - assert props["B09"] + props = resp["properties"]["statistics"] + assert len(props) == 1 + assert set(props["b1"].keys()) == { + *stats_keys, + "percentile_2", + "percentile_98", + } + assert props["b1"]["description"] == "B01/B09" + # with Algorithm response = client.post( - f"/statistics?directory={DATA_DIR}", - json=band_feature["features"][0], + f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&algorithm=normalizedIndex&asset_as_band=True", + json=stac_feature["features"][0], ) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" resp = response.json() props = resp["properties"]["statistics"] - assert props["B01"] - assert props["B09"] + assert len(props) == 1 + assert props["b1"]["description"] == "(B09 - B01) / (B09 + B01)" + + # OGC Tileset + response = client.get(f"/tiles?url={DATA_DIR}/item.json") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp["tilesets"]) == NB_DEFAULT_TMS + + first_tms = resp["tilesets"][0] + first_id = DEFAULT_TMS.list()[0] + assert first_id in first_tms["title"] + assert len(first_tms["links"]) == 2 # no link to the tms definition + + response = client.get(f"/tiles/WebMercatorQuad?url={DATA_DIR}/item.json") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + # default minzoom/maxzoom are 0->24 + assert len(resp["tileMatrixSetLimits"]) == 25 + + +@attr.s +class BandFileReader(MultiBandReader): + """Test MultiBand""" + + input: str = attr.ib() + tms: morecantile.TileMatrixSet = attr.ib( + default=morecantile.tms.get("WebMercatorQuad") + ) + + reader: Type[BaseReader] = attr.ib(default=Reader) + reader_options: Dict = attr.ib(factory=dict) + + bands: Sequence[str] = attr.ib(init=False) + default_bands: Optional[Sequence[str]] = attr.ib(init=False, default=None) + + minzoom: int = attr.ib(init=False) + maxzoom: int = attr.ib(init=False) + + def __attrs_post_init__(self): + """Parse Sceneid and get grid bounds.""" + self.bands = sorted([p.stem for p in pathlib.Path(self.input).glob("B0*.tif")]) + with self.reader(self._get_band_url(self.bands[0])) as cog: + self.bounds = cog.bounds + self.crs = cog.crs + self.minzoom = cog.minzoom + self.maxzoom = cog.maxzoom + self.width = cog.width + self.height = cog.height + self.transform = cog.transform + + def _get_band_url(self, band: str) -> str: + """Validate band's name and return band's url.""" + return os.path.join(self.input, f"{band}.tif") def test_TMSFactory(): @@ -1267,8 +1077,7 @@ def test_TMSFactory(): response = client.get("/tms/tileMatrixSets/WebMercatorQuad") assert response.status_code == 200 body = response.json() - assert body["type"] == "TileMatrixSetType" - assert body["identifier"] == "WebMercatorQuad" + assert body["id"] == "WebMercatorQuad" response = client.get("/tms/tileMatrixSets/WebMercatorQua") assert response.status_code == 422 @@ -1311,15 +1120,15 @@ def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic) route_dependencies=[ ( [ - {"path": "/bounds", "method": "GET"}, - {"path": "/tiles/{z}/{x}/{y}", "method": "GET"}, + {"path": "/info", "method": "GET"}, + {"path": "/tiles/{tileMatrixSetId}/{z}/{x}/{y}", "method": "GET"}, ], [Depends(must_be_bob)], ), ], router_prefix="something", ) - assert len(cog.router.routes) == 27 + assert len(cog.router.routes) == 19 app = FastAPI() app.include_router(cog.router, prefix="/something") @@ -1328,44 +1137,43 @@ def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic) auth_bob = httpx.BasicAuth(username="bob", password="ILoveSponge") auth_notbob = httpx.BasicAuth(username="notbob", password="IHateSponge") - response = client.get(f"/something/tilejson.json?url={DATA_DIR}/cog.tif") + response = client.get( + f"/something/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] - response = client.get( - f"/something/bounds?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_bob - ) + response = client.get(f"/something/info?url={DATA_DIR}/cog.tif", auth=auth_bob) assert response.status_code == 200 - response = client.get( - f"/something/bounds?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_notbob - ) + response = client.get(f"/something/info?url={DATA_DIR}/cog.tif", auth=auth_notbob) assert response.status_code == 401 assert response.json()["detail"] == "You're not Bob" response = client.get( - f"/something/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_bob + f"/something/tiles/WebMercatorQuad/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000", + auth=auth_bob, ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" response = client.get( - f"/something/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000", + f"/something/tiles/WebMercatorQuad/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_notbob, ) assert response.status_code == 401 assert response.json()["detail"] == "You're not Bob" response = client.get( - f"/something/tiles/8/87/48.jpeg?url={DATA_DIR}/cog.tif&rescale=0,1000" + f"/something/tiles/WebMercatorQuad/8/87/48.jpeg?url={DATA_DIR}/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" cog = TilerFactory(router_prefix="something") cog.add_route_dependencies( - scopes=[{"path": "/bounds", "method": "GET"}], + scopes=[{"path": "/info", "method": "GET"}], dependencies=[Depends(must_be_bob)], ) @@ -1373,19 +1181,17 @@ def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic) app.include_router(cog.router, prefix="/something") client = TestClient(app) - response = client.get(f"/something/tilejson.json?url={DATA_DIR}/cog.tif") + response = client.get( + f"/something/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] - response = client.get( - f"/something/bounds?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_bob - ) + response = client.get(f"/something/info?url={DATA_DIR}/cog.tif", auth=auth_bob) assert response.status_code == 200 - response = client.get( - f"/something/bounds?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_notbob - ) + response = client.get(f"/something/info?url={DATA_DIR}/cog.tif", auth=auth_notbob) assert response.status_code == 401 assert response.json()["detail"] == "You're not Bob" @@ -1400,7 +1206,8 @@ def test_TilerFactory_WithGdalEnv(): app.include_router(router) client = TestClient(app) - response = client.get(f"/info?url={DATA_DIR}/non_cog.tif") + with pytest.warns(NoOverviewWarning): + response = client.get(f"/info?url={DATA_DIR}/non_cog.tif") assert not response.json()["overviews"] router = TilerFactory( @@ -1414,7 +1221,6 @@ def test_TilerFactory_WithGdalEnv(): assert response.json()["overviews"] class ReaddirType(str, Enum): - false = "false" true = "true" empty_dir = "empty_dir" @@ -1427,14 +1233,21 @@ def gdal_env(disable_read: ReaddirType = Query(ReaddirType.false)): app.include_router(router) client = TestClient(app) - response = client.get(f"/info?url={DATA_DIR}/non_cog.tif") - assert response.json()["overviews"] + with warnings.catch_warnings(): + warnings.simplefilter("error") + response = client.get(f"/info?url={DATA_DIR}/non_cog.tif") + assert response.json()["overviews"] - response = client.get(f"/info?url={DATA_DIR}/non_cog.tif&disable_read=false") - assert response.json()["overviews"] + with warnings.catch_warnings(): + warnings.simplefilter("error") + response = client.get(f"/info?url={DATA_DIR}/non_cog.tif&disable_read=false") + assert response.json()["overviews"] - response = client.get(f"/info?url={DATA_DIR}/non_cog.tif&disable_read=empty_dir") - assert not response.json()["overviews"] + with pytest.warns(NoOverviewWarning): + response = client.get( + f"/info?url={DATA_DIR}/non_cog.tif&disable_read=empty_dir" + ) + assert not response.json()["overviews"] def test_algorithm(): @@ -1447,17 +1260,34 @@ def test_algorithm(): response = client.get("/algorithms") assert response.status_code == 200 - assert "hillshade" in response.json() + algo_ids = [algo["id"] for algo in response.json()["algorithms"]] + assert "hillshade" in algo_ids + + response = client.get("/algorithms", params={"f": "html"}) + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + + response = client.get("/algorithms", headers={"Accept": "text/html"}) + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] response = client.get("/algorithms/hillshade") assert response.status_code == 200 + response = client.get("/algorithms/hillshade", params={"f": "html"}) + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + + response = client.get("/algorithms/hillshade", headers={"Accept": "text/html"}) + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + def test_path_param_in_prefix(): """Test path params in prefix.""" - @dataclass - class EndpointFactory(BaseTilerFactory): + @define + class EndpointFactory(BaseFactory): def register_routes(self): """register endpoints.""" @@ -1474,7 +1304,7 @@ def route1(param1: int = Path(...), param2: str = Path(...)): return {"value": param2} app = FastAPI() - endpoints = EndpointFactory(reader=Reader, router_prefix="/prefixed/{param1}") + endpoints = EndpointFactory(router_prefix="/prefixed/{param1}") app.include_router(endpoints.router, prefix="/prefixed/{param1}") client = TestClient(app) @@ -1495,9 +1325,11 @@ def test_AutoFormat_Colormap(): app.include_router(cog.router) with TestClient(app) as client: - - cmap = urlencode( - { + response = client.get( + "/preview", + params={ + "url": f"{DATA_DIR}/cog.tif", + "bidx": 1, "colormap": json.dumps( [ # ([min, max], [r, g, b, a]) @@ -1505,14 +1337,11 @@ def test_AutoFormat_Colormap(): ([2, 6000], [255, 0, 0, 255]), ([6001, 300000], [0, 255, 0, 255]), ] - ) - } + ), + }, ) - - response = client.get(f"/preview?url={DATA_DIR}/cog.tif&bidx=1&{cmap}") assert response.status_code == 200 assert response.headers["content-type"] == "image/png" - with MemoryFile(response.content) as mem: with mem.open() as dst: img = dst.read() @@ -1528,11 +1357,34 @@ def test_AutoFormat_Colormap(): def test_rescale_dependency(): """Ensure that we can set default rescale values via the rescale_dependency""" - def custom_rescale_params() -> Optional[RescaleType]: - return [(0, 100)] + @dataclass + class ImageRenderingParams(dependencies.ImageRenderingParams): + """Custom ImageParams.""" + + def __post_init__(self): + if self.rescale: + rescale_array = [] + for r in self.rescale: + parsed = tuple( + map( + float, + r.replace(" ", "") + .replace("[", "") + .replace("]", "") + .split(","), + ) + ) + assert ( + len(parsed) == 2 + ), f"Invalid rescale values: {self.rescale}, should be of form ['min,max', 'min,max'] or [[min,max], [min, max]]" + rescale_array.append(parsed) + + self.rescale = rescale_array # Noqa + else: + self.rescale = [(0, 100)] cog = TilerFactory() - cog_custom_range = TilerFactory(rescale_dependency=custom_rescale_params) + cog_custom_range = TilerFactory(render_dependency=ImageRenderingParams) app = FastAPI() app.include_router(cog.router, prefix="/cog") @@ -1540,7 +1392,7 @@ def custom_rescale_params() -> Optional[RescaleType]: with TestClient(app) as client: response = client.get( - f"/cog/tiles/8/87/48.npy?url={DATA_DIR}/cog.tif&rescale=0,1000" + f"/cog/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -1548,7 +1400,7 @@ def custom_rescale_params() -> Optional[RescaleType]: assert npy_tile.shape == (2, 256, 256) # mask + data response = client.get( - f"/cog_custom/tiles/8/87/48.npy?url={DATA_DIR}/cog.tif&rescale=0,1000" + f"/cog_custom/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -1557,7 +1409,7 @@ def custom_rescale_params() -> Optional[RescaleType]: def test_dst_crs_option(): - """test dst-crs parameter.""" + """test dst_crs parameter.""" app = FastAPI() app.include_router(TilerFactory().router) @@ -1571,14 +1423,14 @@ def test_dst_crs_option(): 32621 ) # return the image in the original CRS - response = client.get(f"/preview.tif?url={DATA_DIR}/cog.tif&dst-crs=epsg:4326") + response = client.get(f"/preview.tif?url={DATA_DIR}/cog.tif&dst_crs=epsg:4326") meta = parse_img(response.content) assert meta["crs"] == CRS.from_epsg(4326) assert not meta["crs"] == CRS.from_epsg(32621) - # /crop endpoints + # /bbox endpoints response = client.get( - f"/crop/-56.228,72.715,-54.547,73.188.tif?url={DATA_DIR}/cog.tif" + f"/bbox/-56.228,72.715,-54.547,73.188.tif?url={DATA_DIR}/cog.tif" ) meta = parse_img(response.content) assert meta["crs"] == CRS.from_epsg( @@ -1588,20 +1440,402 @@ def test_dst_crs_option(): # Force output in epsg:32621 response = client.get( - f"/crop/-56.228,72.715,-54.547,73.188.tif?url={DATA_DIR}/cog.tif&dst-crs=epsg:32621" + f"/bbox/-56.228,72.715,-54.547,73.188.tif?url={DATA_DIR}/cog.tif&dst_crs=epsg:32621" ) meta = parse_img(response.content) assert meta["crs"] == CRS.from_epsg(32621) - # coord-crs + dst-crs + # coord_crs + dst_crs response = client.get( - f"/crop/-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913.tif?url={DATA_DIR}/cog.tif&coord-crs=epsg:3857" + f"/bbox/-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913.tif?url={DATA_DIR}/cog.tif&coord_crs=epsg:3857" ) meta = parse_img(response.content) assert meta["crs"] == CRS.from_epsg(3857) response = client.get( - f"/crop/-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913.tif?url={DATA_DIR}/cog.tif&coord-crs=epsg:3857&dst-crs=epsg:32621" + f"/bbox/-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913.tif?url={DATA_DIR}/cog.tif&coord_crs=epsg:3857&dst_crs=epsg:32621" ) meta = parse_img(response.content) assert meta["crs"] == CRS.from_epsg(32621) + + +def test_color_formula_dependency(): + """Ensure that we can set default color formulae via the color_formula_dependency""" + + @dataclass + class ImageRenderingParams(dependencies.ImageRenderingParams): + """Custom ImageParams.""" + + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = "sigmoidal R 7 0.4" + + cog = TilerFactory() + cog_custom_color_formula = TilerFactory(render_dependency=ImageRenderingParams) + + app = FastAPI() + app.include_router(cog.router, prefix="/cog") + app.include_router(cog_custom_color_formula.router, prefix="/cog_custom") + + with TestClient(app) as client: + response = client.get( + f"/cog/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif&color_formula=sigmoidal R 10 0.1" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/x-binary" + npy_tile = numpy.load(BytesIO(response.content)) + assert npy_tile.shape == (2, 256, 256) # mask + data + + response = client.get( + f"/cog_custom/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/x-binary" + numpy.load(BytesIO(response.content)) + assert npy_tile.shape == (2, 256, 256) # mask + data + + +def test_colormap_factory(): + """Test ColorMapFactory endpoint.""" + # Register custom colormaps + cmaps = default_cmap.register( + { + "cust": {0: (0, 0, 0, 255), 1: (255, 0, 0, 255), 255: (255, 255, 0, 255)}, + "negative": { + -100: (0, 0, 0, 255), + 1: (255, 0, 0, 255), + 255: (255, 255, 0, 255), + }, + "seq": [ + ((1, 2), (255, 0, 0, 255)), + ((2, 3), (255, 240, 255, 255)), + ], + } + ) + + cmaps = ColorMapFactory(supported_colormaps=cmaps) + + app = FastAPI() + app.include_router(cmaps.router) + client = TestClient(app) + + response = client.get("/colorMaps") + assert response.status_code == 200 + cmap_ids = [cm["id"] for cm in response.json()["colormaps"]] + assert "cust" in cmap_ids + assert "negative" in cmap_ids + assert "seq" in cmap_ids + assert "viridis" in cmap_ids + + response = client.get("/colorMaps", headers={"Accept": "text/html"}) + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + + response = client.get("/colorMaps/viridis") + assert response.status_code == 200 + + response = client.get("/colorMaps/viridis", headers={"Accept": "text/html"}) + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + + response = client.get("/colorMaps/cust") + assert response.status_code == 200 + + response = client.get("/colorMaps/negative") + assert response.status_code == 200 + + response = client.get("/colorMaps/seq") + assert response.status_code == 200 + + response = client.get("/colorMaps/yo") + assert response.status_code == 422 + + response = client.get("/colorMaps/viridis", params={"f": "png"}) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 256 + assert meta["height"] == 20 + + response = client.get( + "/colorMaps/viridis", params={"f": "png", "orientation": "vertical"} + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 20 + assert meta["height"] == 256 + + response = client.get( + "/colorMaps/viridis", params={"f": "png", "width": 1000, "height": 100} + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 1000 + assert meta["height"] == 100 + + response = client.get("/colorMaps/cust", params={"f": "png"}) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 256 + assert meta["height"] == 20 + + response = client.get( + "/colorMaps/cust", params={"f": "png", "orientation": "vertical"} + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 20 + assert meta["height"] == 256 + + response = client.get("/colorMaps/negative", params={"f": "png"}) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 256 + assert meta["height"] == 20 + + response = client.get( + "/colorMaps/negative", params={"f": "png", "orientation": "vertical"} + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 20 + assert meta["height"] == 256 + + response = client.get("/colorMaps/seq", params={"f": "png"}) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 256 + assert meta["height"] == 20 + + response = client.get( + "/colorMaps/seq", params={"f": "png", "orientation": "vertical"} + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 20 + assert meta["height"] == 256 + + +def test_ogc_maps_cog(): + """Test TilerFactory class.""" + cog_path = f"{DATA_DIR}/cog.tif" + + cog = TilerFactory(add_ogc_maps=True) + assert len(cog.router.routes) == 20 + + assert "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/core" in cog.conforms_to + + app = FastAPI() + app.include_router(cog.router) + with TestClient(app) as client: + # Conformance Class “Core” + response = client.get( + "/map", + params={ + "url": cog_path, + }, + ) + assert response.status_code == 200 + headers = response.headers + assert ( + headers["Content-Bbox"] + == "373185.0,8019284.949381611,639014.9492102272,8286015.0" + ) + assert headers["Content-Crs"] == "" + assert headers["content-type"] == "image/png" + meta = parse_img(response.content) + assert meta["width"] == 1021 + assert meta["height"] == 1024 # default max size + + response = client.get( + "/map", + params={ + "url": cog_path, + }, + headers={"Accept": "image/jpeg"}, + ) + assert response.status_code == 200 + headers = response.headers + assert "Content-Bbox" in headers + assert "Content-Crs" in headers + assert headers["content-type"] == "image/jpeg" + meta = parse_img(response.content) + assert not meta["crs"] + + response = client.get("/map", params={"url": cog_path, "f": "tif"}) + assert response.status_code == 200 + headers = response.headers + assert "Content-Bbox" in headers + assert "Content-Crs" in headers + assert headers["content-type"] == "image/tiff; application=geotiff" + meta = parse_img(response.content) + assert meta["crs"] + + response = client.get("/map", params={"url": cog_path, "f": "tiff"}) + assert response.status_code == 200 + headers = response.headers + assert "Content-Bbox" in headers + assert "Content-Crs" in headers + assert headers["content-type"] == "image/tiff; application=geotiff" + meta = parse_img(response.content) + assert meta["crs"] + + # Conformance Class “Scaling” + # /req/scaling/width-definition + response = client.get( + "/map", + params={ + "url": cog_path, + "width": 256, + }, + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["width"] == 256 + assert meta["height"] == 257 + + response = client.get( + "/map", + params={ + "url": cog_path, + "width": -256, + }, + ) + assert response.status_code == 422 + + # /req/scaling/height-definition + response = client.get( + "/map", + params={ + "url": cog_path, + "height": 256, + }, + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["height"] == 256 + + response = client.get( + "/map", + params={ + "url": cog_path, + "height": -256, + }, + ) + assert response.status_code == 422 + + # Conformance Class “Spatial Subsetting” + # /conf/spatial-subsetting/bbox-crs + response = client.get( + "/map", + params={ + "url": cog_path, + "bbox": "-56.228,72.715,-54.547,73.188", + }, + ) + headers = response.headers + assert headers["Content-Crs"] == "" + bbox = list(map(float, headers["Content-Bbox"].split(","))) + assert all( + math.isclose(a, b, rel_tol=1e-5) + for a, b in zip( + bbox, + [ + 524922.2217886819, + 8068852.367048624, + 581330.6416587981, + 8123074.564952523, + ], + ) + ) + + assert headers["content-type"] == "image/png" + meta = parse_img(response.content) + + response = client.get( + "/map", + params={ + "url": cog_path, + "bbox": "-56.228,72.715,-54.547,73.188", + "bbox-crs": "http://www.opengis.net/def/crs/OGC/0/CRS84", + }, + ) + headers = response.headers + assert headers["Content-Crs"] == "" + bbox = list(map(float, headers["Content-Bbox"].split(","))) + assert all( + math.isclose(a, b, rel_tol=1e-5) + for a, b in zip( + bbox, + [ + 524922.2217886819, + 8068852.367048624, + 581330.6416587981, + 8123074.564952523, + ], + ) + ) + assert headers["content-type"] == "image/png" + + response = client.get( + "/map", + params={ + "url": cog_path, + "bbox": "-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913", + "bbox-crs": "[EPSG:3857]", + }, + ) + assert response.status_code == 200 + headers = response.headers + assert headers["Content-Crs"] == "" + bbox = list(map(float, headers["Content-Bbox"].split(","))) + assert all( + math.isclose(a, b, rel_tol=1e-5) + for a, b in zip( + bbox, + [ + 524922.2217886819, + 8068852.367048624, + 581330.6416587981, + 8123074.564952523, + ], + ) + ) + assert headers["content-type"] == "image/png" + + # Abstract Test for Requirement crs parameter definition + response = client.get( + "/map", + params={ + "url": cog_path, + "bbox": "-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913", + "bbox-crs": "[EPSG:3857]", + "crs": "[EPSG:4326]", + }, + ) + assert response.status_code == 200 + headers = response.headers + assert headers["Content-Crs"] == "" + assert headers["Content-Bbox"] == "-56.228,72.715,-54.54699999999999,73.188" + assert headers["content-type"] == "image/png" diff --git a/src/titiler/core/tests/test_logger_middleware.py b/src/titiler/core/tests/test_logger_middleware.py index 9c298f5c5..7987d6f0b 100644 --- a/src/titiler/core/tests/test_logger_middleware.py +++ b/src/titiler/core/tests/test_logger_middleware.py @@ -1,12 +1,17 @@ -"""Test titiler.core.middleware.TotalTimeMiddleware.""" +"""Test titiler.core.middleware.LoggerMiddleware.""" -from fastapi import FastAPI +import json +import logging +from logging import config + +import pytest +from fastapi import FastAPI, Path from starlette.testclient import TestClient from titiler.core.middleware import LoggerMiddleware -def test_timing_middleware_exclude(caplog): +def test_logger_middleware(caplog): """Create App.""" app = FastAPI() @@ -15,18 +20,103 @@ async def route1(): """route1.""" return "Yo" - app.add_middleware(LoggerMiddleware, querystrings=True, headers=True) + @app.get("/route2/{value}") + async def route2(value: str = Path()): + """route2.""" + return value + + @app.get("/route3/{value}") + async def route3(value: str = Path()): + """route3.""" + raise Exception("something went wrong") + + app.add_middleware(LoggerMiddleware) + + config.dictConfig( + { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "request": { + "format": ( + "%(asctime)s - %(levelname)s - %(name)s - %(message)s " + + json.dumps( + { + k: f"%({k})s" + for k in [ + "http.method", + "http.referer", + "http.origin", + "http.route", + "http.path", + "titiler.path_params", + "titiler.query_params", + "http.request.header.", + ] + } + ) + ), + }, + }, + "handlers": { + "console_request": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "request", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "titiler.requests": { + "level": "INFO", + "handlers": ["console_request"], + "propagate": True, + }, + }, + } + ) with TestClient(app) as client: - caplog.clear() - client.get("/route1") - assert len([rec.message for rec in caplog.records]) == 2 - - caplog.clear() - client.get("/route1", params={"hey": "yo"}) - assert len([rec.message for rec in caplog.records]) == 3 - - caplog.clear() - client.get("/route1", params={"hey": "yo"}, headers={"accept-encoding": "gzip"}) - h = caplog.records[2].message - assert "'accept-encoding': 'gzip'" in h + with caplog.at_level(logging.INFO, logger="titiler.requests"): + caplog.clear() + client.get("/route1") + log = caplog.records[0] + assert log.name == "titiler.requests" + assert log.levelname == "INFO" + assert log.message == "Request received: /route1 GET" + assert hasattr(log, "titiler.query_params") + assert getattr(log, "http.route") == "/route1" + + caplog.clear() + client.get("/route1", params={"hey": "yo"}) + log = caplog.records[0] + assert log.message == "Request received: /route1 GET" + assert getattr(log, "titiler.query_params") == {"hey": "yo"} + + caplog.clear() + client.get( + "/route1", params={"hey": "yo"}, headers={"accept-encoding": "gzip"} + ) + log = caplog.records[0] + assert log.message == "Request received: /route1 GET" + assert getattr(log, "titiler.query_params") == {"hey": "yo"} + assert getattr(log, "http.request.header.accept-encoding") == "gzip" + + caplog.clear() + client.get("/route2/val") + log = caplog.records[0] + assert log.name == "titiler.requests" + assert log.levelname == "INFO" + assert log.message == "Request received: /route2/val GET" + assert hasattr(log, "titiler.query_params") + assert getattr(log, "http.route") == "/route2/{value}" + + caplog.clear() + with pytest.raises(Exception): # noqa: B017 + client.get("/route3/val") + log = caplog.records[0] + assert log.name == "titiler.requests" + assert log.levelname == "INFO" + assert log.message == "Request received: /route3/val GET" + assert hasattr(log, "titiler.query_params") + assert log.route == "/route3/{value}" diff --git a/src/titiler/core/tests/test_models.py b/src/titiler/core/tests/test_models.py index 10c5511a5..42766ca59 100644 --- a/src/titiler/core/tests/test_models.py +++ b/src/titiler/core/tests/test_models.py @@ -9,8 +9,8 @@ def test_tilejson_model(): """Make sure TileJSON model validates input and return default.""" tj = TileJSON(tiles=["https://something.xyz/{x}/{y}/{z}"]) - assert tj.center == (0.0, 0.0, 0) - assert tj.bounds == [-180, -90, 180, 90] + assert list(map(round, tj.center)) == [0.0, 0.0, 0] + assert tj.bounds == [-180, -85.0511287798066, 180, 85.0511287798066] assert tj.minzoom == 0 assert tj.maxzoom == 30 assert tj.scheme == "xyz" @@ -19,8 +19,12 @@ def test_tilejson_model(): tiles=["https://something.xyz/{x}/{y}/{z}"], center=(10, 10, 4), scheme="tms" ) assert tj.center == (10.0, 10.0, 4) - assert tj.bounds == [-180, -90, 180, 90] + assert tj.bounds == [-180, -85.0511287798066, 180, 85.0511287798066] assert tj.scheme == "tms" with pytest.raises(ValidationError): TileJSON(tiles=["https://something.xyz/{x}/{y}/{z}"], scheme="abc") + + # Check extra fields are allowed + tj = TileJSON(tiles=["https://something.xyz/{x}/{y}/{z}"], dtype="uint8") + assert tj.dtype == "uint8" diff --git a/src/titiler/core/tests/test_rendering.py b/src/titiler/core/tests/test_rendering.py new file mode 100644 index 000000000..d5c56aa5f --- /dev/null +++ b/src/titiler/core/tests/test_rendering.py @@ -0,0 +1,160 @@ +"""test titiler rendering function.""" + +import warnings + +import numpy +import pytest +from rasterio.io import MemoryFile +from rio_tiler.errors import InvalidDatatypeWarning +from rio_tiler.models import ImageData + +from titiler.core.resources.enums import ImageType +from titiler.core.utils import render_image + + +def test_rendering(): + """test rendering.""" + im = ImageData(numpy.zeros((1, 256, 256), dtype="uint8")) + + # Should render as JPEG + content, media = render_image(im) + assert media == "image/jpeg" + with MemoryFile(content) as mem: + with mem.open() as dst: + assert dst.profile["driver"] == "JPEG" + assert dst.count == 1 + assert dst.width == 256 + assert dst.height == 256 + arr = dst.read() + assert numpy.unique(arr).tolist() == [0] + + # Should render as PNG + content, media = render_image(im, output_format=ImageType.png) + assert media == "image/png" + with MemoryFile(content) as mem: + with mem.open() as dst: + assert dst.profile["driver"] == "PNG" + assert dst.count == 2 + arr = dst.read() + assert numpy.unique(arr[0]).tolist() == [0] + + with pytest.warns(InvalidDatatypeWarning): + _, media = render_image( + ImageData(numpy.zeros((1, 256, 256), dtype="uint16")), + output_format=ImageType.jpeg, + ) + assert media == "image/jpeg" + + with pytest.warns(InvalidDatatypeWarning): + _, media = render_image( + ImageData(numpy.zeros((1, 256, 256), dtype="float32")), + output_format=ImageType.png, + ) + assert media == "image/png" + + with pytest.warns(InvalidDatatypeWarning): + _, media = render_image( + ImageData(numpy.zeros((1, 256, 256), dtype="float32")), + output_format=ImageType.jp2, + ) + assert media == "image/jp2" + + # Make sure that we do not rescale uint16 data when there is a colormap + # Because the colormap will result in data between 0 and 255 it should be of type uint8 + with warnings.catch_warnings(): + warnings.simplefilter("error") + cm = {1: (0, 0, 0, 255), 1000: (255, 255, 255, 255)} + d = numpy.zeros((1, 256, 256), dtype="float32") + 1 + d[0, 0:10, 0:10] = 1000 + content, media = render_image( + ImageData(d), + output_format=ImageType.jpeg, + colormap=cm, + ) + assert media == "image/jpeg" + + with MemoryFile(content) as mem: + with mem.open() as dst: + assert dst.count == 3 + assert dst.dtypes == ("uint8", "uint8", "uint8") + assert dst.read()[:, 0, 0].tolist() == [255, 255, 255] + assert dst.read()[:, 11, 11].tolist() == [0, 0, 0] + + # Partial alpha values + cm = { + 1: (0, 0, 0, 0), + 500: (100, 100, 100, 50), + 1000: (255, 255, 255, 255), + } + data = numpy.ma.zeros((1, 256, 256), dtype="float32") + 1 + data.mask = False + + data[0, 0, 0] = 0 + data.mask[0, 0, 0] = True + data[0, 1:, 1:] = 1 + data[0, 2:, 2:] = 500 + data[0, 3:, 3:] = 1000 + + content, media = render_image( + ImageData(data), + output_format=ImageType.png, + colormap=cm, + ) + assert media == "image/png" + + with MemoryFile(content) as mem: + with mem.open() as dst: + data_converted = dst.read() + assert dst.count == 4 + assert dst.dtypes == ("uint8", "uint8", "uint8", "uint8") + # Masked from Original Mask | set to UINT8 (0) + assert data_converted[:, 0, 0].tolist() == [0, 0, 0, 0] + # masked from CMAP + assert data_converted[:, 1, 1].tolist() == [0, 0, 0, 0] + # Partially masked from CMAP + assert data_converted[:, 2, 2].tolist() == [100, 100, 100, 50] + # Non-masked from CMAP + assert data_converted[:, 3, 3].tolist() == [255, 255, 255, 255] + + +def test_rendering_auto_dtype(): + """Test Automatic format selection and dtype""" + data = numpy.ma.zeros((1, 5, 5), dtype="uint16") + 1 + data.mask = False + # add a masked value + data.mask[0, 0, 0] = True + im = ImageData(data) + with pytest.warns(InvalidDatatypeWarning): + content, media = render_image(im) + assert media == "image/png" + + with MemoryFile(content) as mem: + with mem.open() as dst: + assert dst.count == 2 + assert dst.dtypes == ("uint8", "uint8") + + # Not Masked + data = numpy.ma.zeros((1, 5, 5), dtype="uint16") + 1 + data.mask = False + im = ImageData(data) + with pytest.warns(InvalidDatatypeWarning): + content, media = render_image(im) + assert media == "image/jpeg" + + with MemoryFile(content) as mem: + with mem.open() as dst: + assert dst.count == 1 + assert dst.dtypes == ("uint8",) + + # Full Masked + data = numpy.ma.zeros((1, 5, 5), dtype="uint16") + 1 + data.mask = True + im = ImageData(data) + with pytest.warns(InvalidDatatypeWarning): + content, media = render_image(im) + assert media == "image/png" + + with MemoryFile(content) as mem: + with mem.open() as dst: + assert dst.count == 2 + assert dst.dtypes == ("uint8", "uint8") diff --git a/src/titiler/core/tests/test_routing.py b/src/titiler/core/tests/test_routing.py index 32c5789e4..20eb133ec 100644 --- a/src/titiler/core/tests/test_routing.py +++ b/src/titiler/core/tests/test_routing.py @@ -1,131 +1,10 @@ -"""Test Custom APIRoute factory.""" - -from concurrent import futures +"""Test route dependencies.""" import httpx -import pytest -import rasterio -from fastapi import APIRouter, Depends, FastAPI, HTTPException, security, status -from rasterio._env import get_gdal_config +from fastapi import Depends, FastAPI, HTTPException, security, status from starlette.testclient import TestClient -from titiler.core.routing import add_route_dependencies, apiroute_factory - - -@pytest.mark.xfail -def test_withoutCustomRoute(monkeypatch): - """Create App.""" - monkeypatch.setenv("GDAL_DISABLE_READDIR_ON_OPEN", "something") - - app = FastAPI() - router = APIRouter() - - def f(r): - return get_gdal_config("GDAL_DISABLE_READDIR_ON_OPEN") - - @router.get("/simple") - def home(): - """Works and should return FALSE.""" - with rasterio.Env(GDAL_DISABLE_READDIR_ON_OPEN="FALSE"): - res = get_gdal_config("GDAL_DISABLE_READDIR_ON_OPEN") - return {"env": res} - - @router.get("/asimple") - async def home1(): - """Works and should return FALSE.""" - with rasterio.Env(GDAL_DISABLE_READDIR_ON_OPEN="FALSE"): - res = get_gdal_config("GDAL_DISABLE_READDIR_ON_OPEN") - return {"env": res} - - @router.get("/future") - def home2(): - """Doesn't work and should return the value from env.""" - with rasterio.Env(GDAL_DISABLE_READDIR_ON_OPEN="FALSE"): - with futures.ThreadPoolExecutor() as executor: - res = list(executor.map(f, range(1)))[0] - return {"env": res} - - @router.get("/afuture") - async def home3(): - """Works and should return FALSE.""" - with rasterio.Env(GDAL_DISABLE_READDIR_ON_OPEN="FALSE"): - with futures.ThreadPoolExecutor() as executor: - res = list(executor.map(f, range(1)))[0] - return {"env": res} - - app.include_router(router) - client = TestClient(app) - - response = client.get("/simple") - assert response.json()["env"] == "FALSE" - - response = client.get("/asimple") - assert response.json()["env"] == "FALSE" - - # confirm the multi threads case doesn't work - response = client.get("/future") - assert not response.json()["env"] == "FALSE" - - response = client.get("/afuture") - assert response.json()["env"] == "FALSE" - - -@pytest.mark.xfail -def test_withCustomRoute(monkeypatch): - """Create App.""" - monkeypatch.setenv("GDAL_DISABLE_READDIR_ON_OPEN", "something") - - app = FastAPI() - - env = {"GDAL_DISABLE_READDIR_ON_OPEN": "FALSE"} - with pytest.warns(DeprecationWarning): - route_class = apiroute_factory(env) - router = APIRouter(route_class=route_class) - - def f(r): - return get_gdal_config("GDAL_DISABLE_READDIR_ON_OPEN") - - @router.get("/simple") - def home(): - """Works and should return FALSE.""" - res = get_gdal_config("GDAL_DISABLE_READDIR_ON_OPEN") - return {"env": res} - - @router.get("/asimple") - async def home1(): - """Works and should return FALSE.""" - res = get_gdal_config("GDAL_DISABLE_READDIR_ON_OPEN") - return {"env": res} - - @router.get("/future") - def home2(): - """Doesn't work and should return the value from env.""" - with futures.ThreadPoolExecutor() as executor: - res = list(executor.map(f, range(1)))[0] - return {"env": res} - - @router.get("/afuture") - async def home3(): - """Works and should return FALSE.""" - with futures.ThreadPoolExecutor() as executor: - res = list(executor.map(f, range(1)))[0] - return {"env": res} - - app.include_router(router) - client = TestClient(app) - - response = client.get("/simple") - assert response.json()["env"] == "FALSE" - - response = client.get("/asimple") - assert response.json()["env"] == "FALSE" - - # confirm the Custom APIRoute class fix - response = client.get("/future") - assert response.json()["env"] == "FALSE" - - response = client.get("/afuture") - assert response.json()["env"] == "FALSE" +from titiler.core.routing import add_route_dependencies def test_register_deps(): diff --git a/src/titiler/core/tests/test_telemetry.py b/src/titiler/core/tests/test_telemetry.py new file mode 100644 index 000000000..8b3574a59 --- /dev/null +++ b/src/titiler/core/tests/test_telemetry.py @@ -0,0 +1,96 @@ +"""telemetry tests""" + +import os + +import pytest +from fastapi import FastAPI +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.trace import StatusCode +from starlette.testclient import TestClient + +from titiler.core import telemetry +from titiler.core.factory import TilerFactory + +TEST_URL = f"file://{os.path.join(os.path.dirname(__file__), 'fixtures', 'cog.tif')}" +TEST_Z, TEST_X, TEST_Y = 8, 84, 47 + + +@pytest.fixture +def telemetry_disabled(monkeypatch): + """Fixture to simulate OTel being disabled by monkeypatching the tracer.""" + monkeypatch.setattr("titiler.core.telemetry.tracer", None) + monkeypatch.setattr("titiler.core.telemetry.factory_trace.decorator_enabled", False) + + +@pytest.fixture +def memory_exporter(): + """Fixture to configure an in-memory exporter for capturing spans.""" + tracer_provider = TracerProvider() + exporter = InMemorySpanExporter() + processor = SimpleSpanProcessor(exporter) + tracer_provider.add_span_processor(processor) + + original_provider = trace.get_tracer_provider() + trace.set_tracer_provider(tracer_provider) + + yield exporter + + exporter.shutdown() + trace.set_tracer_provider(original_provider) + + +def test_tracing_disabled_noop(telemetry_disabled): + """Test that the application works correctly when OTel is not installed.""" + assert not telemetry.tracer + assert not telemetry.factory_trace.decorator_enabled + + app = FastAPI() + tiler = TilerFactory(router_prefix="cog") + app.include_router(tiler.router, prefix="/cog") + client = TestClient(app) + + response = client.get( + f"/cog/tiles/WebMercatorQuad/{TEST_Z}/{TEST_X}/{TEST_Y}.png", + params={ + "url": TEST_URL, + }, + ) + assert response.status_code == 200 + + +def test_tracing_enabled_but_not_available_warning(telemetry_disabled): + """Test that enabling telemetry without the decorator enabled emits a warning.""" + assert not telemetry.tracer + assert not telemetry.factory_trace.decorator_enabled + + with pytest.warns(match="tracing is not available"): + _ = TilerFactory(router_prefix="cog", enable_telemetry=True) + + +def test_tracing_enabled_success_path(memory_exporter): + """Test that spans are correctly created on a successful request.""" + assert telemetry.tracer + assert telemetry.factory_trace.decorator_enabled + + app = FastAPI() + tiler = TilerFactory(router_prefix="cog", enable_telemetry=True) + app.include_router(tiler.router, prefix="/cog") + client = TestClient(app) + + response = client.get( + f"/cog/tiles/WebMercatorQuad/{TEST_Z}/{TEST_X}/{TEST_Y}.png", + params={ + "url": TEST_URL, + }, + ) + assert response.status_code == 200 + + finished_spans = memory_exporter.get_finished_spans() + assert len(finished_spans) == 1 + + span = next(filter(lambda x: x.name == "TilerFactory.tile", finished_spans), None) + assert span + assert span.status.status_code == StatusCode.OK diff --git a/src/titiler/core/tests/test_utils.py b/src/titiler/core/tests/test_utils.py new file mode 100644 index 000000000..34710cbff --- /dev/null +++ b/src/titiler/core/tests/test_utils.py @@ -0,0 +1,180 @@ +"""Test utils.""" + +import pytest + +from titiler.core.dependencies import AssetsExprParams, BidxParams +from titiler.core.resources.enums import MediaType +from titiler.core.utils import ( + accept_media_type, + check_query_params, + deserialize_query_params, + extract_query_params, + get_dependency_query_params, +) + + +def test_get_dependency_params(): + """Test dependency filtering from query params.""" + + # invalid + values, err = get_dependency_query_params( + dependency=BidxParams, params={"bidx": ["invalid type"]} + ) + assert values == {} + assert err + assert err == [ + { + "input": "invalid type", + "loc": ( + "query", + "bidx", + 0, + ), + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + }, + ] + + # not in dep + values, err = get_dependency_query_params( + dependency=BidxParams, params={"not_in_dep": "no error, no value"} + ) + assert values == {"indexes": None} + assert not err + + # valid + values, err = get_dependency_query_params( + dependency=BidxParams, params={"bidx": [1, 2, 3]} + ) + assert values == {"indexes": [1, 2, 3]} + assert not err + + # valid and not in dep + values, err = get_dependency_query_params( + dependency=BidxParams, + params={"bidx": [1, 2, 3], "other param": "to be filtered out"}, + ) + assert values == {"indexes": [1, 2, 3]} + assert not err + + +def test_deserialize_query_params(): + """Test deserialize_query_params.""" + # invalid + res, err = deserialize_query_params( + dependency=BidxParams, params={"bidx": ["invalid type"]} + ) + assert res == BidxParams(indexes=None) + assert err + + # valid + res, err = deserialize_query_params( + dependency=BidxParams, params={"not_in_dep": "no error, no value", "bidx": [1]} + ) + assert res == BidxParams(indexes=[1]) + assert not err + + +def test_extract_query_params(): + """Test extract_query_params.""" + # invalid + qs, err = extract_query_params( + dependencies=[BidxParams], + params={"bidx": ["invalid type"]}, + ) + assert qs == {} + assert len(err) + + qs, err = extract_query_params( + dependencies=[BidxParams], + params={"bidx": [1]}, + ) + assert qs == {"indexes": [1]} + assert len(err) == 0 + + qs, err = extract_query_params( + dependencies=[BidxParams], + params={"bidx": 1}, + ) + assert qs == {"indexes": [1]} + assert len(err) == 0 + + qs, err = extract_query_params( + dependencies=[BidxParams], + params={"not_in_dep": "no error, no value", "bidx": [1]}, + ) + assert qs == {"indexes": [1]} + assert len(err) == 0 + + +def test_check_query_params(): + """Test check_query_params.""" + # invalid bidx value + assert ( + check_query_params( + dependencies=[BidxParams], + params={"bidx": ["invalid type"]}, + ) + is False + ) + + # assets is required + assert ( + check_query_params( + dependencies=[AssetsExprParams], + params={}, + ) + is False + ) + + assert ( + check_query_params( + dependencies=[AssetsExprParams, BidxParams], + params={"assets": "yo"}, + ) + is True + ) + + +@pytest.mark.parametrize( + "media,accept,expected", + [ + ([MediaType.html], "text/html, application/json;q=0.8", MediaType.html), + ( + [MediaType.html, MediaType.json], + "application/json, text/html;q=0.8", + MediaType.json, + ), + ([MediaType.xml], "application/json, text/html;q=0.8", None), + ([MediaType.json], "", None), + ( + [MediaType.json, MediaType.html], + "application/json;q=1.0, text/html;q=0.8", + MediaType.json, + ), + ( + [MediaType.json, MediaType.html], + "application/json;q=1.0, text/html;q=1.0", + MediaType.json, + ), + ( + [MediaType.html, MediaType.json], + "application/json;q=1.0, text/html;q=1.0", + MediaType.html, + ), + ([MediaType.html, MediaType.json], "*;q=1.0", MediaType.html), + ( + [MediaType.json, MediaType.html], + "application/json;q=aaa, text/html", + MediaType.html, + ), + ( + [MediaType.json, MediaType.html], + "application/json;q=0.0, text/html", + MediaType.html, + ), + ], +) +def test_accept_media_type(media, accept, expected): + """test MetadataOutputType dependency.""" + assert accept_media_type(accept, media) == expected diff --git a/src/titiler/core/titiler/core/__init__.py b/src/titiler/core/titiler/core/__init__.py index f26d8963f..9efd2c27f 100644 --- a/src/titiler/core/titiler/core/__init__.py +++ b/src/titiler/core/titiler/core/__init__.py @@ -1,11 +1,13 @@ """titiler.core""" -__version__ = "0.11.7" +__version__ = "2.0.0b2" from . import dependencies, errors, factory, routing # noqa from .factory import ( # noqa - BaseTilerFactory, - MultiBandTilerFactory, + AlgorithmFactory, + BaseFactory, + ColorMapFactory, MultiBaseTilerFactory, TilerFactory, + TMSFactory, ) diff --git a/src/titiler/core/titiler/core/algorithm/__init__.py b/src/titiler/core/titiler/core/algorithm/__init__.py index b77327384..26fd153cf 100644 --- a/src/titiler/core/titiler/core/algorithm/__init__.py +++ b/src/titiler/core/titiler/core/algorithm/__init__.py @@ -2,22 +2,43 @@ import json from copy import copy -from typing import Dict, List, Literal, Optional, Type +from typing import Annotated, Literal import attr from fastapi import HTTPException, Query -from pydantic import ValidationError - -from titiler.core.algorithm.base import AlgorithmMetadata, BaseAlgorithm # noqa -from titiler.core.algorithm.dem import Contours, HillShade, TerrainRGB, Terrarium +from pydantic import BeforeValidator, ValidationError + +from titiler.core.algorithm.base import ( # noqa + AlgorithmMetadata, + AlgorithmtList, + BaseAlgorithm, +) +from titiler.core.algorithm.dem import Contours, HillShade, Slope, TerrainRGB, Terrarium +from titiler.core.algorithm.image import ToBitonal, ToGrayScale from titiler.core.algorithm.index import NormalizedIndex +from titiler.core.algorithm.math import _Max, _Mean, _Median, _Min, _Std, _Sum, _Var +from titiler.core.algorithm.ops import CastToInt, Ceil, Floor +from titiler.core.validation import validate_json -default_algorithms: Dict[str, Type[BaseAlgorithm]] = { +default_algorithms: dict[str, type[BaseAlgorithm]] = { "hillshade": HillShade, + "slope": Slope, "contours": Contours, "normalizedIndex": NormalizedIndex, "terrarium": Terrarium, "terrainrgb": TerrainRGB, + "cast": CastToInt, + "ceil": Ceil, + "floor": Floor, + "min": _Min, + "max": _Max, + "median": _Median, + "mean": _Mean, + "std": _Std, + "var": _Var, + "sum": _Sum, + "grayscale": ToGrayScale, + "bitonal": ToBitonal, } @@ -25,22 +46,22 @@ class Algorithms: """Algorithms.""" - data: Dict[str, Type[BaseAlgorithm]] = attr.ib() + data: dict[str, type[BaseAlgorithm]] = attr.ib(factory=dict) - def get(self, name: str) -> BaseAlgorithm: + def get(self, name: str) -> type[BaseAlgorithm]: """Fetch a TMS.""" if name not in self.data: raise KeyError(f"Invalid name: {name}") return self.data[name] - def list(self) -> List[str]: + def list(self) -> list[str]: """List registered Algorithm.""" return list(self.data.keys()) def register( self, - algorithms: Dict[str, BaseAlgorithm], + algorithms: dict[str, BaseAlgorithm], overwrite: bool = False, ) -> "Algorithms": """Register Algorithm(s).""" @@ -48,18 +69,23 @@ def register( if name in self.data and not overwrite: raise Exception(f"{name} is already a registered. Use overwrite=True.") - return Algorithms({**self.data, **algorithms}) + return Algorithms({**self.data, **algorithms}) # type: ignore [dict-item] @property def dependency(self): """FastAPI PostProcess dependency.""" def post_process( - algorithm: Literal[tuple(self.data.keys())] = Query( - None, description="Algorithm name" - ), - algorithm_params: str = Query(None, description="Algorithm parameter"), - ) -> Optional[BaseAlgorithm]: + algorithm: Annotated[ + Literal[tuple(self.data.keys())], + Query(description="Algorithm name"), + ] = None, + algorithm_params: Annotated[ + str | None, + BeforeValidator(validate_json), + Query(description="Algorithm parameter"), + ] = None, + ) -> BaseAlgorithm | None: """Data Post-Processing options.""" kwargs = json.loads(algorithm_params) if algorithm_params else {} if algorithm: diff --git a/src/titiler/core/titiler/core/algorithm/base.py b/src/titiler/core/titiler/core/algorithm/base.py index 5eb6bdf3b..79924a7dc 100644 --- a/src/titiler/core/titiler/core/algorithm/base.py +++ b/src/titiler/core/titiler/core/algorithm/base.py @@ -1,11 +1,13 @@ """Algorithm base class.""" import abc -from typing import Dict, Optional, Sequence +from collections.abc import Sequence from pydantic import BaseModel from rio_tiler.models import ImageData +from titiler.core.models.common import Link + class BaseAlgorithm(BaseModel, metaclass=abc.ABCMeta): """Algorithm baseclass. @@ -15,26 +17,40 @@ class BaseAlgorithm(BaseModel, metaclass=abc.ABCMeta): """ # metadata - input_nbands: Optional[int] - output_nbands: Optional[int] - output_dtype: Optional[str] - output_min: Optional[Sequence] - output_max: Optional[Sequence] + input_nbands: int | None = None + output_nbands: int | None = None + output_dtype: str | None = None + output_min: Sequence | None = None + output_max: Sequence | None = None + + model_config = {"extra": "allow"} @abc.abstractmethod def __call__(self, img: ImageData) -> ImageData: """Apply algorithm""" ... - class Config: - """Config for model.""" - - extra = "allow" - class AlgorithmMetadata(BaseModel): """Algorithm metadata.""" - inputs: Dict - outputs: Dict - parameters: Dict + title: str | None = None + description: str | None = None + + inputs: dict + outputs: dict + parameters: dict + + +class AlgorithmRef(BaseModel): + """AlgorithmRef model.""" + + id: str + title: str | None = None + links: list[Link] + + +class AlgorithmtList(BaseModel): + """AlgorithmList model.""" + + algorithms: list[AlgorithmRef] diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index 858d538d2..d71a36405 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -1,6 +1,7 @@ """titiler.core.algorithm DEM.""" import numpy +from pydantic import Field from rasterio import windows from rio_tiler.colormap import apply_cmap, cmap from rio_tiler.models import ImageData @@ -8,14 +9,20 @@ from titiler.core.algorithm.base import BaseAlgorithm +__all__ = ["HillShade", "Slope", "Contours", "Terrarium", "TerrainRGB"] + class HillShade(BaseAlgorithm): """Hillshade.""" + title: str = "Hillshade" + description: str = "Create hillshade from DEM dataset." + # parameters - azimuth: int = 90 - angle_altitude: float = 90 - buffer: int = 3 + azimuth: int = Field(45, ge=0, le=360) + angle_altitude: float = Field(45.0, ge=-90.0, le=90.0) + buffer: int = Field(3, ge=0, le=99) + z_exaggeration: float = Field(1.0, ge=1e-6, le=1e6) # metadata input_nbands: int = 1 @@ -24,41 +31,90 @@ class HillShade(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Create hillshade from DEM dataset.""" - data = img.data[0] - mask = img.mask - bounds = img.bounds - - x, y = numpy.gradient(data) - + x, y = numpy.gradient(img.array[0]) + x *= self.z_exaggeration + y *= self.z_exaggeration slope = numpy.pi / 2.0 - numpy.arctan(numpy.sqrt(x * x + y * y)) aspect = numpy.arctan2(-x, y) - azimuthrad = self.azimuth * numpy.pi / 180.0 - altituderad = self.angle_altitude * numpy.pi / 180.0 + azimuth = 360.0 - self.azimuth + azimuthrad = numpy.deg2rad(azimuth) + altituderad = numpy.deg2rad(self.angle_altitude) shaded = numpy.sin(altituderad) * numpy.sin(slope) + numpy.cos( altituderad ) * numpy.cos(slope) * numpy.cos(azimuthrad - aspect) - hillshade_array = 255 * (shaded + 1) / 2 + data = 255 * (shaded + 1) / 2 + data[data < 0] = 0 # set hillshade values to min of 0. + + bounds = img.bounds + if self.buffer: + data = data[self.buffer : -self.buffer, self.buffer : -self.buffer] + + window = windows.Window( + col_off=self.buffer, + row_off=self.buffer, + width=data.shape[1], + height=data.shape[0], + ) + bounds = windows.bounds(window, img.transform) + + return ImageData( + data.astype(self.output_dtype), + assets=img.assets, + crs=img.crs, + bounds=bounds, + band_descriptions=["hillshade"], + ) + + +class Slope(BaseAlgorithm): + """Slope calculation.""" - data = numpy.expand_dims(hillshade_array, axis=0).astype(dtype=numpy.uint8) + title: str = "Slope" + description: str = "Calculate degrees of slope from DEM dataset." + # parameters + buffer: int = Field(3, ge=0, le=99, description="Buffer size for edge effects") + z_exaggeration: float = Field(1.0, ge=1e-6, le=1e6) + + # metadata + input_nbands: int = 1 + output_nbands: int = 1 + output_dtype: str = "float32" + output_min: list[float] = [0.0] + output_max: list[float] = [90.0] + + def __call__(self, img: ImageData) -> ImageData: + """Calculate degrees slope from DEM dataset.""" + # Get the pixel size from the transform + pixel_size_x = abs(img.transform[0]) + pixel_size_y = abs(img.transform[4]) + + x, y = numpy.gradient(img.array[0]) + x *= self.z_exaggeration + y *= self.z_exaggeration + dx = x / pixel_size_x + dy = y / pixel_size_y + + slope = numpy.rad2deg(numpy.arctan(numpy.sqrt(dx * dx + dy * dy))) + + bounds = img.bounds if self.buffer: - data = data[:, self.buffer : -self.buffer, self.buffer : -self.buffer] - mask = mask[self.buffer : -self.buffer, self.buffer : -self.buffer] - # image bounds without buffer + slope = slope[self.buffer : -self.buffer, self.buffer : -self.buffer] + window = windows.Window( col_off=self.buffer, row_off=self.buffer, - width=mask.shape[1], - height=mask.shape[0], + width=slope.shape[1], + height=slope.shape[0], ) bounds = windows.bounds(window, img.transform) return ImageData( - data, - mask, + slope.astype(self.output_dtype), assets=img.assets, crs=img.crs, bounds=bounds, + band_descriptions=["slope"], ) @@ -68,11 +124,14 @@ class Contours(BaseAlgorithm): Original idea from https://custom-scripts.sentinel-hub.com/dem/contour-lines/ """ + title: str = "Contours" + description: str = "Create contours from DEM dataset." + # parameters - increment: int = 35 - thickness: int = 1 - minz: int = -12000 - maxz: int = 8000 + increment: int = Field(35, ge=0, le=999) + thickness: int = Field(1, ge=0, le=10) + minz: int = Field(-12000, ge=-99999, le=99999) + maxz: int = Field(8000, ge=-99999, le=99999) # metadata input_nbands: int = 1 @@ -81,27 +140,36 @@ class Contours(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Add contours.""" - data = img.data + data = img.data.astype("float64") # Apply rescaling for minz,maxz to 1->255 and apply Terrain colormap - arr = linear_rescale(data, (self.minz, self.maxz), (1, 255)).astype("uint8") + arr = linear_rescale(data, (self.minz, self.maxz), (1, 255)).astype( + self.output_dtype + ) arr, _ = apply_cmap(arr, cmap.get("terrain")) # set black (0) for contour lines arr = numpy.where(data % self.increment < self.thickness, 0, arr) + data = numpy.ma.MaskedArray(arr) + data.mask = ~img.mask + return ImageData( - arr, - img.mask, + data, assets=img.assets, crs=img.crs, bounds=img.bounds, + band_descriptions=["contours_r", "contours_g", "contours_b"], ) class Terrarium(BaseAlgorithm): """Encode DEM into RGB (Mapzen Terrarium).""" + title: str = "Terrarium" + description: str = "Encode DEM into RGB (Mapzen Terrarium)." + nodata_height: float | None = Field(None, ge=-99999.0, le=99999.0) + # metadata input_nbands: int = 1 output_nbands: int = 3 @@ -109,27 +177,34 @@ class Terrarium(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Encode DEM into RGB.""" - data = numpy.clip(img.data[0] + 32768.0, 0.0, 65535.0) + data = numpy.clip(img.array[0] + 32768.0, 0.0, 65535.0) + if self.nodata_height is not None: + data[img.array.mask[0]] = numpy.clip( # type: ignore [index] + self.nodata_height + 32768.0, 0.0, 65535.0 + ) r = data / 256 g = data % 256 b = (data * 256) % 256 - arr = numpy.stack([r, g, b]).astype(numpy.uint8) return ImageData( - arr, - img.mask, + numpy.ma.stack([r, g, b]).astype(self.output_dtype), assets=img.assets, crs=img.crs, bounds=img.bounds, + band_descriptions=["terranium_r", "terranium_g", "terranium_b"], ) class TerrainRGB(BaseAlgorithm): """Encode DEM into RGB (Mapbox Terrain RGB).""" + title: str = "TerrainRGB" + description: str = "Encode DEM into RGB (Mapbox Terrain RGB)." + # parameters - interval: float = 0.1 - baseval: float = -10000.0 + interval: float = Field(0.1, ge=0.0, le=1.0) + baseval: float = Field(-10000.0, ge=-99999.0, le=99999.0) + nodata_height: float | None = Field(None, ge=-99999.0, le=99999.0) # metadata input_nbands: int = 1 @@ -153,28 +228,30 @@ def _range_check(datarange): round_digits = 0 - data = img.data[0].astype(numpy.float64) + data = img.array[0].astype(numpy.float64) data -= self.baseval data /= self.interval data = numpy.around(data / 2**round_digits) * 2**round_digits - rows, cols = data.shape datarange = data.max() - data.min() if _range_check(datarange): - raise ValueError("Data of {} larger than 256 ** 3".format(datarange)) + raise ValueError(f"Data of {datarange} larger than 256 ** 3") + + if self.nodata_height is not None: + data[img.array.mask[0]] = ( # type: ignore [index] + self.nodata_height - self.baseval + ) / self.interval - rgb = numpy.zeros((3, rows, cols), dtype=numpy.uint8) - rgb[2] = ((data / 256) - (data // 256)) * 256 - rgb[1] = (((data // 256) / 256) - ((data // 256) // 256)) * 256 - rgb[0] = ( - (((data // 256) // 256) / 256) - (((data // 256) // 256) // 256) - ) * 256 + data_int32 = data.astype(numpy.int32) + b = (data_int32) & 0xFF + g = (data_int32 >> 8) & 0xFF + r = (data_int32 >> 16) & 0xFF return ImageData( - rgb, - img.mask, + numpy.ma.stack([r, g, b]).astype(self.output_dtype), assets=img.assets, crs=img.crs, bounds=img.bounds, + band_descriptions=["terrain_r", "terrain_g", "terrain_b"], ) diff --git a/src/titiler/core/titiler/core/algorithm/image.py b/src/titiler/core/titiler/core/algorithm/image.py new file mode 100644 index 000000000..a0e061ccf --- /dev/null +++ b/src/titiler/core/titiler/core/algorithm/image.py @@ -0,0 +1,73 @@ +"""titiler.core.algorithm Images""" + +import numpy +from rio_tiler.models import ImageData + +from titiler.core.algorithm.base import BaseAlgorithm + + +class ToGrayScale(BaseAlgorithm): + """Transform a RGB Image to Grayscale.""" + + title: str = "Transform a RGB Image to Grayscale" + description: str = "Transform a RGB Image to Grayscale using the ITU-R 601-2 luma." + + # metadata + output_nbands: int = 1 + + def __call__(self, img: ImageData) -> ImageData: + """RGB to L.""" + if img.count < 3: + raise ValueError( + f"Cannot apply `grayscale` algorithm on image with {img.count} bands." + ) + + arr = ( + img.array[0] * 299 / 1000 + + img.array[1] * 587 / 1000 + + img.array[2] * 114 / 1000 + ) + return ImageData( + arr.astype(img.array.dtype), + assets=img.assets, + crs=img.crs, + band_descriptions=["grayscale"], + bounds=img.bounds, + cutline_mask=img.cutline_mask, + ) + + +class ToBitonal(BaseAlgorithm): + """Transform an Image to Bitonal.""" + + title: str = "Transform an Image to Bitonal" + description: str = "All values larger than 127 are set to 255 (white), all other values to 0 (black)." + + # metadata + output_nbands: int = 1 + output_dtype: str = "uint8" + + def __call__(self, img: ImageData) -> ImageData: + """Image to Bitonal""" + if img.count == 3: + # Convert to Grayscale + arr = ( + img.array[0] * 299 / 1000 + + img.array[1] * 587 / 1000 + + img.array[2] * 114 / 1000 + ) + elif img.count == 1: + arr = img.array + else: + raise ValueError( + f"Cannot apply `bitonal` algorithm on image with {img.count} bands." + ) + + return ImageData( + numpy.ma.where(arr > 127, 255, 0).astype("uint8"), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_descriptions=["bitonal"], + cutline_mask=img.cutline_mask, + ) diff --git a/src/titiler/core/titiler/core/algorithm/index.py b/src/titiler/core/titiler/core/algorithm/index.py index 1e28e0f45..7ab85a425 100644 --- a/src/titiler/core/titiler/core/algorithm/index.py +++ b/src/titiler/core/titiler/core/algorithm/index.py @@ -1,16 +1,21 @@ """titiler.core.algorithm Normalized Index.""" -from typing import Sequence +from collections.abc import Sequence import numpy from rio_tiler.models import ImageData from titiler.core.algorithm.base import BaseAlgorithm +__all__ = ["NormalizedIndex"] + class NormalizedIndex(BaseAlgorithm): """Normalized Difference Index.""" + title: str = "Normalized Difference Index" + description: str = "Compute normalized difference index from two bands." + # metadata input_nbands: int = 2 output_nbands: int = 1 @@ -20,18 +25,16 @@ class NormalizedIndex(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Normalized difference.""" - b1 = img.data[0].astype("float32") - b2 = img.data[1].astype("float32") - - arr = numpy.where(img.mask, (b2 - b1) / (b2 + b1), 0) - - # ImageData only accept image in form of (count, height, width) - arr = numpy.expand_dims(arr, axis=0).astype(self.output_dtype) - + b1 = img.array[0].astype("float32") + b2 = img.array[1].astype("float32") + arr = numpy.ma.MaskedArray((b2 - b1) / (b2 + b1), dtype=self.output_dtype) + bnames = img.band_descriptions return ImageData( arr, - img.mask, assets=img.assets, crs=img.crs, bounds=img.bounds, + band_descriptions=[ + f"({bnames[1]} - {bnames[0]}) / ({bnames[1]} + {bnames[0]})" + ], ) diff --git a/src/titiler/core/titiler/core/algorithm/math.py b/src/titiler/core/titiler/core/algorithm/math.py new file mode 100644 index 000000000..3c4b4353c --- /dev/null +++ b/src/titiler/core/titiler/core/algorithm/math.py @@ -0,0 +1,159 @@ +"""titiler.core.algorithm Math.""" + +import numpy +from rio_tiler.models import ImageData + +from titiler.core.algorithm.base import BaseAlgorithm + +__all__ = ["_Min", "_Max", "_Median", "_Mean", "_Std", "_Var", "_Sum"] + + +class _Min(BaseAlgorithm): + """Return Min values along the `bands` axis.""" + + title: str = "Min" + description: str = "Return Min values along the `bands` axis." + + output_nbands: int = 1 + + def __call__(self, img: ImageData) -> ImageData: + """Return Min.""" + return ImageData( + numpy.ma.min(img.array, axis=0, keepdims=True), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_descriptions=["min"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class _Max(BaseAlgorithm): + """Return Max values along the `bands` axis.""" + + title: str = "Max" + description: str = "Return Max values along the `bands` axis." + + output_nbands: int = 1 + + def __call__(self, img: ImageData) -> ImageData: + """Return Max.""" + return ImageData( + numpy.ma.max(img.array, axis=0, keepdims=True), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_descriptions=["max"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class _Median(BaseAlgorithm): + """Return Median values along the `bands` axis.""" + + title: str = "Median" + description: str = "Return Median values along the `bands` axis." + + output_nbands: int = 1 + output_dtype: str = "float64" + + def __call__(self, img: ImageData) -> ImageData: + """Return Median.""" + return ImageData( + numpy.ma.median(img.array, axis=0, keepdims=True), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_descriptions=["median"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class _Mean(BaseAlgorithm): + """Return Mean values along the `bands` axis.""" + + title: str = "Mean" + description: str = "Return Mean values." + + output_nbands: int = 1 + output_dtype: str = "float64" + + def __call__(self, img: ImageData) -> ImageData: + """Return Mean.""" + return ImageData( + numpy.ma.mean(img.array, axis=0, keepdims=True), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_descriptions=["mean"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class _Std(BaseAlgorithm): + """Return the standard deviation along the `bands` axis.""" + + title: str = "Standard deviation" + description: str = "Return the Standard Deviation along the `bands` axis." + + output_nbands: int = 1 + output_dtype: str = "float64" + + def __call__(self, img: ImageData) -> ImageData: + """Return Stddev.""" + return ImageData( + numpy.ma.std(img.array, axis=0, keepdims=True, ddof=1), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_descriptions=["std"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class _Var(BaseAlgorithm): + """Return Variance values along the `bands` axis.""" + + title: str = "Variance" + description: str = "Return Variance along the `bands` axis." + + output_nbands: int = 1 + output_dtype: str = "float64" + + def __call__(self, img: ImageData) -> ImageData: + """Return Variance.""" + return ImageData( + numpy.ma.var(img.array, axis=0, keepdims=True, ddof=1), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_descriptions=["var"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class _Sum(BaseAlgorithm): + """Return Sum values along the `bands` axis.""" + + title: str = "Sum" + description: str = "Return Sum values along the `bands` axis." + + output_nbands: int = 1 + + def __call__(self, img: ImageData) -> ImageData: + """Return Min.""" + return ImageData( + numpy.ma.sum(img.array, axis=0, keepdims=True), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_descriptions=["sum"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) diff --git a/src/titiler/core/titiler/core/algorithm/ops.py b/src/titiler/core/titiler/core/algorithm/ops.py new file mode 100644 index 000000000..a6ace6493 --- /dev/null +++ b/src/titiler/core/titiler/core/algorithm/ops.py @@ -0,0 +1,82 @@ +"""titiler.core.algorithm Ops.""" + +from collections.abc import Sequence + +import numpy +from rio_tiler.models import ImageData + +from titiler.core.algorithm.base import BaseAlgorithm + +__all__ = ["CastToInt", "Ceil", "Floor"] + + +class CastToInt(BaseAlgorithm): + """Cast data to Integer.""" + + title: str = "Cast data to Integer" + description: str = "Cast data to Integer." + + # metadata + output_dtype: str = "uint8" + output_min: Sequence[int] = [0] + output_max: Sequence[int] = [255] + + def __call__(self, img: ImageData) -> ImageData: + """Cast Data.""" + return ImageData( + img.array.astype("uint8"), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_descriptions=img.band_descriptions, + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class Ceil(BaseAlgorithm): + """Round data to the smallest integer.""" + + title: str = "Round data to the smallest integer" + description: str = "Round data to the smallest integer." + + # metadata + output_dtype: str = "uint8" + output_min: Sequence[int] = [0] + output_max: Sequence[int] = [255] + + def __call__(self, img: ImageData) -> ImageData: + """Cast Data.""" + return ImageData( + numpy.ceil(img.array).astype("uint8"), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_descriptions=img.band_descriptions, + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class Floor(BaseAlgorithm): + """Round data to the largest integer.""" + + title: str = "Round data to the largest integer" + description: str = "Round data to the largest integer." + + # metadata + output_dtype: str = "uint8" + output_min: Sequence[int] = [0] + output_max: Sequence[int] = [255] + + def __call__(self, img: ImageData) -> ImageData: + """Cast Data.""" + return ImageData( + numpy.floor(img.array).astype("uint8"), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_descriptions=img.band_descriptions, + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) diff --git a/src/titiler/core/titiler/core/dependencies.py b/src/titiler/core/titiler/core/dependencies.py index efc19260c..eec4f2723 100644 --- a/src/titiler/core/titiler/core/dependencies.py +++ b/src/titiler/core/titiler/core/dependencies.py @@ -1,55 +1,74 @@ """Common dependency.""" import json -from dataclasses import dataclass -from enum import Enum -from typing import Dict, List, Optional, Sequence, Tuple, Union +from collections.abc import Callable, Sequence +from dataclasses import dataclass, field +from typing import Annotated, Any, Literal, cast import numpy from fastapi import HTTPException, Query +from pydantic import AfterValidator, BeforeValidator, Field from rasterio.crs import CRS -from rasterio.enums import Resampling -from rio_tiler.colormap import cmap, parse_color -from rio_tiler.errors import MissingAssets, MissingBands -from rio_tiler.types import ColorMapType - -ColorMapName = Enum( # type: ignore - "ColorMapName", [(a, a) for a in sorted(cmap.list())] -) -ResamplingName = Enum( # type: ignore - "ResamplingName", [(r.name, r.name) for r in Resampling] +from rio_tiler.colormap import ColorMaps +from rio_tiler.colormap import cmap as default_cmap +from rio_tiler.colormap import parse_color +from rio_tiler.types import AssetType, AssetWithOptions, RIOResampling, WarpResampling +from starlette.requests import Request + +from titiler.core.resources.enums import ImageType, MediaType +from titiler.core.utils import accept_media_type +from titiler.core.validation import ( + separated_parseable_floats_regex, + validate_bbox, + validate_color_formula, + validate_crs, + validate_rescale, ) -def ColorMapParams( - colormap_name: ColorMapName = Query(None, description="Colormap name"), - colormap: str = Query(None, description="JSON encoded custom Colormap"), -) -> Optional[ColorMapType]: - """Colormap Dependency.""" - if colormap_name: - return cmap.get(colormap_name.value) +def create_colormap_dependency(cmap: ColorMaps) -> Callable: + """Create Colormap Dependency.""" - if colormap: - try: - c = json.loads( - colormap, - object_hook=lambda x: {int(k): parse_color(v) for k, v in x.items()}, - ) + def deps( + colormap_name: Annotated[ # type: ignore + Literal[tuple(cmap.list())], + Query(description="Colormap name"), + ] = None, + colormap: Annotated[ + str | None, Query(description="JSON encoded custom Colormap") + ] = None, + ): + if colormap_name: + return cmap.get(colormap_name) - # Make sure to match colormap type - if isinstance(c, Sequence): - c = [(tuple(inter), parse_color(v)) for (inter, v) in c] + if colormap: + try: + c = json.loads( + colormap, + object_hook=lambda x: { + int(k): parse_color(v) for k, v in x.items() + }, + ) - return c - except json.JSONDecodeError as e: - raise HTTPException( - status_code=400, detail="Could not parse the colormap value." - ) from e + # Make sure to match colormap type + if isinstance(c, Sequence): + c = [(tuple(inter), parse_color(v)) for (inter, v) in c] - return None + return c + except json.JSONDecodeError as e: + raise HTTPException( + status_code=400, detail="Could not parse the colormap value." + ) from e + + return None + + return deps + + +ColorMapParams = create_colormap_dependency(default_cmap) -def DatasetPathParams(url: str = Query(..., description="Dataset URL")) -> str: +def DatasetPathParams(url: Annotated[str, Query(description="Dataset URL")]) -> str: """Create dataset path from args""" return url @@ -58,13 +77,12 @@ def DatasetPathParams(url: str = Query(..., description="Dataset URL")) -> str: class DefaultDependency: """Dataclass with dict unpacking""" - def keys(self): - """Return Keys.""" - return self.__dict__.keys() + def as_dict(self, exclude_none: bool = True) -> dict: + """Transform dataclass to dict.""" + if exclude_none: + return {k: v for k, v in self.__dict__.items() if v is not None} - def __getitem__(self, key): - """Return value.""" - return self.__dict__[key] + return dict(self.__dict__.items()) # Dependencies for simple BaseReader (e.g COGReader) @@ -72,31 +90,40 @@ def __getitem__(self, key): class BidxParams(DefaultDependency): """Band Indexes parameters.""" - indexes: Optional[List[int]] = Query( - None, - title="Band indexes", - alias="bidx", - description="Dataset band indexes", - examples={"one-band": {"value": [1]}, "multi-bands": {"value": [1, 2, 3]}}, - ) + indexes: Annotated[ + list[int] | None, + Query( + title="Band indexes", + alias="bidx", + description="Dataset band indexes", + openapi_examples={ + "user-provided": {"value": None}, + "one-band": {"value": [1]}, + "multi-bands": {"value": [1, 2, 3]}, + }, + ), + ] = None @dataclass class ExpressionParams(DefaultDependency): """Expression parameters.""" - expression: Optional[str] = Query( - None, - title="Band Math expression", - description="rio-tiler's band math expression", - examples={ - "simple": {"description": "Simple band math.", "value": "b1/b2"}, - "multi-bands": { - "description": "Semicolon (;) delimited expressions (band1: b1/b2, band2: b2+b3).", - "value": "b1/b2;b2+b3", + expression: Annotated[ + str | None, + Query( + title="Band Math expression", + description="rio-tiler's band math expression", + openapi_examples={ + "user-provided": {"value": None}, + "simple": {"description": "Simple band math.", "value": "b1/b2"}, + "multi-bands": { + "description": "Semicolon (;) delimited expressions (band1: b1/b2, band2: b2+b3).", + "value": "b1/b2;b2+b3", + }, }, - }, - ) + ), + ] = None @dataclass @@ -107,213 +134,107 @@ class BidxExprParams(ExpressionParams, BidxParams): # Dependencies for MultiBaseReader (e.g STACReader) -@dataclass -class AssetsParams(DefaultDependency): - """Assets parameters.""" +def _parse_asset(values: list[str]) -> list[AssetType]: + """Parse assets with optional parameter.""" + assets: list[AssetType] = [] + for v in values: + if "|" in v: + asset_name, params = v.split("|", 1) + opts: dict[str, Any] = {"name": asset_name} + for option in params.split("|"): + key, value = option.split("=", 1) + if key == "bidx": + opts["indexes"] = list(map(int, value.split(","))) + elif key == "expression": + opts["expression"] = value + elif key == "bands": + opts["bands"] = value.split(",") + + asset = cast(AssetWithOptions, opts) + assets.append(asset) + else: + assets.append(v) - assets: List[str] = Query( - None, - title="Asset names", - description="Asset's names.", - examples={ - "one-asset": { - "description": "Return results for asset `data`.", - "value": ["data"], - }, - "multi-assets": { - "description": "Return results for assets `data` and `cog`.", - "value": ["data", "cog"], - }, - }, - ) + return assets @dataclass -class AssetsBidxExprParams(DefaultDependency): - """Assets, Expression and Asset's band Indexes parameters.""" - - assets: Optional[List[str]] = Query( - None, - title="Asset names", - description="Asset's names.", - examples={ - "one-asset": { - "description": "Return results for asset `data`.", - "value": ["data"], - }, - "multi-assets": { - "description": "Return results for assets `data` and `cog`.", - "value": ["data", "cog"], - }, - }, - ) - expression: Optional[str] = Query( - None, - title="Band Math expression", - description="Band math expression between assets", - examples={ - "simple": { - "description": "Return results of expression between assets.", - "value": "asset1_b1 + asset2_b1 / asset3_b1", - }, - }, - ) +class AssetsParams(DefaultDependency): + """Assets parameters.""" - asset_indexes: Optional[Sequence[str]] = Query( - None, - title="Per asset band indexes", - description="Per asset band indexes (coma separated indexes)", - alias="asset_bidx", - examples={ - "one-asset": { - "description": "Return indexes 1,2,3 of asset `data`.", - "value": ["data|1,2,3"], - }, - "multi-assets": { - "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", - "value": ["data|1,2,3", "cog|1"], + assets: Annotated[ + list[str], + AfterValidator(_parse_asset), + Query( + title="Asset names", + description="Asset's names.", + openapi_examples={ + "user-provided": {"value": None}, + "one-asset": { + "description": "Return results for asset `data`.", + "value": ["data"], + }, + "multi-assets": { + "description": "Return results for assets `data` and `cog`.", + "value": ["data", "cog"], + }, + "multi-assets-with-options": { + "description": "Return results for assets `data` and `cog`.", + "value": ["data|bidx=1", "cog|bidx=1,2"], + }, }, - }, - ) - - asset_as_band: Optional[bool] = Query( - None, - title="Consider asset as a 1 band dataset", - description="Asset as Band", - ) - - def __post_init__(self): - """Post Init.""" - if not self.assets and not self.expression: - raise MissingAssets( - "assets must be defined either via expression or assets options." - ) - - if self.asset_indexes: - self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore - idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) - for idx in self.asset_indexes - } + ), + ] @dataclass -class AssetsBidxExprParamsOptional(AssetsBidxExprParams): - """Assets, Expression and Asset's band Indexes parameters but with no requirement.""" +class AssetsExprParams(ExpressionParams, AssetsParams): + """Assets and Expression parameters.""" - def __post_init__(self): - """Post Init.""" - if self.asset_indexes: - self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore - idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) - for idx in self.asset_indexes - } + asset_as_band: Annotated[ + bool | None, + Query( + title="Consider asset as a 1 band dataset", + description="Asset as Band", + ), + ] = None @dataclass -class AssetsBidxParams(AssetsParams): - """Assets, Asset's band Indexes and Asset's band Expression parameters.""" - - asset_indexes: Optional[Sequence[str]] = Query( - None, - title="Per asset band indexes", - description="Per asset band indexes", - alias="asset_bidx", - examples={ - "one-asset": { - "description": "Return indexes 1,2,3 of asset `data`.", - "value": ["data|1;2;3"], - }, - "multi-assets": { - "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", - "value": ["data|1;2;3", "cog|1"], - }, - }, - ) +class PreviewParams(DefaultDependency): + """Common Preview parameters.""" - asset_expression: Optional[Sequence[str]] = Query( - None, - title="Per asset band expression", - description="Per asset band expression", - examples={ - "one-asset": { - "description": "Return results for expression `b1*b2+b3` of asset `data`.", - "value": ["data|b1*b2+b3"], - }, - "multi-assets": { - "description": "Return results for expressions `b1*b2+b3` for asset `data` and `b1+b3` for asset `cog`.", - "value": ["data|b1*b2+b3", "cog|b1+b3"], - }, - }, + # NOTE: sizes dependency can either be a Query or a Path Parameter + max_size: Annotated[int, Field(description="Maximum image size to read onto.")] = ( + 1024 ) - - def __post_init__(self): - """Post Init.""" - if self.asset_indexes: - self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore - idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) - for idx in self.asset_indexes - } - - if self.asset_expression: - self.asset_expression: Dict[str, str] = { # type: ignore - idx.split("|")[0]: idx.split("|")[1] for idx in self.asset_expression - } - - -# Dependencies for MultiBandReader -@dataclass -class BandsParams(DefaultDependency): - """Band names parameters.""" - - bands: List[str] = Query( - None, - title="Band names", - description="Band's names.", - examples={ - "one-band": { - "description": "Return results for band `B01`.", - "value": ["B01"], - }, - "multi-bands": { - "description": "Return results for bands `B01` and `B02`.", - "value": ["B01", "B02"], - }, - }, + height: Annotated[int | None, Field(description="Force output image height.")] = ( + None ) - - -@dataclass -class BandsExprParamsOptional(ExpressionParams, BandsParams): - """Optional Band names and Expression parameters.""" - - pass - - -@dataclass -class BandsExprParams(ExpressionParams, BandsParams): - """Band names and Expression parameters (Band or Expression required).""" + width: Annotated[int | None, Field(description="Force output image width.")] = None def __post_init__(self): """Post Init.""" - if not self.bands and not self.expression: - raise MissingBands( - "bands must be defined either via expression or bands options." - ) + if self.width or self.height: + self.max_size = None @dataclass -class ImageParams(DefaultDependency): - """Common Preview/Crop parameters.""" - - max_size: Optional[int] = Query( - 1024, description="Maximum image size to read onto." +class PartFeatureParams(DefaultDependency): + """Common parameters for bbox and feature.""" + + # NOTE: the part sizes dependency can either be a Query or a Path Parameter + max_size: Annotated[ + int | None, Field(description="Maximum image size to read onto.") + ] = None + height: Annotated[int | None, Field(description="Force output image height.")] = ( + None ) - height: Optional[int] = Query(None, description="Force output image height.") - width: Optional[int] = Query(None, description="Force output image width.") + width: Annotated[int | None, Field(description="Force output image width.")] = None def __post_init__(self): """Post Init.""" - if self.width and self.height: + if self.width or self.height: self.max_size = None @@ -321,109 +242,174 @@ def __post_init__(self): class DatasetParams(DefaultDependency): """Low level WarpedVRT Optional parameters.""" - nodata: Optional[Union[str, int, float]] = Query( - None, title="Nodata value", description="Overwrite internal Nodata value" - ) - unscale: Optional[bool] = Query( - False, - title="Apply internal Scale/Offset", - description="Apply internal Scale/Offset", - ) - resampling_method: ResamplingName = Query( - ResamplingName.nearest, # type: ignore - alias="resampling", - description="Resampling method.", - ) + nodata: Annotated[ + Literal["nan", "inf", "-inf"] | int | float | None, + Query( + title="Nodata value", + description="Overwrite internal Nodata value; nan or valid float values only.", + ), + ] = None + unscale: Annotated[ + bool | None, + Query( + title="Apply internal Scale/Offset", + description="Apply internal Scale/Offset. Defaults to `False`.", + ), + ] = None + resampling_method: Annotated[ + RIOResampling | None, + Query( + alias="resampling", + description="RasterIO resampling algorithm. Defaults to `nearest`.", + ), + ] = None + reproject_method: Annotated[ + WarpResampling | None, + Query( + alias="reproject", + description="WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`.", + ), + ] = None def __post_init__(self): """Post Init.""" if self.nodata is not None: self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata) - self.resampling_method = self.resampling_method.value # type: ignore + if self.unscale is not None: + self.unscale = bool(self.unscale) -@dataclass -class ImageRenderingParams(DefaultDependency): - """Image Rendering options.""" - add_mask: bool = Query( - True, alias="return_mask", description="Add mask to the output data." - ) +RescaleType = list[tuple[float, float]] -RescaleType = List[Tuple[float, ...]] +@dataclass +class RenderingParams(DefaultDependency): + """Image Rendering options.""" + rescale: Annotated[ + list[str] | None, + BeforeValidator(validate_rescale), + Query( + title="Min/Max data Rescaling", + description="comma (',') delimited Min,Max range. Can set multiple time for multiple bands.", + examples=["0,2000", "0,1000", "0,10000"], # band 1 # band 2 # band 3 + ), + ] = None + + color_formula: Annotated[ + str | None, + BeforeValidator(validate_color_formula), + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None + + def __post_init__(self) -> None: + """Post Init.""" + if self.rescale: + rescale_array = [] + for r in self.rescale: + parsed = tuple( + map( + float, + r.split(","), + ) + ) + assert ( + len(parsed) == 2 + ), f"Invalid rescale values: {self.rescale}, should be of form ['min,max', 'min,max'] or [[min,max], [min, max]]" + rescale_array.append(parsed) + + self.rescale: RescaleType = rescale_array # type: ignore -def RescalingParams( - rescale: Optional[List[str]] = Query( - None, - title="Min/Max data Rescaling", - description="comma (',') delimited Min,Max range. Can set multiple time for multiple bands.", - example=["0,2000", "0,1000", "0,10000"], # band 1 # band 2 # band 3 - ) -) -> Optional[RescaleType]: - """Min/Max data Rescaling""" - if rescale: - return [tuple(map(float, r.replace(" ", "").split(","))) for r in rescale] - return None +@dataclass +class ImageRenderingParams(RenderingParams): + """Image Rendering options.""" + + add_mask: Annotated[ + bool | None, + Query( + alias="return_mask", + description="Add mask to the output data. Defaults to `True`", + ), + ] = None @dataclass class StatisticsParams(DefaultDependency): """Statistics options.""" - categorical: bool = Query( - False, description="Return statistics for categorical dataset." - ) - categories: List[Union[float, int]] = Query( - None, - alias="c", - title="Pixels values for categories.", - description="List of values for which to report counts.", - example=[1, 2, 3], - ) - percentiles: List[int] = Query( - [2, 98], - alias="p", - title="Percentile values", - description="List of percentile values.", - example=[2, 5, 95, 98], - ) + categorical: Annotated[ + bool | None, + Query( + description="Return statistics for categorical dataset. Defaults to `False`" + ), + ] = None + categories: Annotated[ + list[float | int] | None, + Query( + alias="c", + title="Pixels values for categories.", + description="List of values for which to report counts.", + examples=[1, 2, 3], + ), + ] = None + percentiles: Annotated[ + list[int] | None, + Query( + alias="p", + title="Percentile values", + description="List of percentile values (default to [2, 98]).", + examples=[2, 5, 95, 98], + ), + ] = None + + def __post_init__(self): + """Set percentiles default.""" + if not self.percentiles: + self.percentiles = [2, 98] @dataclass class HistogramParams(DefaultDependency): """Numpy Histogram options.""" - bins: Optional[str] = Query( - None, - alias="histogram_bins", - title="Histogram bins.", - description=""" + bins: Annotated[ + str | None, + Query( + alias="histogram_bins", + title="Histogram bins.", + description=""" Defines the number of equal-width bins in the given range (10, by default). If bins is a sequence (comma `,` delimited values), it defines a monotonically increasing array of bin edges, including the rightmost edge, allowing for non-uniform bin widths. link: https://numpy.org/doc/stable/reference/generated/numpy.histogram.html - """, - examples={ - "simple": { - "description": "Defines the number of equal-width bins", - "value": 8, + """, + openapi_examples={ + "user-provided": {"value": None}, + "simple": { + "description": "Defines the number of equal-width bins", + "value": 8, + }, + "array": { + "description": "Defines custom bin edges (comma `,` delimited values)", + "value": "0,100,200,300", + }, }, - "array": { - "description": "Defines custom bin edges (comma `,` delimited values)", - "value": "0,100,200,300", - }, - }, - ) - - range: Optional[str] = Query( - None, - alias="histogram_range", - title="Histogram range", - description=""" + pattern=r"^\d+(,\d+)*$", + ), + ] = None + + range: Annotated[ + str | None, + Query( + alias="histogram_range", + title="Histogram range", + description=""" Comma `,` delimited range of the bins. The lower and upper range of the bins. If not provided, range is simply (a.min(), a.max()). @@ -432,9 +418,17 @@ class HistogramParams(DefaultDependency): range affects the automatic bin computation as well. link: https://numpy.org/doc/stable/reference/generated/numpy.histogram.html - """, - example="0,1000", - ) + """, + openapi_examples={ + "user-provided": {"value": None}, + "array": { + "description": "Defines custom histogram range (comma `,` delimited values)", + "value": "0,1000", + }, + }, + pattern=separated_parseable_floats_regex(count=2), + ), + ] = None def __post_init__(self): """Post Init.""" @@ -448,16 +442,24 @@ def __post_init__(self): self.bins = 10 if self.range: - self.range = list(map(float, self.range.split(","))) # type: ignore + parsed = list(map(float, self.range.split(","))) + assert ( + len(parsed) == 2 + ), f"Invalid histogram_range values: {self.range}, should be of form 'min,max'" + + self.range = parsed # type: ignore def CoordCRSParams( - crs: str = Query( - None, - alias="coord-crs", - description="Coordinate Reference System of the input coords. Default to `epsg:4326`.", - ) -) -> Optional[CRS]: + crs: Annotated[ + str | None, + BeforeValidator(validate_crs), + Query( + alias="coord_crs", + description="Coordinate Reference System of the input coords. Default to `epsg:4326`.", + ), + ] = None, +) -> CRS | None: """Coordinate Reference System Coordinates Param.""" if crs: return CRS.from_user_input(crs) @@ -466,14 +468,155 @@ def CoordCRSParams( def DstCRSParams( - crs: str = Query( - None, - alias="dst-crs", - description="Output Coordinate Reference System.", - ) -) -> Optional[CRS]: + crs: Annotated[ + str | None, + BeforeValidator(validate_crs), + Query( + alias="dst_crs", + description="Output Coordinate Reference System.", + ), + ] = None, +) -> CRS | None: """Coordinate Reference System Coordinates Param.""" if crs: return CRS.from_user_input(crs) return None + + +def CRSParams( + crs: Annotated[ + str | None, + BeforeValidator(validate_crs), + Query( + description="Coordinate Reference System.", + ), + ] = None, +) -> CRS | None: + """Coordinate Reference System Coordinates Param.""" + if crs: + return CRS.from_user_input(crs) + + return None + + +def BufferParams( + buffer: Annotated[ + float | None, + Query( + gt=0, + title="Tile buffer.", + description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", + ), + ] = None, +) -> float | None: + """Tile buffer Parameter.""" + return buffer + + +@dataclass +class TileParams(DefaultDependency): + """Tile options.""" + + buffer: Annotated[ + float | None, + Query( + gt=0, + title="Tile buffer.", + description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", + ), + ] = None + + padding: Annotated[ + int | None, + Query( + gt=0, + title="Tile padding.", + description="Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`.", + ), + ] = None + + +@dataclass +class OGCMapsParams(DefaultDependency): + """OGC Maps options.""" + + request: Request + + bbox: Annotated[ + str | None, + BeforeValidator(validate_bbox), + Query( + description="Bounding box of the rendered map. The bounding box is provided as four or six coordinates.", + ), + ] = None + + crs: Annotated[ + str | None, + BeforeValidator(validate_crs), + Query( + description="Reproject the output to the given crs.", + ), + ] = None + + bbox_crs: Annotated[ + str | None, + BeforeValidator(validate_crs), + Query( + description="crs for the specified bbox.", + alias="bbox-crs", + ), + ] = None + + height: Annotated[ + int | None, + Query( + description="Height of the map in pixels. If omitted and `width` is specified, defaults to the `height` maintaining a 1:1 aspect ratio. If both `width` and `height` are omitted, the server will select default dimensions.", + gt=0, + ), + ] = None + + width: Annotated[ + int | None, + Query( + description="Width of the map in pixels. If omitted and `height` is specified, defaults to the `width` maintaining a 1:1 aspect ratio. If both `width` and `height` are omitted, the server will select default dimensions.", + gt=0, + ), + ] = None + + f: Annotated[ + ImageType | None, + Query(description="The format of the map response (e.g. png)."), + ] = None + + max_size: int | None = field(init=False, default=None) + + format: ImageType | None = field(init=False, default=ImageType.png) + + def __post_init__(self): # noqa: C901 + """Parse and validate.""" + if self.crs: + self.crs = CRS.from_user_input(self.crs) # type: ignore + + if self.bbox_crs: + self.bbox_crs = CRS.from_user_input(self.bbox_crs) # type: ignore + + if not self.height and not self.width: + self.max_size = 1024 + + if self.bbox: + bounds = list(map(float, self.bbox.split(","))) + if len(bounds) == 6: + bounds = [bounds[0], bounds[1], bounds[3], bounds[4]] + + self.bbox = bounds # type: ignore + + if self.f: + self.format = ImageType[self.f] + + else: + if media := accept_media_type( + self.request.headers.get("accept", ""), + [MediaType[e] for e in ImageType], + ): + self.format = ImageType[media.name] diff --git a/src/titiler/core/titiler/core/errors.py b/src/titiler/core/titiler/core/errors.py index 141aadcc7..cd9ee4a71 100644 --- a/src/titiler/core/titiler/core/errors.py +++ b/src/titiler/core/titiler/core/errors.py @@ -1,6 +1,6 @@ """Titiler error classes.""" -from typing import Callable, Dict, Type +from collections.abc import Callable from fastapi import FastAPI from rasterio.errors import RasterioError, RasterioIOError @@ -8,6 +8,7 @@ InvalidAssetName, InvalidBandName, InvalidColorFormat, + InvalidExpression, MissingAssets, MissingBands, RioTilerError, @@ -15,7 +16,7 @@ ) from starlette import status from starlette.requests import Request -from starlette.responses import JSONResponse +from starlette.responses import JSONResponse, Response class TilerError(Exception): @@ -43,6 +44,7 @@ class BadRequestError(TilerError): RasterioError: status.HTTP_500_INTERNAL_SERVER_ERROR, RioTilerError: status.HTTP_500_INTERNAL_SERVER_ERROR, Exception: status.HTTP_500_INTERNAL_SERVER_ERROR, + InvalidExpression: status.HTTP_400_BAD_REQUEST, } @@ -52,16 +54,19 @@ def exception_handler_factory(status_code: int) -> Callable: """ def handler(request: Request, exc: Exception): + if status_code == status.HTTP_204_NO_CONTENT: + return Response(content=None, status_code=204) + return JSONResponse(content={"detail": str(exc)}, status_code=status_code) return handler def add_exception_handlers( - app: FastAPI, status_codes: Dict[Type[Exception], int] + app: FastAPI, status_codes: dict[type[Exception], int] ) -> None: """ Add exception handlers to the FastAPI app. """ - for (exc, code) in status_codes.items(): + for exc, code in status_codes.items(): app.add_exception_handler(exc, exception_handler_factory(code)) diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index 52e16a3d6..7ba655889 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -1,58 +1,70 @@ """TiTiler Router factories.""" import abc -from dataclasses import dataclass, field -from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Type, Union +import base64 +import logging +import os +import warnings +from collections.abc import Callable, Sequence +from typing import Annotated, Any, Literal from urllib.parse import urlencode import jinja2 +import numpy import rasterio +from attrs import define, field from fastapi import APIRouter, Body, Depends, Path, Query from fastapi.dependencies.utils import get_parameterless_sub_dependant from fastapi.params import Depends as DependsFunc from geojson_pydantic.features import Feature, FeatureCollection -from geojson_pydantic.geometries import Polygon from morecantile import TileMatrixSet from morecantile import tms as morecantile_tms from morecantile.defaults import TileMatrixSets -from rasterio.crs import CRS +from pydantic import Field +from rio_tiler.colormap import ColorMaps +from rio_tiler.colormap import cmap as default_cmap from rio_tiler.constants import WGS84_CRS -from rio_tiler.io import BaseReader, MultiBandReader, MultiBaseReader, Reader -from rio_tiler.models import BandStatistics, Bounds, Info +from rio_tiler.io import BaseReader, MultiBaseReader, Reader +from rio_tiler.models import ImageData, Info from rio_tiler.types import ColorMapType -from rio_tiler.utils import get_array_statistics +from rio_tiler.utils import CRS_to_uri from starlette.requests import Request from starlette.responses import HTMLResponse, Response -from starlette.routing import Match, compile_path, replace_params +from starlette.routing import Match, NoMatchFound +from starlette.routing import Route as APIRoute +from starlette.routing import compile_path, replace_params from starlette.templating import Jinja2Templates -from titiler.core.algorithm import AlgorithmMetadata, Algorithms, BaseAlgorithm +from titiler.core.algorithm import ( + AlgorithmMetadata, + Algorithms, + AlgorithmtList, + BaseAlgorithm, +) from titiler.core.algorithm import algorithms as available_algorithms from titiler.core.dependencies import ( - AssetsBidxExprParams, - AssetsBidxExprParamsOptional, - AssetsBidxParams, + AssetsExprParams, AssetsParams, - BandsExprParams, - BandsExprParamsOptional, - BandsParams, BidxExprParams, ColorMapParams, CoordCRSParams, + CRSParams, DatasetParams, DatasetPathParams, DefaultDependency, DstCRSParams, HistogramParams, - ImageParams, ImageRenderingParams, - RescaleType, - RescalingParams, + OGCMapsParams, + PartFeatureParams, + PreviewParams, StatisticsParams, + TileParams, ) from titiler.core.models.mapbox import TileJSON -from titiler.core.models.OGC import TileMatrixSetList +from titiler.core.models.OGC import TileMatrixSetList, TileSet, TileSetList from titiler.core.models.responses import ( + ColorMapList, InfoGeoJSON, MultiBaseInfo, MultiBaseInfoGeoJSON, @@ -62,17 +74,25 @@ Statistics, StatisticsGeoJSON, ) -from titiler.core.resources.enums import ImageType, MediaType, OptionalHeader -from titiler.core.resources.responses import GeoJSONResponse, JSONResponse, XMLResponse +from titiler.core.resources.enums import ImageType, MediaType +from titiler.core.resources.responses import GeoJSONResponse, JSONResponse from titiler.core.routing import EndpointScope +from titiler.core.telemetry import factory_trace +from titiler.core.utils import ( + accept_media_type, + bounds_to_geometry, + create_html_response, + render_image, + tms_limits, +) -DEFAULT_TEMPLATES = Jinja2Templates( - directory="", +jinja2_env = jinja2.Environment( + autoescape=jinja2.select_autoescape(["html"]), loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), -) # type:ignore - +) +DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) -img_endpoint_params: Dict[str, Any] = { +img_endpoint_params: dict[str, Any] = { "responses": { 200: { "content": { @@ -90,95 +110,62 @@ "response_class": Response, } +logger = logging.getLogger(__name__) -@dataclass # type: ignore + +@define class FactoryExtension(metaclass=abc.ABCMeta): """Factory Extension.""" @abc.abstractmethod - def register(self, factory: "BaseTilerFactory"): + def register(self, factory: "BaseFactory"): """Register extension to the factory.""" ... -# ref: https://github.com/python/mypy/issues/5374 -@dataclass # type: ignore -class BaseTilerFactory(metaclass=abc.ABCMeta): - """BaseTiler Factory. +@define(kw_only=True) +class BaseFactory(metaclass=abc.ABCMeta): + """Base Factory. Abstract Base Class which defines most inputs used by dynamic tiler. Attributes: - reader (rio_tiler.io.base.BaseReader): A rio-tiler reader (e.g Reader). router (fastapi.APIRouter): Application router to register endpoints to. - path_dependency (Callable): Endpoint dependency defining `path` to pass to the reader init. - dataset_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset overwriting options (e.g nodata). - layer_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset indexes/bands/assets options. - render_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image rendering options (e.g add_mask). - colormap_dependency (Callable): Endpoint dependency defining ColorMap options (e.g colormap_name). - process_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image post-processing options (e.g rescaling, color-formula). - tms_dependency (Callable): Endpoint dependency defining TileMatrixSet to use. - reader_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining BaseReader options. - environment_dependency (Callable): Endpoint dependency to define GDAL environment at runtime. router_prefix (str): prefix where the router will be mounted in the application. - optional_headers(sequence of titiler.core.resources.enums.OptionalHeader): additional headers to return with the response. + route_dependencies (list): Additional routes dependencies to add after routes creations. """ - reader: Type[BaseReader] - # FastAPI router - router: APIRouter = field(default_factory=APIRouter) - - # Path Dependency - path_dependency: Callable[..., Any] = DatasetPathParams - - # Rasterio Dataset Options (nodata, unscale, resampling) - dataset_dependency: Type[DefaultDependency] = DatasetParams - - # Indexes/Expression Dependencies - layer_dependency: Type[DefaultDependency] = BidxExprParams - - # Image rendering Dependencies - render_dependency: Type[DefaultDependency] = ImageRenderingParams - colormap_dependency: Callable[..., Optional[ColorMapType]] = ColorMapParams - - rescale_dependency: Callable[..., Optional[RescaleType]] = RescalingParams - - # Post Processing Dependencies (algorithm) - process_dependency: Callable[ - ..., Optional[BaseAlgorithm] - ] = available_algorithms.dependency - - # Reader dependency - reader_dependency: Type[DefaultDependency] = DefaultDependency - - # GDAL ENV dependency - environment_dependency: Callable[..., Dict] = field(default=lambda: {}) - - # TileMatrixSet dependency - supported_tms: TileMatrixSets = morecantile_tms - default_tms: str = "WebMercatorQuad" + router: APIRouter = field(factory=APIRouter) # Router Prefix is needed to find the path for /tile if the TilerFactory.router is mounted # with other router (multiple `.../tile` routes). # e.g if you mount the route with `/cog` prefix, set router_prefix to cog and router_prefix: str = "" - # add additional headers in response - optional_headers: List[OptionalHeader] = field(default_factory=list) - # add dependencies to specific routes - route_dependencies: List[Tuple[List[EndpointScope], List[DependsFunc]]] = field( - default_factory=list + route_dependencies: list[tuple[list[EndpointScope], list[DependsFunc]]] = field( + factory=list ) - extensions: List[FactoryExtension] = field(default_factory=list) + extensions: list[FactoryExtension] = field(factory=list) + + name: str | None = field(default=None) + operation_prefix: str = field(init=False, default="") + + conforms_to: set[str] = field(factory=set) + + enable_telemetry: bool = field(default=False) templates: Jinja2Templates = DEFAULT_TEMPLATES - def __post_init__(self): + def __attrs_post_init__(self): """Post Init: register route and configure specific options.""" + # prefix for endpoint's operationId + name = self.name or self.router_prefix.replace("/", ".") + self.operation_prefix = f"{name}." if name else "" + # Register endpoints self.register_routes() @@ -190,9 +177,12 @@ def __post_init__(self): for scopes, dependencies in self.route_dependencies: self.add_route_dependencies(scopes=scopes, dependencies=dependencies) + if self.enable_telemetry: + self.add_telemetry() + @abc.abstractmethod def register_routes(self): - """Register Tiler Routes.""" + """Register Routes.""" ... def url_for(self, request: Request, name: str, **path_params: Any) -> str: @@ -206,7 +196,7 @@ def url_for(self, request: Request, name: str, **path_params: Any) -> str: if "{" in prefix: _, path_format, param_convertors = compile_path(prefix) prefix, _ = replace_params( - path_format, param_convertors, request.path_params + path_format, param_convertors, request.path_params.copy() ) base_url += prefix @@ -215,8 +205,8 @@ def url_for(self, request: Request, name: str, **path_params: Any) -> str: def add_route_dependencies( self, *, - scopes: List[EndpointScope], - dependencies=List[DependsFunc], + scopes: list[EndpointScope], + dependencies=list[DependsFunc], ): """Add dependencies to routes. @@ -235,7 +225,8 @@ def add_route_dependencies( route.dependant.dependencies.insert( # type: ignore 0, get_parameterless_sub_dependant( - depends=depends, path=route.path_format # type: ignore + depends=depends, + path=route.path_format, # type: ignore ), ) @@ -245,36 +236,117 @@ def add_route_dependencies( # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678 route.dependencies.extend(dependencies) # type: ignore + def add_telemetry(self): + """ + Applies the factory_trace decorator to all registered API routes. + + This method iterates through the router's routes and wraps the endpoint + of each APIRoute to ensure consistent OpenTelemetry tracing. + """ + if not factory_trace.decorator_enabled: + warnings.warn( + "telemetry enabled for the factory class but tracing is not available", + RuntimeWarning, + stacklevel=2, + ) + return + + for route in self.router.routes: + if isinstance(route, APIRoute): + route.endpoint = factory_trace(route.endpoint, factory_instance=self) -@dataclass -class TilerFactory(BaseTilerFactory): + +@define(kw_only=True) +class TilerFactory(BaseFactory): """Tiler Factory. Attributes: reader (rio_tiler.io.base.BaseReader): A rio-tiler reader. Defaults to `rio_tiler.io.Reader`. + reader_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining BaseReader options. + path_dependency (Callable): Endpoint dependency defining `path` to pass to the reader init. + layer_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset indexes/bands/assets options. + dataset_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset overwriting options (e.g nodata). + tile_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining tile options (e.g buffer, padding). stats_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's statistics method. histogram_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for numpy's histogram method. - img_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's preview/crop method. + img_preview_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's preview method. + img_part_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's part/feature methods. + process_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image post-processing options (e.g rescaling, color-formula). + rescale_dependency (Callable[..., Optional[RescaleType]]): + color_formula_dependency (Callable[..., Optional[str]]): + colormap_dependency (Callable): Endpoint dependency defining ColorMap options (e.g colormap_name). + render_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image rendering options (e.g add_mask). + environment_dependency (Callable): Endpoint dependency to define GDAL environment at runtime. + supported_tms (morecantile.defaults.TileMatrixSets): TileMatrixSets object holding the supported TileMatrixSets. + templates (Jinja2Templates): Jinja2 templates. add_preview (bool): add `/preview` endpoints. Defaults to True. - add_part (bool): add `/crop` endpoints. Defaults to True. - add_viewer (bool): add `/map` endpoints. Defaults to True. + add_part (bool): add `/bbox` and `/feature` endpoints. Defaults to True. + add_viewer (bool): add `/map.html` endpoints. Defaults to True. """ # Default reader is set to rio_tiler.io.Reader - reader: Type[BaseReader] = Reader + reader: type[BaseReader] = Reader + + # Reader dependency + reader_dependency: type[DefaultDependency] = DefaultDependency + + # Path Dependency + path_dependency: Callable[..., Any] = DatasetPathParams + + # Indexes/Expression Dependencies + layer_dependency: type[DefaultDependency] = BidxExprParams + + # Rasterio Dataset Options (nodata, unscale, resampling, reproject) + dataset_dependency: type[DefaultDependency] = DatasetParams + + # Tile/Tilejson/WMTS Dependencies + tile_dependency: type[DefaultDependency] = TileParams # Statistics/Histogram Dependencies - stats_dependency: Type[DefaultDependency] = StatisticsParams - histogram_dependency: Type[DefaultDependency] = HistogramParams + stats_dependency: type[DefaultDependency] = StatisticsParams + histogram_dependency: type[DefaultDependency] = HistogramParams # Crop/Preview endpoints Dependencies - img_dependency: Type[DefaultDependency] = ImageParams + img_preview_dependency: type[DefaultDependency] = PreviewParams + img_part_dependency: type[DefaultDependency] = PartFeatureParams + + # Post Processing Dependencies (algorithm) + process_dependency: Callable[..., BaseAlgorithm | None] = ( + available_algorithms.dependency + ) + + # Image rendering Dependencies + colormap_dependency: Callable[..., ColorMapType | None] = ColorMapParams + render_dependency: type[DefaultDependency] = ImageRenderingParams + + # GDAL ENV dependency + environment_dependency: Callable[..., dict] = field(default=lambda: {}) + + # TileMatrixSet dependency + supported_tms: TileMatrixSets = morecantile_tms + + render_func: Callable[..., tuple[bytes, str]] = render_image # Add/Remove some endpoints add_preview: bool = True add_part: bool = True add_viewer: bool = True + add_ogc_maps: bool = False + + conforms_to: set[str] = field( + factory=lambda: { + # https://docs.ogc.org/is/20-057/20-057.html#toc30 + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tileset", + # https://docs.ogc.org/is/20-057/20-057.html#toc34 + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tilesets-list", + # https://docs.ogc.org/is/20-057/20-057.html#toc65 + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/core", + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/png", + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/jpeg", + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tiff", + } + ) def register_routes(self): """ @@ -286,13 +358,14 @@ def register_routes(self): """ # Default Routes - # (/bounds, /info, /statistics, /tile, /tilejson.json, /WMTSCapabilities.xml and /point) - self.bounds() + # (/info, /statistics, /tiles, /tilejson.json, and /point) self.info() self.statistics() + self.tilesets() self.tile() + if self.add_viewer: + self.map_viewer() self.tilejson() - self.wmts() self.point() # Optional Routes @@ -302,29 +375,8 @@ def register_routes(self): if self.add_part: self.part() - if self.add_viewer: - self.map_viewer() - - ############################################################################ - # /bounds - ############################################################################ - def bounds(self): - """Register /bounds endpoint.""" - - @self.router.get( - "/bounds", - response_model=Bounds, - responses={200: {"description": "Return dataset's bounds."}}, - ) - def bounds( - src_path=Depends(self.path_dependency), - reader_params=Depends(self.reader_dependency), - env=Depends(self.environment_dependency), - ): - """Return the bounds of the COG.""" - with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return {"bounds": src_dst.geographic_bounds} + if self.add_ogc_maps: + self.ogc_maps() ############################################################################ # /info @@ -338,6 +390,7 @@ def info(self): response_model_exclude_none=True, response_class=JSONResponse, responses={200: {"description": "Return dataset's basic info."}}, + operation_id=f"{self.operation_prefix}getInfo", ) def info( src_path=Depends(self.path_dependency), @@ -346,7 +399,7 @@ def info( ): """Return dataset's basic info.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + with self.reader(src_path, **reader_params.as_dict()) as src_dst: return src_dst.info() @self.router.get( @@ -360,18 +413,24 @@ def info( "description": "Return dataset's basic info as a GeoJSON feature.", } }, + operation_id=f"{self.operation_prefix}getInfoGeoJSON", ) def info_geojson( src_path=Depends(self.path_dependency), reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): """Return dataset's basic info as a GeoJSON feature.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + geometry = bounds_to_geometry(bounds) + return Feature( type="Feature", - geometry=Polygon.from_bounds(*src_dst.geographic_bounds), + bbox=bounds, + geometry=geometry, properties=src_dst.info(), ) @@ -392,26 +451,34 @@ def statistics(self): "description": "Return dataset's statistics.", } }, + operation_id=f"{self.operation_prefix}getStatistics", ) def statistics( src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_preview_dependency), + post_process=Depends(self.process_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Get Dataset statistics.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return src_dst.statistics( - **layer_params, - **image_params, - **dataset_params, - **stats_params, - hist_options={**histogram_params}, + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + image = src_dst.preview( + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), + ) + + if post_process: + image = post_process(image) + + return image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), ) # POST endpoint @@ -422,23 +489,27 @@ def statistics( response_class=GeoJSONResponse, responses={ 200: { - "content": {"application/json": {}}, - "description": "Return dataset's statistics.", + "content": {"application/geo+json": {}}, + "description": "Return dataset's statistics from feature or featureCollection.", } }, + operation_id=f"{self.operation_prefix}postStatisticsForGeoJSON", ) def geojson_statistics( - geojson: Union[FeatureCollection, Feature] = Body( - ..., description="GeoJSON Feature or FeatureCollection." - ), + geojson: Annotated[ + FeatureCollection | Feature, + Body(description="GeoJSON Feature or FeatureCollection."), + ], src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + reader_params=Depends(self.reader_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_part_dependency), + post_process=Depends(self.process_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Get Statistics from a geojson feature or featureCollection.""" @@ -447,181 +518,458 @@ def geojson_statistics( fc = FeatureCollection(type="FeatureCollection", features=[geojson]) with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - for feature in fc: - data = src_dst.feature( - feature.dict(exclude_none=True), + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + for feature in fc.features: + shape = feature.model_dump(exclude_none=True) + image = src_dst.feature( + shape, shape_crs=coord_crs or WGS84_CRS, - **layer_params, - **image_params, - **dataset_params, + dst_crs=dst_crs, + align_bounds_with_dataset=True, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), ) - stats = get_array_statistics( - data.as_masked(), - **stats_params, - **histogram_params, + + # Get the coverage % array + coverage_array = image.get_coverage_array( + shape, + shape_crs=coord_crs or WGS84_CRS, ) - feature.properties = feature.properties or {} - feature.properties.update( - { - "statistics": { - f"{data.band_names[ix]}": BandStatistics( - **stats[ix] - ) - for ix in range(len(stats)) - } - } + if post_process: + image = post_process(image) + + stats = image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), + coverage=coverage_array, ) + feature.properties = feature.properties or {} + feature.properties.update({"statistics": stats}) + return fc.features[0] if isinstance(geojson, Feature) else fc + ############################################################################ + # /tileset + ############################################################################ + def tilesets(self): # noqa: C901 + """Register OGC tilesets endpoints.""" + + @self.router.get( + "/tiles", + response_model=TileSetList, + response_class=JSONResponse, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "application/json": {}, + "text/html": {}, + } + } + }, + summary="Retrieve a list of available raster tilesets for the specified dataset.", + operation_id=f"{self.operation_prefix}getTileSetList", + ) + async def tileset_list( + request: Request, + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), + env=Depends(self.environment_dependency), + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, + ): + """Retrieve a list of available raster tilesets for the specified dataset.""" + with rasterio.Env(**env): + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + + collection_bbox = { + "lowerLeft": [bounds[0], bounds[1]], + "upperRight": [bounds[2], bounds[3]], + "crs": CRS_to_uri(crs or WGS84_CRS), + } + + qs = [ + (key, value) + for (key, value) in request.query_params._list + if key.lower() not in ["crs"] + ] + query_string = f"?{urlencode(qs)}" if qs else "" + + attribution = os.environ.get("TITILER_DEFAULT_ATTRIBUTION") + + tilesets: list[dict[str, Any]] = [] + for tms in self.supported_tms.list(): + tileset: dict[str, Any] = { + "title": f"tileset tiled using {tms} TileMatrixSet", + "attribution": attribution, + "dataType": "map", + "crs": self.supported_tms.get(tms).crs, + "boundingBox": collection_bbox, + "links": [ + { + "href": self.url_for( + request, "tileset", tileMatrixSetId=tms + ) + + query_string, + "rel": "self", + "type": "application/json", + "title": f"Tileset tiled using {tms} TileMatrixSet", + }, + { + "href": self.url_for( + request, + "tile", + tileMatrixSetId=tms, + z="{z}", + x="{x}", + y="{y}", + ) + + query_string, + "rel": "tile", + "title": "Templated link for retrieving Raster tiles", + }, + ], + } + + try: + tileset["links"].append( + { + "href": str( + request.url_for("tilematrixset", tileMatrixSetId=tms) + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type": "application/json", + "title": f"Definition of '{tms}' tileMatrixSet", + } + ) + except NoMatchFound: + pass + + tilesets.append(tileset) + + data = TileSetList.model_validate({"tilesets": tilesets}) + + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) + + if output_type == MediaType.html: + return create_html_response( + request, + data.model_dump(exclude_none=True, mode="json"), + title="Tilesets", + template_name="tilesets", + templates=self.templates, + ) + + return data + + @self.router.get( + "/tiles/{tileMatrixSetId}", + response_model=TileSet, + response_class=JSONResponse, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "application/json": {}, + "text/html": {}, + } + } + }, + summary="Retrieve the raster tileset metadata for the specified dataset and tiling scheme (tile matrix set).", + operation_id=f"{self.operation_prefix}getTileSet", + ) + async def tileset( + request: Request, + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + env=Depends(self.environment_dependency), + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, + ): + """Retrieve the raster tileset metadata for the specified dataset and tiling scheme (tile matrix set).""" + tms = self.supported_tms.get(tileMatrixSetId) + with rasterio.Env(**env): + with self.reader( + src_path, tms=tms, **reader_params.as_dict() + ) as src_dst: + bounds = src_dst.get_geographic_bounds(tms.rasterio_geographic_crs) + minzoom = src_dst.minzoom + maxzoom = src_dst.maxzoom + + collection_bbox = { + "lowerLeft": [bounds[0], bounds[1]], + "upperRight": [bounds[2], bounds[3]], + "crs": CRS_to_uri(tms.rasterio_geographic_crs), + } + + tilematrix_limits = tms_limits( + tms, + bounds, + zooms=(minzoom, maxzoom), + ) + + query_string = ( + f"?{urlencode(request.query_params._list)}" + if request.query_params._list + else "" + ) + + links = [ + { + "href": self.url_for( + request, + "tileset", + tileMatrixSetId=tileMatrixSetId, + ), + "rel": "self", + "type": "application/json", + "title": f"Tileset tiled using {tileMatrixSetId} TileMatrixSet", + }, + { + "href": self.url_for( + request, + "tile", + tileMatrixSetId=tileMatrixSetId, + z="{z}", + x="{x}", + y="{y}", + ) + + query_string, + "rel": "tile", + "title": "Templated link for retrieving Raster tiles", + "templated": True, + }, + ] + try: + links.append( + { + "href": str( + request.url_for( + "tilematrixset", tileMatrixSetId=tileMatrixSetId + ) + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type": "application/json", + "title": f"Definition of '{tileMatrixSetId}' tileMatrixSet", + } + ) + except NoMatchFound: + pass + + if self.add_viewer: + links.append( + { + "href": self.url_for( + request, + "map_viewer", + tileMatrixSetId=tileMatrixSetId, + ) + + query_string, + "type": "text/html", + "rel": "data", + "title": f"Map viewer for '{tileMatrixSetId}' tileMatrixSet", + } + ) + + data = TileSet.model_validate( + { + "title": f"tileset tiled using {tileMatrixSetId} TileMatrixSet", + "dataType": "map", + "crs": tms.crs, + "boundingBox": collection_bbox, + "links": links, + "tileMatrixSetLimits": tilematrix_limits, + "attribution": os.environ.get("TITILER_DEFAULT_ATTRIBUTION"), + } + ) + + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) + + if output_type == MediaType.html: + return create_html_response( + request, + data, + title=tileMatrixSetId, + template_name="tileset", + templates=self.templates, + ) + + return data + ############################################################################ # /tiles ############################################################################ def tile(self): # noqa: C901 """Register /tiles endpoint.""" - @self.router.get(r"/tiles/{z}/{x}/{y}", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) - @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params - ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}", + operation_id=f"{self.operation_prefix}getTile", + **img_endpoint_params, ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}", + operation_id=f"{self.operation_prefix}getTileWithFormat", **img_endpoint_params, ) def tile( - z: int = Path(..., ge=0, le=30, description="TMS tiles's zoom level"), - x: int = Path(..., description="TMS tiles's column"), - y: int = Path(..., description="TMS tiles's row"), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), - scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - format: ImageType = Query( - None, description="Output image type. Default is auto." - ), + z: Annotated[ + int, + Path( + description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.", + ), + ], + x: Annotated[ + int, + Path( + description="Column (X) index of the tile on the selected TileMatrix. It cannot exceed the MatrixHeight-1 for the selected TileMatrix.", + ), + ], + y: Annotated[ + int, + Path( + description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.", + ), + ], + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + format: Annotated[ + ImageType | None, + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." + ), + ] = None, + tilesize: Annotated[ + int | None, + Query(gt=0, description="Tilesize in pixels."), + ] = None, src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + tile_params=Depends(self.tile_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - buffer: Optional[float] = Query( - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), post_process=Depends(self.process_dependency), - rescale=Depends(self.rescale_dependency), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Create map tile from a dataset.""" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader(src_path, tms=tms, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader( + src_path, tms=tms, **reader_params.as_dict() + ) as src_dst: image = src_dst.tile( x, y, z, - tilesize=scale * 256, - buffer=buffer, - **layer_params, - **dataset_params, + tilesize=tilesize, + **tile_params.as_dict(), + **layer_params.as_dict(), + **dataset_params.as_dict(), ) dst_colormap = getattr(src_dst, "colormap", None) if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) - - if color_formula: - image.apply_color_formula(color_formula) - - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) - - if not format: - format = ImageType.jpeg if image.mask.all() else ImageType.png - - content = image.render( - img_format=format.driver, - **format.profile, - **render_params, + content, media_type = self.render_func( + image, + output_format=format, + colormap=colormap or dst_colormap, + **render_params.as_dict(), ) - return Response(content, media_type=format.mediatype) + headers: dict[str, str] = {} + if image.bounds is not None: + headers["Content-Bbox"] = ",".join(map(str, image.bounds)) + if uri := CRS_to_uri(image.crs): + headers["Content-Crs"] = f"<{uri}>" + + return Response(content, media_type=media_type, headers=headers) def tilejson(self): # noqa: C901 """Register /tilejson.json endpoint.""" @self.router.get( - "/tilejson.json", - response_model=TileJSON, - responses={200: {"description": "Return a tilejson"}}, - response_model_exclude_none=True, - ) - @self.router.get( - "/{TileMatrixSetId}/tilejson.json", + "/{tileMatrixSetId}/tilejson.json", response_model=TileJSON, responses={200: {"description": "Return a tilejson"}}, response_model_exclude_none=True, + operation_id=f"{self.operation_prefix}getTileJSON", ) def tilejson( request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + tilesize: Annotated[ + int | None, + Query(gt=0, description="Tilesize in pixels. Default to 512."), + ] = 512, + tile_format: Annotated[ + ImageType | None, + Query( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, + minzoom: Annotated[ + int | None, + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + int | None, + Query(description="Overwrite default maxzoom."), + ] = None, src_path=Depends(self.path_dependency), - tile_format: Optional[ImageType] = Query( - None, description="Output image type. Default is auto." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa reader_params=Depends(self.reader_dependency), + tile_params=Depends(self.tile_dependency), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + post_process=Depends(self.process_dependency), + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), env=Depends(self.environment_dependency), ): """Return TileJSON document for a dataset.""" @@ -629,8 +977,7 @@ def tilejson( "z": "{z}", "x": "{x}", "y": "{y}", - "scale": tile_scale, - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } if tile_format: route_params["format"] = tile_format.value @@ -639,240 +986,160 @@ def tilejson( qs_key_to_remove = [ "tilematrixsetid", "tile_format", - "tile_scale", "minzoom", "maxzoom", ] - qs = [ + qs: list[tuple[str, Any]] = [ (key, value) for (key, value) in request.query_params._list if key.lower() not in qs_key_to_remove ] - if qs: - tiles_url += f"?{urlencode(qs)}" + if "tilesize" not in request.query_params: + qs.append(("tilesize", str(tilesize))) + tiles_url += f"?{urlencode(qs)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader(src_path, tms=tms, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader( + src_path, tms=tms, **reader_params.as_dict() + ) as src_dst: return { - "bounds": src_dst.geographic_bounds, + "bounds": src_dst.get_geographic_bounds( + tms.rasterio_geographic_crs + ), "minzoom": minzoom if minzoom is not None else src_dst.minzoom, "maxzoom": maxzoom if maxzoom is not None else src_dst.maxzoom, "tiles": [tiles_url], + "attribution": os.environ.get("TITILER_DEFAULT_ATTRIBUTION"), } def map_viewer(self): # noqa: C901 - """Register /map endpoint.""" + """Register /map.html endpoint.""" - @self.router.get("/map", response_class=HTMLResponse) - @self.router.get("/{TileMatrixSetId}/map", response_class=HTMLResponse) + @self.router.get( + "/{tileMatrixSetId}/map.html", + response_class=HTMLResponse, + operation_id=f"{self.operation_prefix}getMapViewer", + ) def map_viewer( request: Request, + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + tile_format: Annotated[ + ImageType | None, + Query( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, + tilesize: Annotated[ + int, + Query(gt=0, description="Tilesize in pixels. Default to 256."), + ] = 256, + minzoom: Annotated[ + int | None, + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + int | None, + Query(description="Overwrite default maxzoom."), + ] = None, src_path=Depends(self.path_dependency), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), # noqa - tile_format: Optional[ImageType] = Query( - None, description="Output image type. Default is auto." - ), # noqa - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), # noqa - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), # noqa - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), # noqa - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa - reader_params=Depends(self.reader_dependency), # noqa - env=Depends(self.environment_dependency), # noqa + reader_params=Depends(self.reader_dependency), + tile_params=Depends(self.tile_dependency), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + post_process=Depends(self.process_dependency), + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), + env=Depends(self.environment_dependency), ): """Return TileJSON document for a dataset.""" tilejson_url = self.url_for( - request, "tilejson", TileMatrixSetId=TileMatrixSetId + request, "tilejson", tileMatrixSetId=tileMatrixSetId ) + + qs = list(request.query_params._list) + if "tilesize" not in request.query_params: + qs.append(("tilesize", tilesize)) + tilejson_url += f"?{urlencode(qs)}" + + point_url = self.url_for(request, "point", lon="{lon}", lat="{lat}") if request.query_params._list: - tilejson_url += f"?{urlencode(request.query_params._list)}" + qs_key_to_remove = [ + "tilesize", + "tile_format", + "minzoom", + "maxzoom", + "buffer", + "padding", + "colormap", + "colormap_name", + ] + qs = [ + (key, value) + for (key, value) in request.query_params._list + if key.lower() not in qs_key_to_remove + ] + point_url += f"?{urlencode(qs)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) return self.templates.TemplateResponse( + request, name="map.html", context={ - "request": request, "tilejson_endpoint": tilejson_url, + "point_endpoint": point_url, "tms": tms, - "resolutions": [tms._resolution(matrix) for matrix in tms], + "resolutions": [matrix.cellSize for matrix in tms], }, media_type="text/html", ) - def wmts(self): # noqa: C901 - """Register /wmts endpoint.""" + ############################################################################ + # /point + ############################################################################ + def point(self): + """Register /point endpoints.""" - @self.router.get("/WMTSCapabilities.xml", response_class=XMLResponse) @self.router.get( - "/{TileMatrixSetId}/WMTSCapabilities.xml", response_class=XMLResponse + "/point/{lon},{lat}", + response_model=Point, + response_class=JSONResponse, + responses={200: {"description": "Return a value for a point"}}, + operation_id=f"{self.operation_prefix}getDataForPoint", ) - def wmts( - request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), + def point( + lon: Annotated[float, Path(description="Longitude")], + lat: Annotated[float, Path(description="Latitude")], src_path=Depends(self.path_dependency), - tile_format: ImageType = Query( - ImageType.png, description="Output image type. Default is png." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa - reader_params=Depends(self.reader_dependency), - env=Depends(self.environment_dependency), - ): - """OGC WMTS endpoint.""" - route_params = { - "z": "{TileMatrix}", - "x": "{TileCol}", - "y": "{TileRow}", - "scale": tile_scale, - "format": tile_format.value, - "TileMatrixSetId": TileMatrixSetId, - } - tiles_url = self.url_for(request, "tile", **route_params) - - qs_key_to_remove = [ - "tilematrixsetid", - "tile_format", - "tile_scale", - "minzoom", - "maxzoom", - "service", - "request", - ] - qs = [ - (key, value) - for (key, value) in request.query_params._list - if key.lower() not in qs_key_to_remove - ] - if qs: - tiles_url += f"?{urlencode(qs)}" - - tms = self.supported_tms.get(TileMatrixSetId) - with rasterio.Env(**env): - with self.reader(src_path, tms=tms, **reader_params) as src_dst: - bounds = src_dst.geographic_bounds - minzoom = minzoom if minzoom is not None else src_dst.minzoom - maxzoom = maxzoom if maxzoom is not None else src_dst.maxzoom - - tileMatrix = [] - for zoom in range(minzoom, maxzoom + 1): - matrix = tms.matrix(zoom) - tm = f""" - - {matrix.identifier} - {matrix.scaleDenominator} - {matrix.topLeftCorner[0]} {matrix.topLeftCorner[1]} - {matrix.tileWidth} - {matrix.tileHeight} - {matrix.matrixWidth} - {matrix.matrixHeight} - """ - tileMatrix.append(tm) - - return self.templates.TemplateResponse( - "wmts.xml", - { - "request": request, - "tiles_endpoint": tiles_url, - "bounds": bounds, - "tileMatrix": tileMatrix, - "tms": tms, - "title": "Cloud Optimized GeoTIFF", - "layer_name": "cogeo", - "media_type": tile_format.mediatype, - }, - media_type=MediaType.xml.value, - ) - - ############################################################################ - # /point - ############################################################################ - def point(self): - """Register /point endpoints.""" - - @self.router.get( - r"/point/{lon},{lat}", - response_model=Point, - response_class=JSONResponse, - responses={200: {"description": "Return a value for a point"}}, - ) - def point( - lon: float = Path(..., description="Longitude"), - lat: float = Path(..., description="Latitude"), - src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), - layer_params=Depends(self.layer_dependency), - dataset_params=Depends(self.dataset_dependency), reader_params=Depends(self.reader_dependency), + coord_crs=Depends(CoordCRSParams), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), env=Depends(self.environment_dependency), ): """Get Point value for a dataset.""" - with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: pts = src_dst.point( lon, lat, coord_crs=coord_crs or WGS84_CRS, - **layer_params, - **dataset_params, + **layer_params.as_dict(), + **dataset_params.as_dict(), ) return { "coordinates": [lon, lat], - "values": pts.data.tolist(), + "values": pts.array.tolist(), "band_names": pts.band_names, + "band_descriptions": pts.band_descriptions, } ############################################################################ @@ -881,36 +1148,47 @@ def point( def preview(self): """Register /preview endpoint.""" - @self.router.get(r"/preview", **img_endpoint_params) - @self.router.get(r"/preview.{format}", **img_endpoint_params) + @self.router.get( + "/preview", + operation_id=f"{self.operation_prefix}getPreview", + **img_endpoint_params, + ) + @self.router.get( + "/preview.{format}", + operation_id=f"{self.operation_prefix}getPreviewWithFormat", + **img_endpoint_params, + ) + @self.router.get( + "/preview/{width}x{height}.{format}", + operation_id=f"{self.operation_prefix}getPreviewWithSizeAndFormat", + **img_endpoint_params, + ) def preview( - format: ImageType = Query( - None, description="Output image type. Default is auto." - ), + format: Annotated[ + ImageType | None, + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." + ), + ] = None, src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), layer_params=Depends(self.layer_dependency), - dst_crs: Optional[CRS] = Depends(DstCRSParams), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_preview_dependency), + dst_crs=Depends(DstCRSParams), post_process=Depends(self.process_dependency), - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Create preview of a dataset.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: image = src_dst.preview( - **layer_params, - **image_params, - **dataset_params, + **layer_params.as_dict(), + **image_params.as_dict(exclude_none=False), + **dataset_params.as_dict(), dst_crs=dst_crs, ) dst_colormap = getattr(src_dst, "colormap", None) @@ -918,170 +1196,249 @@ def preview( if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) - - if color_formula: - image.apply_color_formula(color_formula) - - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) - - if not format: - format = ImageType.jpeg if image.mask.all() else ImageType.png - - content = image.render( - img_format=format.driver, - **format.profile, - **render_params, + content, media_type = self.render_func( + image, + output_format=format, + colormap=colormap or dst_colormap, + **render_params.as_dict(), ) - return Response(content, media_type=format.mediatype) + headers: dict[str, str] = {} + if image.bounds is not None: + headers["Content-Bbox"] = ",".join(map(str, image.bounds)) + if uri := CRS_to_uri(image.crs): + headers["Content-Crs"] = f"<{uri}>" + + return Response(content, media_type=media_type, headers=headers) ############################################################################ - # /crop (Optional) + # /bbox and /feature (Optional) ############################################################################ def part(self): # noqa: C901 - """Register /crop endpoint.""" + """Register /bbox and `/feature` endpoints.""" # GET endpoints @self.router.get( - r"/crop/{minx},{miny},{maxx},{maxy}.{format}", + "/bbox/{minx},{miny},{maxx},{maxy}.{format}", + operation_id=f"{self.operation_prefix}getDataForBoundingBoxWithFormat", **img_endpoint_params, ) @self.router.get( - r"/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}", + "/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}", + operation_id=f"{self.operation_prefix}getDataForBoundingBoxWithSizesAndFormat", **img_endpoint_params, ) - def part( - minx: float = Path(..., description="Bounding box min X"), - miny: float = Path(..., description="Bounding box min Y"), - maxx: float = Path(..., description="Bounding box max X"), - maxy: float = Path(..., description="Bounding box max Y"), - format: ImageType = Query(..., description="Output image type."), + def bbox_image( + minx: Annotated[float, Path(description="Bounding box min X")], + miny: Annotated[float, Path(description="Bounding box min Y")], + maxx: Annotated[float, Path(description="Bounding box max X")], + maxy: Annotated[float, Path(description="Bounding box max Y")], + format: Annotated[ + ImageType | None, + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, src_path=Depends(self.path_dependency), - dst_crs: Optional[CRS] = Depends(DstCRSParams), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + reader_params=Depends(self.reader_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_part_dependency), + dst_crs=Depends(DstCRSParams), + coord_crs=Depends(CoordCRSParams), post_process=Depends(self.process_dependency), - rescale=Depends(self.rescale_dependency), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): - """Create image from part of a dataset.""" + """Create image from a bbox.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: image = src_dst.part( [minx, miny, maxx, maxy], dst_crs=dst_crs, bounds_crs=coord_crs or WGS84_CRS, - **layer_params, - **image_params, - **dataset_params, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), ) dst_colormap = getattr(src_dst, "colormap", None) if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) - - if color_formula: - image.apply_color_formula(color_formula) - - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) - - content = image.render( - img_format=format.driver, - **format.profile, - **render_params, + content, media_type = self.render_func( + image, + output_format=format, + colormap=colormap or dst_colormap, + **render_params.as_dict(), ) - return Response(content, media_type=format.mediatype) + headers: dict[str, str] = {} + if image.bounds is not None: + headers["Content-Bbox"] = ",".join(map(str, image.bounds)) + if uri := CRS_to_uri(image.crs): + headers["Content-Crs"] = f"<{uri}>" + + return Response(content, media_type=media_type, headers=headers) # POST endpoints @self.router.post( - r"/crop", + "/feature", + operation_id=f"{self.operation_prefix}postDataForGeoJSON", **img_endpoint_params, ) @self.router.post( - r"/crop.{format}", + "/feature.{format}", + operation_id=f"{self.operation_prefix}postDataForGeoJSONWithFormat", **img_endpoint_params, ) @self.router.post( - r"/crop/{width}x{height}.{format}", + "/feature/{width}x{height}.{format}", + operation_id=f"{self.operation_prefix}postDataForGeoJSONWithSizesAndFormat", **img_endpoint_params, ) - def geojson_crop( - geojson: Feature = Body(..., description="GeoJSON Feature."), - format: ImageType = Query( - None, description="Output image type. Default is auto." - ), + def feature_image( + geojson: Annotated[Feature, Body(description="GeoJSON Feature.")], + format: Annotated[ + ImageType | None, + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." + ), + ] = None, src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + reader_params=Depends(self.reader_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_part_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), post_process=Depends(self.process_dependency), - rescale=Depends(self.rescale_dependency), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Create image from a geojson feature.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: image = src_dst.feature( - geojson.dict(exclude_none=True), + geojson.model_dump(exclude_none=True), shape_crs=coord_crs or WGS84_CRS, - **layer_params, - **image_params, - **dataset_params, + dst_crs=dst_crs, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), ) dst_colormap = getattr(src_dst, "colormap", None) if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) + content, media_type = self.render_func( + image, + output_format=format, + colormap=colormap or dst_colormap, + **render_params.as_dict(), + ) + + headers: dict[str, str] = {} + if image.bounds is not None: + headers["Content-Bbox"] = ",".join(map(str, image.bounds)) + if uri := CRS_to_uri(image.crs): + headers["Content-Crs"] = f"<{uri}>" + + return Response(content, media_type=media_type, headers=headers) + + ############################################################################ + # OGC Maps (Optional) + ############################################################################ + def ogc_maps(self): # noqa: C901 + """Register OGC Maps /map` endpoint.""" + + self.conforms_to.update( + { + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/core", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/crs", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/scaling", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/scaling/width-definition", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/scaling/height-definition", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/spatial-subsetting", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/spatial-subsetting/bbox-definition", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/spatial-subsetting/bbox-crs", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/spatial-subsetting/crs-curie", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/png", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/jpeg", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/tiff", + } + ) + + # GET endpoints + @self.router.get( + "/map", + operation_id=f"{self.operation_prefix}getMap", + **img_endpoint_params, + ) + def get_map( + src_path=Depends(self.path_dependency), + ogc_params=Depends(OGCMapsParams), + reader_params=Depends(self.reader_dependency), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + post_process=Depends(self.process_dependency), + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), + env=Depends(self.environment_dependency), + ) -> Response: + """OGC Maps API.""" + with rasterio.Env(**env): + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + if ogc_params.bbox is not None: + image = src_dst.part( + ogc_params.bbox, + dst_crs=ogc_params.crs or src_dst.crs, + bounds_crs=ogc_params.bbox_crs or WGS84_CRS, + width=ogc_params.width, + height=ogc_params.height, + max_size=ogc_params.max_size, + **layer_params.as_dict(), + **dataset_params.as_dict(), + ) - if color_formula: - image.apply_color_formula(color_formula) + else: + image = src_dst.preview( + width=ogc_params.width, + height=ogc_params.height, + max_size=ogc_params.max_size, + dst_crs=ogc_params.crs or src_dst.crs, + **layer_params.as_dict(), + **dataset_params.as_dict(), + ) - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) + dst_colormap = getattr(src_dst, "colormap", None) - if not format: - format = ImageType.jpeg if image.mask.all() else ImageType.png + if post_process: + image = post_process(image) - content = image.render( - img_format=format.driver, - **format.profile, - **render_params, + content, media_type = self.render_func( + image, + output_format=ogc_params.format, + colormap=colormap or dst_colormap, + **render_params.as_dict(), ) - return Response(content, media_type=format.mediatype) + headers: dict[str, str] = {} + if image.bounds is not None: + headers["Content-Bbox"] = ",".join(map(str, image.bounds)) + if uri := CRS_to_uri(image.crs): + headers["Content-Crs"] = f"<{uri}>" + return Response(content, media_type=media_type, headers=headers) -@dataclass + +@define(kw_only=True) class MultiBaseTilerFactory(TilerFactory): """Custom Tiler Factory for MultiBaseReader classes. @@ -1096,13 +1453,13 @@ class MultiBaseTilerFactory(TilerFactory): """ - reader: Type[MultiBaseReader] + reader: type[MultiBaseReader] # type: ignore - # Assets/Indexes/Expression dependency - layer_dependency: Type[DefaultDependency] = AssetsBidxExprParams + # Assets/Expression dependency + layer_dependency: type[DefaultDependency] = AssetsExprParams # Assets dependency - assets_dependency: Type[DefaultDependency] = AssetsParams + assets_dependency: type[DefaultDependency] = AssetsParams # Overwrite the `/info` endpoint to return the list of assets when no assets is passed. def info(self): @@ -1118,17 +1475,22 @@ def info(self): "description": "Return dataset's basic info or the list of available assets." } }, + operation_id=f"{self.operation_prefix}getInfo", ) def info( src_path=Depends(self.path_dependency), - asset_params=Depends(self.assets_dependency), reader_params=Depends(self.reader_dependency), + asset_params=Depends(self.assets_dependency), env=Depends(self.environment_dependency), - ): + ) -> MultiBaseInfo: """Return dataset's basic info or the list of available assets.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return src_dst.info(**asset_params) + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + if asset_params.assets == [":all:"]: + asset_params.assets = src_dst.assets + + return src_dst.info(**asset_params.as_dict()) @self.router.get( "/info.geojson", @@ -1141,31 +1503,37 @@ def info( "description": "Return dataset's basic info as a GeoJSON feature.", } }, + operation_id=f"{self.operation_prefix}getInfoGeoJSON", ) def info_geojson( src_path=Depends(self.path_dependency), - asset_params=Depends(self.assets_dependency), reader_params=Depends(self.reader_dependency), + asset_params=Depends(self.assets_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), - ): + ) -> MultiBaseInfoGeoJSON: """Return dataset's basic info as a GeoJSON feature.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + if asset_params.assets == [":all:"]: + asset_params.assets = src_dst.assets + + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + geometry = bounds_to_geometry(bounds) + return Feature( type="Feature", - geometry=Polygon.from_bounds(*src_dst.geographic_bounds), - properties={ - asset: asset_info - for asset, asset_info in src_dst.info( - **asset_params - ).items() - }, + bbox=bounds, + geometry=geometry, + properties=src_dst.info(**asset_params.as_dict()), ) @self.router.get( "/assets", - response_model=List[str], + response_model=list[str], responses={200: {"description": "Return a list of supported assets."}}, + operation_id=f"{self.operation_prefix}getAssets", ) def available_assets( src_path=Depends(self.path_dependency), @@ -1174,7 +1542,8 @@ def available_assets( ): """Return a list of supported assets.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: return src_dst.assets # Overwrite the `/statistics` endpoint because the MultiBaseReader output model is different (Dict[str, Dict[str, BandStatistics]]) @@ -1193,26 +1562,31 @@ def statistics(self): # noqa: C901 "description": "Return dataset's statistics.", } }, + operation_id=f"{self.operation_prefix}getAssetsStatistics", ) def asset_statistics( src_path=Depends(self.path_dependency), - asset_params=Depends(AssetsBidxParams), + reader_params=Depends(self.reader_dependency), + asset_params=Depends(self.assets_dependency), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_preview_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Per Asset statistics""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + if asset_params.assets == [":all:"]: + asset_params.assets = src_dst.assets + return src_dst.statistics( - **asset_params, - **image_params, - **dataset_params, - **stats_params, - hist_options={**histogram_params}, + **asset_params.as_dict(), + **image_params.as_dict(exclude_none=False), + **dataset_params.as_dict(), + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), ) # MultiBaseReader merged statistics @@ -1228,30 +1602,38 @@ def asset_statistics( "description": "Return dataset's statistics.", } }, + operation_id=f"{self.operation_prefix}getStatistics", ) def statistics( src_path=Depends(self.path_dependency), - layer_params=Depends(AssetsBidxExprParamsOptional), + reader_params=Depends(self.reader_dependency), + layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_preview_dependency), + post_process=Depends(self.process_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Merged assets statistics.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - # Default to all available assets - if not layer_params.assets and not layer_params.expression: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + if layer_params.assets == [":all:"]: layer_params.assets = src_dst.assets - return src_dst.merged_statistics( - **layer_params, - **image_params, - **dataset_params, - **stats_params, - hist_options={**histogram_params}, + image = src_dst.preview( + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), + ) + + if post_process: + image = post_process(image) + + return image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), ) # POST endpoint @@ -1262,23 +1644,27 @@ def statistics( response_class=GeoJSONResponse, responses={ 200: { - "content": {"application/json": {}}, - "description": "Return dataset's statistics.", + "content": {"application/geo+json": {}}, + "description": "Return dataset's statistics from feature or featureCollection.", } }, + operation_id=f"{self.operation_prefix}postStatisticsForGeoJSON", ) def geojson_statistics( - geojson: Union[FeatureCollection, Feature] = Body( - ..., description="GeoJSON Feature or FeatureCollection." - ), + geojson: Annotated[ + FeatureCollection | Feature, + Body(description="GeoJSON Feature or FeatureCollection."), + ], src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), - layer_params=Depends(AssetsBidxExprParamsOptional), + reader_params=Depends(self.reader_dependency), + layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), + post_process=Depends(self.process_dependency), + image_params=Depends(self.img_part_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Get Statistics from a geojson feature or featureCollection.""" @@ -1287,372 +1673,580 @@ def geojson_statistics( fc = FeatureCollection(type="FeatureCollection", features=[geojson]) with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - # Default to all available assets - if not layer_params.assets and not layer_params.expression: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + if layer_params.assets == [":all:"]: layer_params.assets = src_dst.assets - for feature in fc: - data = src_dst.feature( - feature.dict(exclude_none=True), + for feature in fc.features: + image = src_dst.feature( + feature.model_dump(exclude_none=True), shape_crs=coord_crs or WGS84_CRS, - **layer_params, - **image_params, - **dataset_params, + dst_crs=dst_crs, + align_bounds_with_dataset=True, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), ) - stats = get_array_statistics( - data.as_masked(), - **stats_params, - **histogram_params, + if post_process: + image = post_process(image) + + stats = image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), ) - feature.properties = feature.properties or {} - feature.properties.update( - { - # NOTE: because we use `src_dst.feature` the statistics will be in form of - # `Dict[str, BandStatistics]` and not `Dict[str, Dict[str, BandStatistics]]` - "statistics": { - f"{data.band_names[ix]}": BandStatistics(**stats[ix]) - for ix in range(len(stats)) - } - } - ) + feature.properties = feature.properties or {} + # NOTE: because we use `src_dst.feature` the statistics will be in form of + # `Dict[str, BandStatistics]` and not `Dict[str, Dict[str, BandStatistics]]` + feature.properties.update({"statistics": stats}) return fc.features[0] if isinstance(geojson, Feature) else fc -@dataclass -class MultiBandTilerFactory(TilerFactory): - """Custom Tiler Factory for MultiBandReader classes. - - Note: - To be able to use the rio_tiler.io.MultiBandReader we need to be able to pass a `bands` - argument to most of its methods. By using the `BandsExprParams` for the `layer_dependency`, the - .tile(), .point(), .preview() and the .part() methods will receive bands or expression arguments. - - The rio_tiler.io.MultiBandReader `.info()` and `.metadata()` have `bands` as - a requirement arguments (https://github.com/cogeotiff/rio-tiler/blob/main/rio_tiler/io/base.py#L775). - This means we have to update the /info and /metadata endpoints in order to add the `bands` dependency. - - For implementation example see https://github.com/developmentseed/titiler-pds - - """ - - reader: Type[MultiBandReader] - - # Assets/Expression dependency - layer_dependency: Type[DefaultDependency] = BandsExprParams +@define(kw_only=True) +class TMSFactory(BaseFactory): + """TileMatrixSet endpoints Factory.""" - # Bands dependency - bands_dependency: Type[DefaultDependency] = BandsParams + supported_tms: TileMatrixSets = morecantile_tms - def info(self): - """Register /info endpoint.""" + def register_routes(self): + """Register TMS endpoint routes.""" @self.router.get( - "/info", - response_model=Info, + "/tileMatrixSets", + response_model=TileMatrixSetList, response_model_exclude_none=True, - response_class=JSONResponse, - responses={200: {"description": "Return dataset's basic info."}}, + summary="Retrieve the list of available tiling schemes (tile matrix sets).", + operation_id=f"{self.operation_prefix}getTileMatrixSetsList", + responses={ + 200: { + "content": { + "application/json": {}, + "text/html": {}, + }, + }, + }, ) - def info( - src_path=Depends(self.path_dependency), - bands_params=Depends(self.bands_dependency), - reader_params=Depends(self.reader_dependency), - env=Depends(self.environment_dependency), + async def tilematrixsets( + request: Request, + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, ): - """Return dataset's basic info.""" - with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return src_dst.info(**bands_params) + """ + OGC Specification: http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets + """ + + data = TileMatrixSetList( + tileMatrixSets=[ + { # type: ignore + "id": tms_id, + "links": [ + { + "href": self.url_for( + request, + "tilematrixset", + tileMatrixSetId=tms_id, + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type": "application/json", + "title": f"Definition of {tms_id} tileMatrixSet", + } + ], + } + for tms_id in self.supported_tms.list() + ] + ) + + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) + + if output_type == MediaType.html: + return create_html_response( + request, + data.model_dump(exclude_none=True, mode="json"), + title="TileMatrixSets", + template_name="tilematrixsets", + templates=self.templates, + ) + + return data @self.router.get( - "/info.geojson", - response_model=InfoGeoJSON, + "/tileMatrixSets/{tileMatrixSetId}", + response_model=TileMatrixSet, response_model_exclude_none=True, - response_class=GeoJSONResponse, + summary="Retrieve the definition of the specified tiling scheme (tile matrix set).", + operation_id=f"{self.operation_prefix}getTileMatrixSet", responses={ 200: { - "content": {"application/geo+json": {}}, - "description": "Return dataset's basic info as a GeoJSON feature.", - } + "content": { + "application/json": {}, + "text/html": {}, + }, + }, }, ) - def info_geojson( - src_path=Depends(self.path_dependency), - bands_params=Depends(self.bands_dependency), - reader_params=Depends(self.reader_dependency), - env=Depends(self.environment_dependency), + async def tilematrixset( + request: Request, + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path(description="Identifier for a supported TileMatrixSet."), + ], + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, ): - """Return dataset's basic info as a GeoJSON feature.""" - with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return Feature( - type="Feature", - geometry=Polygon.from_bounds(*src_dst.geographic_bounds), - properties=src_dst.info(**bands_params), - ) + """ + OGC Specification: http://docs.opengeospatial.org/per/19-069.html#_tilematrixset + """ + tms = self.supported_tms.get(tileMatrixSetId) + + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) - @self.router.get( - "/bands", - response_model=List[str], - responses={200: {"description": "Return a list of supported bands."}}, + if output_type == MediaType.html: + return create_html_response( + request, + { + **tms.model_dump(exclude_none=True, mode="json"), + # For visualization purpose we add the tms bbox + "bbox": list(tms.bbox), + }, + title=f"{tileMatrixSetId} TileMatrixSet", + template_name="tilematrixset", + templates=self.templates, + ) + + return tms + + +@define(kw_only=True) +class AlgorithmFactory(BaseFactory): + """Algorithm endpoints Factory.""" + + # Supported algorithm + supported_algorithm: Algorithms = available_algorithms + + def _get_algo_metadata(self, algorithm: type[BaseAlgorithm]) -> AlgorithmMetadata: + """Algorithm Metadata""" + props = algorithm.model_json_schema()["properties"] + + # title and description + info = { + k: v["default"] + for k, v in props.items() + if k == "title" or k == "description" + } + title = info.get("title", None) + description = info.get("description", None) + + # Inputs Metadata + ins = { + k.replace("input_", ""): v["default"] + for k, v in props.items() + if k.startswith("input_") and "default" in v + } + + # Output Metadata + outs = { + k.replace("output_", ""): v["default"] + for k, v in props.items() + if k.startswith("output_") and "default" in v + } + + # Algorithm Parameters + params = { + k: v + for k, v in props.items() + if not k.startswith("input_") + and not k.startswith("output_") + and k != "title" + and k != "description" + } + return AlgorithmMetadata( + title=title, + description=description, + inputs=ins, + outputs=outs, + parameters=params, ) - def available_bands( - src_path=Depends(self.path_dependency), - reader_params=Depends(self.reader_dependency), - env=Depends(self.environment_dependency), - ): - """Return a list of supported bands.""" - with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return src_dst.bands - # Overwrite the `/statistics` endpoint because we need bands to default to the list of bands. - def statistics(self): # noqa: C901 - """add statistics endpoints.""" + def register_routes(self): + """Register Algorithm routes.""" - # GET endpoint @self.router.get( - "/statistics", - response_class=JSONResponse, - response_model=Statistics, + "/algorithms", + response_model=AlgorithmtList, + summary="Retrieve the list of available Algorithms.", + operation_id=f"{self.operation_prefix}getAlgorithmList", responses={ 200: { - "content": {"application/json": {}}, - "description": "Return dataset's statistics.", - } + "content": { + "application/json": {}, + "text/html": {}, + }, + }, }, ) - def statistics( - src_path=Depends(self.path_dependency), - bands_params=Depends(BandsExprParamsOptional), - dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), - stats_params=Depends(self.stats_dependency), - histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), - env=Depends(self.environment_dependency), + def available_algorithms( + request: Request, + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, ): - """Get Dataset statistics.""" - with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return src_dst.statistics( - **bands_params, - **image_params, - **dataset_params, - **stats_params, - hist_options={**histogram_params}, - ) + """Retrieve the list of available Algorithms.""" + data = AlgorithmtList( + algorithms=[ + { # type: ignore + "id": algo_id, + "links": [ + { + "href": self.url_for( + request, + "algorithm_metadata", + algorithmId=algo_id, + ), + "rel": "item", + "type": "application/json", + "title": f"Definition of {algo_id} Algorithm", + } + ], + } + for algo_id, _ in self.supported_algorithm.data.items() + ], + ) - # POST endpoint - @self.router.post( - "/statistics", - response_model=StatisticsGeoJSON, - response_model_exclude_none=True, - response_class=GeoJSONResponse, + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) + + if output_type == MediaType.html: + return create_html_response( + request, + data.model_dump(exclude_none=True, mode="json"), + title="Algorithms", + template_name="algorithms", + templates=self.templates, + ) + + return data + + @self.router.get( + "/algorithms/{algorithmId}", + response_model=AlgorithmMetadata, + summary="Retrieve the metadata of the specified algorithm.", + operation_id=f"{self.operation_prefix}getAlgorithm", responses={ 200: { - "content": {"application/json": {}}, - "description": "Return dataset's statistics.", - } + "content": { + "application/json": {}, + "text/html": {}, + }, + }, }, ) - def geojson_statistics( - geojson: Union[FeatureCollection, Feature] = Body( - ..., description="GeoJSON Feature or FeatureCollection." - ), - src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), - bands_params=Depends(BandsExprParamsOptional), - dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), - stats_params=Depends(self.stats_dependency), - histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), - env=Depends(self.environment_dependency), + def algorithm_metadata( + request: Request, + algorithmId: Annotated[ + Literal[tuple(self.supported_algorithm.list())], + Path(description="Algorithm name"), + ], + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, ): - """Get Statistics from a geojson feature or featureCollection.""" - fc = geojson - if isinstance(fc, Feature): - fc = FeatureCollection(type="FeatureCollection", features=[geojson]) - - with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - # Default to all available bands - if not bands_params.bands and not bands_params.expression: - bands_params.bands = src_dst.bands - - for feature in fc: - data = src_dst.feature( - feature.dict(exclude_none=True), - shape_crs=coord_crs or WGS84_CRS, - **bands_params, - **image_params, - **dataset_params, - ) - stats = get_array_statistics( - data.as_masked(), - **stats_params, - **histogram_params, - ) + """Retrieve the metadata of the specified algorithm.""" + data = self._get_algo_metadata(self.supported_algorithm.get(algorithmId)) + + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) - feature.properties = feature.properties or {} - feature.properties.update( - { - "statistics": { - f"{data.band_names[ix]}": BandStatistics( - **stats[ix] - ) - for ix in range(len(stats)) - } - } - ) + if output_type == MediaType.html: + return create_html_response( + request, + data.model_dump(exclude_none=True, mode="json"), + title=f"{algorithmId} Algorithm", + template_name="algorithm", + templates=self.templates, + ) - return fc.features[0] if isinstance(geojson, Feature) else fc + return data -@dataclass -class TMSFactory: - """TileMatrixSet endpoints Factory.""" +@define(kw_only=True) +class ColorMapFactory(BaseFactory): + """Colormap endpoints Factory.""" - supported_tms: TileMatrixSets = morecantile_tms + # Supported colormaps + supported_colormaps: ColorMaps = default_cmap - # FastAPI router - router: APIRouter = field(default_factory=APIRouter) + def _image_from_colormap( + self, + cmap, + orientation: Literal["vertical", "horizontal"] | None = None, + width: int | None = None, + height: int | None = None, + ) -> ImageData: + """Create an image from a colormap.""" + orientation = orientation or "horizontal" + + if isinstance(cmap, Sequence): + values = [minv for ((minv, _), _) in cmap] + arr = numpy.array([values] * 20) + + if orientation == "vertical": + height = height or 256 if len(values) < 256 else len(values) + else: + width = width or 256 if len(values) < 256 else len(values) + + ############################################################### + # DISCRETE CMAP + elif len(cmap) != 256 or max(cmap) >= 256 or min(cmap) < 0: + values = list(cmap) + arr = numpy.array([values] * 20) + + if orientation == "vertical": + height = height or 256 if len(values) < 256 else len(values) + else: + width = width or 256 if len(values) < 256 else len(values) + + ############################################################### + # LINEAR CMAP + else: + cmin, cmax = min(cmap), max(cmap) + arr = numpy.array( + [numpy.round(numpy.linspace(cmin, cmax, num=256)).astype(numpy.uint8)] + * 20 + ) - # Router Prefix is needed to find the path for /tile if the TilerFactory.router is mounted - # with other router (multiple `.../tile` routes). - # e.g if you mount the route with `/cog` prefix, set router_prefix to cog and - router_prefix: str = "" + if orientation == "vertical": + arr = arr.transpose([1, 0]) - def __post_init__(self): - """Post Init: register route and configure specific options.""" - self.register_routes() + img = ImageData(arr) - def url_for(self, request: Request, name: str, **path_params: Any) -> str: - """Return full url (with prefix) for a specific endpoint.""" - url_path = self.router.url_path_for(name, **path_params) - base_url = str(request.base_url) - if self.router_prefix: - base_url += self.router_prefix.lstrip("/") + width = width or img.width + height = height or img.height + if width != img.width or height != img.height: + img = img.resize(height, width) - return str(url_path.make_absolute_url(base_url=base_url)) + return img - def register_routes(self): - """Register TMS endpoint routes.""" + def register_routes(self): # noqa: C901 + """Register ColorMap routes.""" @self.router.get( - r"/tileMatrixSets", - response_model=TileMatrixSetList, + "/colorMaps", + response_model=ColorMapList, response_model_exclude_none=True, - summary="Retrieve the list of available tiling schemes (tile matrix sets).", - operation_id="getTileMatrixSetsList", + summary="Retrieve the list of available colormaps.", + operation_id=f"{self.operation_prefix}getColorMapList", + responses={ + 200: { + "content": { + "application/json": {}, + "text/html": {}, + }, + }, + }, ) - async def TileMatrixSet_list(request: Request): - """ - OGC Specification: http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets - """ - return { - "tileMatrixSets": [ - { - "id": tms, - "title": tms, + def available_colormaps( + request: Request, + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, + ): + """Retrieve the list of available colormaps.""" + data = ColorMapList( + colormaps=[ + { # type: ignore + "id": cmap_name, "links": [ { "href": self.url_for( request, - "TileMatrixSet_info", - TileMatrixSetId=tms, + "colormap_metadata", + colorMapId=cmap_name, ), "rel": "item", "type": "application/json", + "title": f"Definition of {cmap_name} ColorMap", } ], } - for tms in self.supported_tms.list() - ] - } - - @self.router.get( - r"/tileMatrixSets/{TileMatrixSetId}", - response_model=TileMatrixSet, - response_model_exclude_none=True, - summary="Retrieve the definition of the specified tiling scheme (tile matrix set).", - operation_id="getTileMatrixSet", - ) - async def TileMatrixSet_info( - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Path( - ..., description="TileMatrixSet Name." + for cmap_name in self.supported_colormaps.list() + ], ) - ): - """ - OGC Specification: http://docs.opengeospatial.org/per/19-069.html#_tilematrixset - """ - return self.supported_tms.get(TileMatrixSetId) + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) -@dataclass -class AlgorithmFactory: - """Algorithm endpoints Factory.""" - - # Supported algorithm - supported_algorithm: Algorithms = available_algorithms + if output_type == MediaType.html: + return create_html_response( + request, + data.model_dump(exclude_none=True, mode="json"), + title="ColorMaps", + template_name="colormaps", + templates=self.templates, + ) - # FastAPI router - router: APIRouter = field(default_factory=APIRouter) + return data - def __post_init__(self): - """Post Init: register routes""" + @self.router.get( + "/colorMaps/{colorMapId}", + response_model=ColorMapType, + summary="Retrieve the colorMap metadata or image.", + operation_id=f"{self.operation_prefix}getColorMap", + responses={ + 200: { + "content": { + "application/json": {}, + "image/png": {}, + "image/jpeg": {}, + "image/jpg": {}, + "image/webp": {}, + "image/jp2": {}, + "image/tiff; application=geotiff": {}, + "application/x-binary": {}, + "text/html": {}, + } + }, + }, + ) + def colormap_metadata( # noqa: C901 + request: Request, + colorMapId: Annotated[ + Literal[tuple(self.supported_colormaps.list())], + Path(description="ColorMap name"), + ], + f: Annotated[ + ( + Literal[ + "html", + "json", + "png", + "npy", + "tif", + "tiff", + "jpeg", + "jpg", + "jp2", + "webp", + "pngraw", + ] + | None + ), + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, + orientation: Annotated[ + Literal["vertical", "horizontal"] | None, + Query( + description="Image Orientation.", + ), + ] = None, + height: Annotated[ + int | None, + Query( + description="Image Height (default to 20px for horizontal or 256px for vertical).", + ), + ] = None, + width: Annotated[ + int | None, + Query( + description="Image Width (default to 256px for horizontal or 20px for vertical).", + ), + ] = None, + ): + """Retrieve the metadata of the specified colormap.""" + cmap = self.supported_colormaps.get(colorMapId) + + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) - def metadata(algorithm: BaseAlgorithm) -> AlgorithmMetadata: - """Algorithm Metadata""" - props = algorithm.schema()["properties"] + if output_type.name in [im.name for im in ImageType]: + img = self._image_from_colormap( + cmap, + orientation, + width=width, + height=height, + ) - # Inputs Metadata - ins = { - k.replace("input_", ""): v["default"] - for k, v in props.items() - if k.startswith("input_") and "default" in v - } + format = ImageType[output_type.name] + return Response( + img.render(img_format=format.driver, colormap=cmap), + media_type=format.mediatype, + ) - # Output Metadata - outs = { - k.replace("output_", ""): v["default"] - for k, v in props.items() - if k.startswith("output_") and "default" in v - } + elif output_type == MediaType.html: + img = self._image_from_colormap(cmap, orientation="vertical") + content = img.render(img_format="PNG", colormap=cmap) - # Algorithm Parameters - params = { - k: v - for k, v in props.items() - if not k.startswith("input_") and not k.startswith("output_") - } - return AlgorithmMetadata(inputs=ins, outputs=outs, parameters=params) + return create_html_response( + request, + base64.b64encode(content).decode(), + title=colorMapId, + template_name="colormap", + templates=self.templates, + ) - @self.router.get( - "/algorithms", - response_model=Dict[str, AlgorithmMetadata], - summary="Retrieve the list of available Algorithms.", - operation_id="getAlgorithms", - ) - def available_algorithms(request: Request): - """Retrieve the list of available Algorithms.""" - return {k: metadata(v) for k, v in self.supported_algorithm.data.items()} + data: ColorMapType + if isinstance(cmap, Sequence): + data = [(k, numpy.array(v).tolist()) for (k, v) in cmap] + else: + data = {k: numpy.array(v).tolist() for k, v in cmap.items()} - @self.router.get( - "/algorithms/{algorithmId}", - response_model=AlgorithmMetadata, - summary="Retrieve the metadata of the specified algorithm.", - operation_id="getAlgorithm", - ) - def algorithm_metadata( - algorithm: Literal[tuple(self.supported_algorithm.list())] = Path( - ..., description="Algorithm name", alias="algorithmId" - ), - ): - """Retrieve the metadata of the specified algorithm.""" - return metadata(self.supported_algorithm.get(algorithm)) + return data diff --git a/src/titiler/core/titiler/core/middleware.py b/src/titiler/core/titiler/core/middleware.py index f592c962d..46ca2e7c4 100644 --- a/src/titiler/core/titiler/core/middleware.py +++ b/src/titiler/core/titiler/core/middleware.py @@ -1,38 +1,35 @@ """Titiler middlewares.""" +from __future__ import annotations + import logging import re import time -from typing import Optional, Set +from dataclasses import dataclass, field +from urllib.parse import urlencode -from fastapi.logger import logger from starlette.datastructures import MutableHeaders from starlette.requests import Request from starlette.types import ASGIApp, Message, Receive, Scope, Send +from titiler.core import telemetry + +@dataclass(frozen=True) class CacheControlMiddleware: - """MiddleWare to add CacheControl in response headers.""" - - def __init__( - self, - app: ASGIApp, - cachecontrol: Optional[str] = None, - cachecontrol_max_http_code: Optional[int] = 500, - exclude_path: Optional[Set[str]] = None, - ) -> None: - """Init Middleware. - - Args: - app (ASGIApp): starlette/FastAPI application. - cachecontrol (str): Cache-Control string to add to the response. - exclude_path (set): Set of regex expression to use to filter the path. - - """ - self.app = app - self.cachecontrol = cachecontrol - self.cachecontrol_max_http_code = cachecontrol_max_http_code - self.exclude_path = exclude_path or set() + """MiddleWare to add CacheControl in response headers. + + Args: + app (ASGIApp): starlette/FastAPI application. + cachecontrol (str): Cache-Control string to add to the response. + exclude_path (set): Set of regex expression to use to filter the path. + + """ + + app: ASGIApp + cachecontrol: str | None = None + cachecontrol_max_http_code: int = 500 + exclude_path: set[str] = field(default_factory=set) async def __call__(self, scope: Scope, receive: Receive, send: Send): """Handle call.""" @@ -49,10 +46,7 @@ async def send_wrapper(message: Message): scope["method"] in ["HEAD", "GET"] and message["status"] < self.cachecontrol_max_http_code and not any( - [ - re.match(path, scope["path"]) - for path in self.exclude_path - ] + re.match(path, scope["path"]) for path in self.exclude_path ) ): response_headers["Cache-Control"] = self.cachecontrol @@ -62,17 +56,16 @@ async def send_wrapper(message: Message): await self.app(scope, receive, send_wrapper) +@dataclass(frozen=True) class TotalTimeMiddleware: - """MiddleWare to add Total process time in response headers.""" + """MiddleWare to add Total process time in response headers. - def __init__(self, app: ASGIApp) -> None: - """Init Middleware. + Args: + app (ASGIApp): starlette/FastAPI application. - Args: - app (ASGIApp): starlette/FastAPI application. + """ - """ - self.app = app + app: ASGIApp async def __call__(self, scope: Scope, receive: Receive, send: Send): """Handle call.""" @@ -84,6 +77,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send): async def send_wrapper(message: Message): """Send Message.""" + nonlocal start_time + if message["type"] == "http.response.start": response_headers = MutableHeaders(scope=message) process_time = time.time() - start_time @@ -99,70 +94,82 @@ async def send_wrapper(message: Message): await self.app(scope, receive, send_wrapper) +@dataclass(frozen=True) class LoggerMiddleware: """MiddleWare to add logging.""" - def __init__( - self, - app: ASGIApp, - querystrings: bool = False, - headers: bool = False, - ) -> None: - """Init Middleware. - - Args: - app (ASGIApp): starlette/FastAPI application. - - """ - self.app = app - self.querystrings = querystrings - self.headers = headers - self.logger = logger - logger.setLevel(logging.DEBUG) + app: ASGIApp + logger: logging.Logger = field( + default_factory=lambda: logging.getLogger("titiler.requests") + ) async def __call__(self, scope: Scope, receive: Receive, send: Send): """Handle call.""" - if scope["type"] == "http": - request = Request(scope) - - self.logger.debug(str(request.url)) + if scope["type"] != "http": + return await self.app(scope, receive, send) + + request = Request(scope, receive=receive) + data = { + "http.method": request.method, + "http.url": str(request.url), + "http.scheme": request.url.scheme, + "http.host": request.headers.get("host", request.url.hostname or "unknown"), + "http.target": request.url.path + + (f"?{request.url.query}" if request.url.query else ""), + "http.user_agent": request.headers.get("user-agent"), + "http.referer": next( + (request.headers.get(attr) for attr in ["referer", "referrer"]), + None, + ), + "http.request.header.content-length": request.headers.get("content-length"), + "http.request.header.accept-encoding": request.headers.get( + "accept-encoding" + ), + "http.request.header.origin": request.headers.get("origin"), + "net.host.name": request.url.hostname, + "net.host.port": request.url.port, + "titiler.query_params": dict(request.query_params), + } + + telemetry.add_span_attributes(telemetry.flatten_dict(data)) + + exception: Exception | None = None + try: + await self.app(scope, receive, send) + except Exception as e: + exception = e - qs = dict(request.query_params) - if qs and self.querystrings: - self.logger.debug(qs) + if route := scope.get("route"): + data["http.route"] = route.path - if self.headers: - self.logger.debug(dict(request.headers)) + data["titiler.path_params"] = request.path_params - await self.app(scope, receive, send) + self.logger.info( + f"Request received: {request.url.path} {request.method}", + extra=data, + ) + if exception: + raise exception +@dataclass(frozen=True) class LowerCaseQueryStringMiddleware: """Middleware to make URL parameters case-insensitive. taken from: https://github.com/tiangolo/fastapi/issues/826 - """ - def __init__(self, app: ASGIApp) -> None: - """Init Middleware. - - Args: - app (ASGIApp): starlette/FastAPI application. + """ - """ - self.app = app + app: ASGIApp async def __call__(self, scope: Scope, receive: Receive, send: Send): """Handle call.""" if scope["type"] == "http": request = Request(scope) - DECODE_FORMAT = "latin-1" - - query_string = "" - for k, v in request.query_params.multi_items(): - query_string += k.lower() + "=" + v + "&" - - query_string = query_string[:-1] + query_items = [ + (k.lower(), v) for k, v in request.query_params.multi_items() + ] + query_string = urlencode(query_items, doseq=True) request.scope["query_string"] = query_string.encode(DECODE_FORMAT) await self.app(scope, receive, send) diff --git a/src/titiler/core/titiler/core/models/OGC.py b/src/titiler/core/titiler/core/models/OGC.py index 6b735dff2..c09be7779 100644 --- a/src/titiler/core/titiler/core/models/OGC.py +++ b/src/titiler/core/titiler/core/models/OGC.py @@ -1,48 +1,663 @@ """OGC models.""" +from datetime import datetime +from typing import Annotated, Literal -from typing import List +from morecantile.models import CRSType +from pydantic import AnyUrl, BaseModel, Field, RootModel -from pydantic import AnyHttpUrl, BaseModel +from titiler.core.models.common import Link -class TileMatrixSetLink(BaseModel): +class TileMatrixSetRef(BaseModel): """ - TileMatrixSetLink model. + TileMatrixSetRef model. Based on http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets + """ + + id: str + title: str | None = None + links: list[Link] + + +class TileMatrixSetList(BaseModel): + """ + TileMatrixSetList model. + Based on http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets """ - href: AnyHttpUrl - rel: str = "item" - type: str = "application/json" + tileMatrixSets: list[TileMatrixSetRef] - class Config: - """Config for model.""" - use_enum_values = True +class TimeStamp(RootModel): + """TimeStamp model. + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/common-geodata/timeStamp.yaml -class TileMatrixSetRef(BaseModel): + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ """ - TileMatrixSetRef model. - Based on http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets + root: Annotated[ + datetime, + Field( + json_schema_extra={ + "description": "This property indicates the time and date when the response was generated using RFC 3339 notation.", + "examples": ["2017-08-17T08:05:32Z"], + } + ), + ] + +class BoundingBox(BaseModel): + """BoundingBox model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/2DBoundingBox.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ """ - id: str - title: str - links: List[TileMatrixSetLink] + lowerLeft: Annotated[ + list[float], + Field( + max_length=2, + min_length=2, + json_schema_extra={ + "description": "A 2D Point in the CRS indicated elsewhere", + }, + ), + ] + upperRight: Annotated[ + list[float], + Field( + max_length=2, + min_length=2, + json_schema_extra={ + "description": "A 2D Point in the CRS indicated elsewhere", + }, + ), + ] + crs: Annotated[CRSType | None, Field(json_schema_extra={"title": "CRS"})] = None + orderedAxes: Annotated[list[str] | None, Field(max_length=2, min_length=2)] = None -class TileMatrixSetList(BaseModel): +# Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml +Type = Literal["array", "boolean", "integer", "null", "number", "object", "string"] + +# Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml +AccessConstraints = Literal[ + "unclassified", "restricted", "confidential", "secret", "topSecret" +] + + +class Properties(BaseModel): + """Properties model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ """ - TileMatrixSetList model. - Based on http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets + title: str | None = None + description: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Implements 'description'", + } + ), + ] = None + type: Type | None = None + enum: Annotated[ + set | None, + Field( + min_length=1, + json_schema_extra={ + "description": "Implements 'acceptedValues'", + }, + ), + ] = None + format: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Complements implementation of 'type'", + } + ), + ] = None + contentMediaType: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Implements 'mediaType'", + } + ), + ] = None + maximum: Annotated[ + float | None, + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + exclusiveMaximum: Annotated[ + float | None, + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + minimum: Annotated[ + float | None, + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + exclusiveMinimum: Annotated[ + float | None, + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + pattern: str | None = None + maxItems: Annotated[ + int | None, + Field( + ge=0, + json_schema_extra={ + "description": "Implements 'upperMultiplicity'", + }, + ), + ] = None + minItems: Annotated[ + int | None, + Field( + ge=0, + json_schema_extra={ + "description": "Implements 'lowerMultiplicity'", + }, + ), + ] = 0 + observedProperty: str | None = None + observedPropertyURI: AnyUrl | None = None + uom: str | None = None + uomURI: AnyUrl | None = None + + +class PropertiesSchema(BaseModel): + """PropertiesSchema model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + type: Literal["object"] + required: Annotated[ + list[str] | None, + Field( + min_length=1, + json_schema_extra={ + "description": "Implements 'multiplicity' by citing property 'name' defined as 'additionalProperties'", + }, + ), + ] = None + properties: dict[str, Properties] + + +class Style(BaseModel): + """Style model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/style.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + id: Annotated[ + str, + Field( + json_schema_extra={ + "description": "An identifier for this style. Implementation of 'identifier'", + } + ), + ] + title: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "A title for this style", + } + ), + ] = None + description: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Brief narrative description of this style", + } + ), + ] = None + keywords: Annotated[ + list[str] | None, + Field( + json_schema_extra={ + "description": "keywords about this style", + } + ), + ] = None + links: Annotated[ + list[Link] | None, + Field( + min_length=1, + json_schema_extra={ + "description": "Links to style related resources. Possible link 'rel' values are: 'style' for a URL pointing to the style description, 'styleSpec' for a URL pointing to the specification or standard used to define the style.", + }, + ), + ] = None + + +class GeospatialData(BaseModel): + """Geospatial model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/geospatialData.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + title: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Title of this tile matrix set, normally used for display to a human", + } + ), + ] = None + description: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Brief narrative description of this tile matrix set, normally available for display to a human", + } + ), + ] = None + keywords: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this layer", + } + ), + ] = None + id: Annotated[ + str, + Field( + json_schema_extra={ + "description": "Unique identifier of the Layer. Implementation of 'identifier'", + } + ), + ] + dataType: Annotated[ + Literal["map", "vector", "coverage"], + Field( + json_schema_extra={ + "description": "Type of data represented in the tileset", + } + ), + ] + geometryDimension: Annotated[ + int | None, + Field( # type: ignore + ge=0, + le=3, + json_schema_extra={ + "description": "The geometry dimension of the features shown in this layer (0: points, 1: curves, 2: surfaces, 3: solids), unspecified: mixed or unknown", + }, + ), + ] = None + featureType: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Feature type identifier. Only applicable to layers of datatype 'geometries'", + } + ), + ] = None + attribution: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Short reference to recognize the author or provider", + } + ), + ] = None + license: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "License applicable to the tiles", + } + ), + ] = None + pointOfContact: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Useful information to contact the authors or custodians for the layer (e.g. e-mail address, a physical address, phone numbers, etc)", + } + ), + ] = None + publisher: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Organization or individual responsible for making the layer available", + } + ), + ] = None + theme: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Category where the layer can be grouped", + } + ), + ] = None + crs: Annotated[CRSType | None, Field(json_schema_extra={"title": "CRS"})] = None + epoch: Annotated[ + float | None, + Field( + json_schema_extra={ + "description": "Epoch of the Coordinate Reference System (CRS)", + } + ), + ] = None + minScaleDenominator: Annotated[ + float | None, + Field( + json_schema_extra={ + "description": "Minimum scale denominator for usage of the layer", + } + ), + ] = None + maxScaleDenominator: Annotated[ + float | None, + Field( + json_schema_extra={ + "description": "Maximum scale denominator for usage of the layer", + } + ), + ] = None + minCellSize: Annotated[ + float | None, + Field( + json_schema_extra={ + "description": "Minimum cell size for usage of the layer", + } + ), + ] = None + maxCellSize: Annotated[ + float | None, + Field( + json_schema_extra={ + "description": "Maximum cell size for usage of the layer", + } + ), + ] = None + maxTileMatrix: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "TileMatrix identifier associated with the minScaleDenominator", + } + ), + ] = None + minTileMatrix: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "TileMatrix identifier associated with the maxScaleDenominator", + } + ), + ] = None + boundingBox: BoundingBox | None = None + created: TimeStamp | None = None + updated: TimeStamp | None = None + style: Style | None = None + geoDataClasses: Annotated[ + list[str] | None, + Field( + json_schema_extra={ + "description": "URI identifying a class of data contained in this layer (useful to determine compatibility with styles or processes)", + } + ), + ] = None + propertiesSchema: PropertiesSchema | None = None + links: Annotated[ + list[Link] | None, + Field( + min_length=1, + json_schema_extra={ + "description": "Links related to this layer. Possible link 'rel' values are: 'geodata' for a URL pointing to the collection of geospatial data.", + }, + ), + ] = None + + +class TilePoint(BaseModel): + """TilePoint model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/tilePoint.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + coordinates: Annotated[list[float], Field(max_length=2, min_length=2)] + crs: Annotated[CRSType | None, Field(json_schema_extra={"title": "CRS"})] + tileMatrix: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "TileMatrix identifier associated with the scaleDenominator", + } + ), + ] = None + scaleDenominator: Annotated[ + float | None, + Field( + json_schema_extra={ + "description": "Scale denominator of the tile matrix selected", + } + ), + ] = None + cellSize: Annotated[ + float | None, + Field( + json_schema_extra={ + "description": "Cell size of the tile matrix selected", + } + ), + ] = None + + +class TileMatrixLimits(BaseModel): + """ + The limits for an individual tile matrix of a TileSet's TileMatrixSet, as defined in the OGC 2D TileMatrixSet and TileSet Metadata Standard + + Based on https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/tileMatrixLimits.yaml + """ + + tileMatrix: str + minTileRow: Annotated[int, Field(ge=0)] + maxTileRow: Annotated[int, Field(ge=0)] + minTileCol: Annotated[int, Field(ge=0)] + maxTileCol: Annotated[int, Field(ge=0)] + + +class TileSet(BaseModel): + """ + TileSet model. + + Based on https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/tileSet.yaml + """ + + title: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "A title for this tileset", + } + ), + ] = None + description: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Brief narrative description of this tile set", + } + ), + ] = None + dataType: Annotated[ + Literal["map", "vector", "coverage"], + Field( + json_schema_extra={ + "description": "Type of data represented in the tileset", + } + ), + ] + crs: Annotated[CRSType, Field(json_schema_extra={"title": "CRS"})] + tileMatrixSetURI: Annotated[ + AnyUrl | None, + Field( + json_schema_extra={ + "description": "Reference to a Tile Matrix Set on an official source for Tile Matrix Sets", + } + ), + ] = None + links: Annotated[ + list[Link], + Field( + json_schema_extra={ + "description": "Links to related resources", + } + ), + ] + tileMatrixSetLimits: Annotated[ + list[TileMatrixLimits] | None, + Field( + json_schema_extra={ + "description": "Limits for the TileRow and TileCol values for each TileMatrix in the tileMatrixSet. If missing, there are no limits other that the ones imposed by the TileMatrixSet. If present the TileMatrices listed are limited and the rest not available at all", + } + ), + ] = None + epoch: Annotated[ + float | int | None, + Field( + json_schema_extra={ + "description": "Epoch of the Coordinate Reference System (CRS)", + } + ), + ] = None + layers: Annotated[ + list[GeospatialData] | None, + Field(min_length=1), + ] = None + boundingBox: BoundingBox | None = None + centerPoint: TilePoint | None = None + style: Style | None = None + attribution: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Short reference to recognize the author or provider", + } + ), + ] = None + license: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "License applicable to the tiles", + } + ), + ] = None + accessConstraints: Annotated[ + AccessConstraints | None, + Field( + json_schema_extra={ + "description": "Restrictions on the availability of the Tile Set that the user needs to be aware of before using or redistributing the Tile Set", + } + ), + ] = "unclassified" + keywords: Annotated[ + list[str] | None, + Field( + json_schema_extra={ + "description": "keywords about this tileset", + } + ), + ] = None + version: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Version of the Tile Set. Changes if the data behind the tiles has been changed", + } + ), + ] = None + created: TimeStamp | None = None + updated: TimeStamp | None = None + pointOfContact: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Useful information to contact the authors or custodians for the Tile Set", + } + ), + ] = None + mediaTypes: Annotated[ + list[str] | None, + Field( + json_schema_extra={ + "description": "Media types available for the tiles", + } + ), + ] = None + + +class TileSetList(BaseModel): + """ + TileSetList model. + + Based on https://docs.ogc.org/is/20-057/20-057.html#toc34 + """ + + tilesets: list[TileSet] + + +class Conformance(BaseModel): + """Conformance model. + + Ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/confClasses.yaml + + """ + + conformsTo: list[str] + + +class Landing(BaseModel): + """Landing page model. + + Ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/landingPage.yaml """ - tileMatrixSets: List[TileMatrixSetRef] + title: str | None = None + description: str | None = None + links: list[Link] diff --git a/src/titiler/core/titiler/core/models/common.py b/src/titiler/core/titiler/core/models/common.py new file mode 100644 index 000000000..56477e14c --- /dev/null +++ b/src/titiler/core/titiler/core/models/common.py @@ -0,0 +1,80 @@ +"""titiler.core.models.common""" + +from typing import Annotated + +from pydantic import AnyUrl, BaseModel, Field + + +class Link(BaseModel): + """Link model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/common-core/link.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + href: Annotated[ + AnyUrl, + Field( + json_schema_extra={ + "description": "Supplies the URI to a remote resource (or resource fragment).", + "examples": ["http://data.example.com/buildings/123"], + } + ), + ] + rel: Annotated[ + str, + Field( + json_schema_extra={ + "description": "The type or semantics of the relation.", + "examples": ["alternate"], + } + ), + ] + type: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "A hint indicating what the media type of the result of dereferencing the link should be.", + "examples": ["application/geo+json"], + } + ), + ] = None + templated: Annotated[ + bool | None, + Field( + json_schema_extra={ + "description": "This flag set to true if the link is a URL template.", + } + ), + ] = None + varBase: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "A base path to retrieve semantic information about the variables used in URL template.", + "examples": ["/ogcapi/vars/"], + } + ), + ] = None + hreflang: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "A hint indicating what the language of the result of dereferencing the link should be.", + "examples": ["en"], + } + ), + ] = None + title: Annotated[ + str | None, + Field( + json_schema_extra={ + "description": "Used to label the destination of a link such that it can be used as a human-readable identifier.", + "examples": ["Trierer Strasse 70, 53115 Bonn"], + } + ), + ] = None + length: int | None = None + + model_config = {"use_enum_values": True} diff --git a/src/titiler/core/titiler/core/models/mapbox.py b/src/titiler/core/titiler/core/models/mapbox.py index 755ce6901..1cdf3d2e7 100644 --- a/src/titiler/core/titiler/core/models/mapbox.py +++ b/src/titiler/core/titiler/core/models/mapbox.py @@ -1,55 +1,56 @@ """Common response models.""" -from enum import Enum -from typing import List, Optional, Tuple +from typing import Annotated, Literal -from pydantic import BaseModel, Field, root_validator +from pydantic import BaseModel, Field, model_validator -class SchemeEnum(str, Enum): - """TileJSON scheme choice.""" +class LayerJSON(BaseModel): + """ + https://github.com/mapbox/tilejson-spec/tree/master/3.0.0#33-vector_layers + """ - xyz = "xyz" - tms = "tms" + id: str + fields: Annotated[dict, Field(default_factory=dict)] + description: str | None = None + minzoom: int | None = None + maxzoom: int | None = None -class TileJSON(BaseModel): +class TileJSON(BaseModel, extra="allow"): """ TileJSON model. - Based on https://github.com/mapbox/tilejson-spec/tree/master/2.2.0 + Based on https://github.com/mapbox/tilejson-spec/tree/master/3.0.0 """ - tilejson: str = "2.2.0" - name: Optional[str] - description: Optional[str] + tilejson: str = "3.0.0" + name: str | None = None + description: str | None = None version: str = "1.0.0" - attribution: Optional[str] - template: Optional[str] - legend: Optional[str] - scheme: SchemeEnum = SchemeEnum.xyz - tiles: List[str] - grids: Optional[List[str]] - data: Optional[List[str]] - minzoom: int = Field(0, ge=0, le=30) - maxzoom: int = Field(30, ge=0, le=30) - bounds: List[float] = [-180, -90, 180, 90] - center: Optional[Tuple[float, float, int]] - - @root_validator - def compute_center(cls, values): + attribution: str | None = None + template: str | None = None + legend: str | None = None + scheme: Literal["xyz", "tms"] = "xyz" + tiles: list[str] + vector_layers: list[LayerJSON] | None = None + grids: list[str] | None = None + data: list[str] | None = None + minzoom: int = Field(0) + maxzoom: int = Field(30) + fillzoom: int | None = None + bounds: list[float] = [-180, -85.0511287798066, 180, 85.0511287798066] + center: tuple[float, float, int] | None = None + + @model_validator(mode="after") + def compute_center(self): """Compute center if it does not exist.""" - bounds = values["bounds"] - if not values.get("center"): - values["center"] = ( + bounds = self.bounds + if not self.center: + self.center = ( (bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, - values["minzoom"], + self.minzoom, ) - return values - - class Config: - """TileJSON model configuration.""" - - use_enum_values = True + return self diff --git a/src/titiler/core/titiler/core/models/responses.py b/src/titiler/core/titiler/core/models/responses.py index 7932c02e8..cbf818f55 100644 --- a/src/titiler/core/titiler/core/models/responses.py +++ b/src/titiler/core/titiler/core/models/responses.py @@ -1,12 +1,12 @@ """TiTiler response models.""" -from typing import Dict, List, Union - from geojson_pydantic.features import Feature, FeatureCollection -from geojson_pydantic.geometries import Geometry, Polygon +from geojson_pydantic.geometries import Geometry, MultiPolygon, Polygon from pydantic import BaseModel from rio_tiler.models import BandStatistics, Info +from titiler.core.models.common import Link + class Point(BaseModel): """ @@ -16,13 +16,14 @@ class Point(BaseModel): """ - coordinates: List[float] - values: List[float] - band_names: List[str] + coordinates: list[float] + values: list[float | None] + band_names: list[str] + band_descriptions: list[str] | None = None -InfoGeoJSON = Feature[Polygon, Info] -Statistics = Dict[str, BandStatistics] +InfoGeoJSON = Feature[Polygon | MultiPolygon, Info] +Statistics = dict[str, BandStatistics] class StatisticsInGeoJSON(BaseModel): @@ -30,23 +31,31 @@ class StatisticsInGeoJSON(BaseModel): statistics: Statistics - class Config: - """Config for model.""" - - extra = "allow" + model_config = {"extra": "allow"} -StatisticsGeoJSON = Union[ - FeatureCollection[Geometry, StatisticsInGeoJSON], - Feature[Geometry, StatisticsInGeoJSON], -] +StatisticsGeoJSON = ( + FeatureCollection[Feature[Geometry, StatisticsInGeoJSON]] + | Feature[Geometry, StatisticsInGeoJSON] +) # MultiBase Models -MultiBaseInfo = Dict[str, Info] -MultiBaseInfoGeoJSON = Feature[Polygon, MultiBaseInfo] - -MultiBaseStatistics = Dict[str, Statistics] -MultiBaseStatisticsGeoJSON = Union[ - FeatureCollection[Geometry, StatisticsInGeoJSON], - Feature[Geometry, StatisticsInGeoJSON], -] +MultiBaseInfo = dict[str, Info] +MultiBaseInfoGeoJSON = Feature[Polygon | MultiPolygon, MultiBaseInfo] + +MultiBaseStatistics = dict[str, Statistics] +MultiBaseStatisticsGeoJSON = StatisticsGeoJSON + + +class ColorMapRef(BaseModel): + """ColorMapRef model.""" + + id: str + title: str | None = None + links: list[Link] + + +class ColorMapList(BaseModel): + """Model for colormap list.""" + + colormaps: list[ColorMapRef] diff --git a/src/titiler/core/titiler/core/py.typed b/src/titiler/core/titiler/core/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/titiler/core/titiler/core/resources/enums.py b/src/titiler/core/titiler/core/resources/enums.py index 1f0fbfa32..11915cbc5 100644 --- a/src/titiler/core/titiler/core/resources/enums.py +++ b/src/titiler/core/titiler/core/resources/enums.py @@ -10,6 +10,7 @@ class MediaType(str, Enum): """Responses Media types formerly known as MIME types.""" tif = "image/tiff; application=geotiff" + tiff = "image/tiff; application=geotiff" jp2 = "image/jp2" png = "image/png" pngraw = "image/png" @@ -23,7 +24,14 @@ class MediaType(str, Enum): html = "text/html" text = "text/plain" pbf = "application/x-protobuf" - mvt = "application/x-protobuf" + mvt = "application/vnd.mapbox-vector-tile" + ndjson = "application/ndjson" + geojsonseq = "application/geo+json-seq" + schemajson = "application/schema+json" + csv = "text/csv" + openapi30_json = "application/vnd.oai.openapi+json;version=3.0" + openapi30_yaml = "application/vnd.oai.openapi;version=3.0" + gif = "image/gif" class ImageDriver(str, Enum): @@ -34,9 +42,11 @@ class ImageDriver(str, Enum): png = "PNG" pngraw = "PNG" tif = "GTiff" + tiff = "GTiff" webp = "WEBP" jp2 = "JP2OpenJPEG" npy = "NPY" + gif = "GIF" class ImageType(str, Enum): @@ -45,6 +55,7 @@ class ImageType(str, Enum): png = "png" npy = "npy" tif = "tif" + tiff = "tiff" jpeg = "jpeg" jpg = "jpg" jp2 = "jp2" diff --git a/src/titiler/core/titiler/core/resources/responses.py b/src/titiler/core/titiler/core/resources/responses.py index cd37a6a85..3a99d6be5 100644 --- a/src/titiler/core/titiler/core/resources/responses.py +++ b/src/titiler/core/titiler/core/resources/responses.py @@ -2,6 +2,7 @@ from typing import Any +import numpy import simplejson as json from starlette import responses @@ -12,6 +13,16 @@ class XMLResponse(responses.Response): media_type = "application/xml" +class NumpyEncoder(json.JSONEncoder): + """Custom JSON Encoder.""" + + def default(self, obj): + """Catch numpy types and convert them.""" + if isinstance(obj, (numpy.ndarray, numpy.generic)): + return obj.tolist() + return super().default(obj) + + class JSONResponse(responses.JSONResponse): """Custom JSON Response.""" @@ -27,6 +38,7 @@ def render(self, content: Any) -> bytes: indent=None, ignore_nan=True, separators=(",", ":"), + cls=NumpyEncoder, ).encode("utf-8") diff --git a/src/titiler/core/titiler/core/routing.py b/src/titiler/core/titiler/core/routing.py index 28b66d6ec..f6a443c38 100644 --- a/src/titiler/core/titiler/core/routing.py +++ b/src/titiler/core/titiler/core/routing.py @@ -1,58 +1,11 @@ """Custom routing classes.""" -import sys -import warnings -from typing import Callable, Dict, List, Optional, Type +from typing import TypedDict -import rasterio from fastapi import params from fastapi.dependencies.utils import get_parameterless_sub_dependant -from fastapi.routing import APIRoute -from starlette.requests import Request -from starlette.responses import Response from starlette.routing import BaseRoute, Match -if sys.version_info >= (3, 8): - from typing import TypedDict # pylint: disable=no-name-in-module -else: - from typing_extensions import TypedDict - - -def apiroute_factory(env: Optional[Dict] = None) -> Type[APIRoute]: - """ - Create Custom API Route class with custom Env. - - Because we cannot create middleware for specific router we need to create - a custom APIRoute which add the `rasterio.Env(` block before the endpoint is - actually called. This way we set the env outside the threads and we make sure - that event multithreaded Reader will get the environment set. - - Note: This has been tested in python 3.6 and 3.7 only. - - """ - warnings.warn( - "'apiroute_factory' has been deprecated and will be removed" - "in titiler 0.1.0. Please see `environment_dependency` option in endpoint factories.", - DeprecationWarning, - ) - - class EnvAPIRoute(APIRoute): - """Custom API route with env.""" - - config = env or {} - - def get_route_handler(self) -> Callable: - original_route_handler = super().get_route_handler() - - async def custom_route_handler(request: Request) -> Response: - with rasterio.Env(**self.config): - response: Response = await original_route_handler(request) - return response - - return custom_route_handler - - return EnvAPIRoute - class EndpointScope(TypedDict, total=False): """Define endpoint.""" @@ -61,14 +14,14 @@ class EndpointScope(TypedDict, total=False): # https://github.com/encode/starlette/blob/6af5c515e0a896cbf3f86ee043b88f6c24200bcf/starlette/types.py#L3 path: str method: str - type: Optional[str] # http or websocket + type: str | None # http or websocket def add_route_dependencies( - routes: List[BaseRoute], + routes: list[BaseRoute], *, - scopes: List[EndpointScope], - dependencies=List[params.Depends], + scopes: list[EndpointScope], + dependencies=list[params.Depends], ): """Add dependencies to routes. @@ -87,7 +40,8 @@ def add_route_dependencies( route.dependant.dependencies.insert( # type: ignore 0, get_parameterless_sub_dependant( - depends=depends, path=route.path_format # type: ignore + depends=depends, + path=route.path_format, # type: ignore ), ) diff --git a/src/titiler/core/titiler/core/telemetry.py b/src/titiler/core/titiler/core/telemetry.py new file mode 100644 index 000000000..6446edb81 --- /dev/null +++ b/src/titiler/core/titiler/core/telemetry.py @@ -0,0 +1,142 @@ +"""OpenTelemetry instrumentation for titiler.core.""" + +from __future__ import annotations + +import functools +import inspect +from collections.abc import Callable +from contextlib import contextmanager +from typing import Any, Iterator, ParamSpec, TypeVar + +from titiler.core import __version__ + +try: + from opentelemetry import trace + from opentelemetry.trace import Span, Status, StatusCode + + tracer = trace.get_tracer("titiler.core", __version__) +except ImportError: + trace = None # type: ignore + Span = None # type: ignore + Status = None # type: ignore + StatusCode = None # type: ignore + tracer = None # type: ignore + +P = ParamSpec("P") +R = TypeVar("R") + + +def add_span_attributes(attributes: dict[str, Any]) -> None: + """Adds attributes to the current active span.""" + if not tracer: + return + span = trace.get_current_span() + if span and span.is_recording(): + span.set_attributes(attributes) + + +def flatten_dict(d: dict, parent_key: str = "", sep: str = ".") -> dict: + """Flattens a nested dictionary for adding span attributes.""" + items = {} + for k, v in d.items(): + new_key = parent_key + sep + k if parent_key else k + if isinstance(v, dict): + items.update(flatten_dict(v, new_key, sep=sep)) + else: + if not isinstance(v, (str, bool, int, float)): + v = str(v) + items[new_key] = v + return items + + +class SpanWrapper: + """A wrapper class to safely handle an optional OpenTelemetry Span.""" + + def __init__(self, span: Span | None): + """Set the span""" + self._span = span + + def set_attributes(self, attributes: dict[str, Any]) -> None: + """Safely set attributes on the wrapped span if it exists.""" + if self._span: + self._span.set_attributes(attributes) + + def record_exception(self, exception: Exception) -> None: + """Safely record an exception on the wrapped span if it exists.""" + if self._span: + self._span.record_exception(exception) + + +@contextmanager +def operation_tracer( + operation_name: str, + attributes: dict[str, Any] | None = None, +) -> Iterator[SpanWrapper]: + """Context manager for creating granular child spans.""" + if not tracer: + yield SpanWrapper(None) + return + + with tracer.start_as_current_span(operation_name) as span: + wrapped_span = SpanWrapper(span) + if attributes: + wrapped_span.set_attributes(attributes) + try: + yield wrapped_span + span.set_status(Status(StatusCode.OK)) + except Exception as e: + span.set_status(Status(StatusCode.ERROR, str(e))) + raise + + +def _get_span_name(op_name: str, factory_instance: Any) -> str: + """Determine the span name using the factory class name.""" + if not factory_instance: + return op_name + class_name = factory_instance.__class__.__name__ + return f"{class_name}.{op_name}" + + +def factory_trace( + _func: Callable[P, Any] | None = None, + *, + factory_instance: Any | None = None, +) -> Any: + """A decorator for Factory methods that automatically handles tracing for factory methods""" + + def decorator(func: Callable[P, Any]) -> Callable[P, Any]: + if not tracer: + return func + + op_name = func.__name__ + + attributes = {} + if factory_instance: + if hasattr(factory_instance, "reader"): + attributes["reader"] = str(factory_instance.reader) + if hasattr(factory_instance, "backend"): + attributes["backend"] = str(factory_instance.backend) + + if inspect.iscoroutinefunction(func): + + @functools.wraps(func) + async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: + span_name = _get_span_name(op_name, factory_instance) + with operation_tracer(span_name, attributes=attributes): + return await func(*args, **kwargs) + + return async_wrapper + else: + + @functools.wraps(func) + def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: + span_name = _get_span_name(op_name, factory_instance) + with operation_tracer(span_name, attributes=attributes): + return func(*args, **kwargs) + + return sync_wrapper + + return decorator if _func is None else decorator(_func) + + +factory_trace.decorator_enabled = bool(tracer) # type: ignore [attr-defined] diff --git a/src/titiler/core/titiler/core/templates/algorithm.html b/src/titiler/core/titiler/core/templates/algorithm.html new file mode 100644 index 000000000..6bb9c8c06 --- /dev/null +++ b/src/titiler/core/titiler/core/templates/algorithm.html @@ -0,0 +1,66 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + + + +

{{ response.title}}

+

{{ response.description}}

+ +
+
+

Inputs

+ {% for k, v in response.inputs.items() %} +
  • {{ k }}: {{ v }}
  • + {% endfor %} + +

    Outputs

    + {% for k, v in response.outputs.items() %} +
  • {{ k }}: {{ v }}
  • + {% endfor %} + +

    Parameters

    + {% for k, v in response.parameters.items() %} +
  • {{ k }}:
  • + + + + + + + + + + + + + +
    TypeDefaultMinMax
    {{ v.type }}{{ v.default }}{{ v.minimum }}{{ v.maximum }}
    + {% endfor %} +
    +
    +{% include "footer.html" %} diff --git a/src/titiler/core/titiler/core/templates/algorithms.html b/src/titiler/core/titiler/core/templates/algorithms.html new file mode 100644 index 000000000..14a93e4f8 --- /dev/null +++ b/src/titiler/core/titiler/core/templates/algorithms.html @@ -0,0 +1,31 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

    {{ template.title }}

    + +

    This API implements algorithms that are listed below.

    + +
      +{% for algo in response.algorithms %} +
    • {{ algo.id }}
    • +{% endfor %} +
    + +{% include "footer.html" %} diff --git a/src/titiler/core/titiler/core/templates/base.html b/src/titiler/core/titiler/core/templates/base.html new file mode 100644 index 000000000..49764d1b2 --- /dev/null +++ b/src/titiler/core/titiler/core/templates/base.html @@ -0,0 +1,48 @@ + + + + {{ template.title }} + + + + + + + + + + +
    +
    + {% block content %}{% endblock %} + {% include "debug.html" %} +
    +
    + + diff --git a/src/titiler/core/titiler/core/templates/colormap.html b/src/titiler/core/titiler/core/templates/colormap.html new file mode 100644 index 000000000..2bf2b0e95 --- /dev/null +++ b/src/titiler/core/titiler/core/templates/colormap.html @@ -0,0 +1,37 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + + + +

    {{ template.title }}

    + +{{ template.title }} +
    +
    + +{% include "footer.html" %} diff --git a/src/titiler/core/titiler/core/templates/colormaps.html b/src/titiler/core/titiler/core/templates/colormaps.html new file mode 100644 index 000000000..778c45717 --- /dev/null +++ b/src/titiler/core/titiler/core/templates/colormaps.html @@ -0,0 +1,31 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

    {{ template.title }}

    + +

    This API implements ColorMaps that are listed below.

    + +
      +{% for cmap in response.colormaps %} +
    • {{ cmap.id }}
    • +{% endfor %} +
    + +{% include "footer.html" %} diff --git a/src/titiler/core/titiler/core/templates/debug.html b/src/titiler/core/titiler/core/templates/debug.html new file mode 100644 index 000000000..e69de29bb diff --git a/src/titiler/core/titiler/core/templates/footer.html b/src/titiler/core/titiler/core/templates/footer.html new file mode 100644 index 000000000..0519bcd8d --- /dev/null +++ b/src/titiler/core/titiler/core/templates/footer.html @@ -0,0 +1,16 @@ + {% include "debug.html" %} +
    +
    + +
    +
    + Created by +
    + + Development Seed + +
    +
    + + diff --git a/src/titiler/core/titiler/core/templates/header.html b/src/titiler/core/titiler/core/templates/header.html new file mode 100644 index 000000000..a3938c37a --- /dev/null +++ b/src/titiler/core/titiler/core/templates/header.html @@ -0,0 +1,23 @@ + + + + {{ template.title }} + + + + + + + + + + +
    +
    diff --git a/src/titiler/core/titiler/core/templates/icons/clock.html b/src/titiler/core/titiler/core/templates/icons/clock.html new file mode 100644 index 000000000..139df0679 --- /dev/null +++ b/src/titiler/core/titiler/core/templates/icons/clock.html @@ -0,0 +1 @@ + diff --git a/src/titiler/core/titiler/core/templates/icons/license.html b/src/titiler/core/titiler/core/templates/icons/license.html new file mode 100644 index 000000000..624f77ec2 --- /dev/null +++ b/src/titiler/core/titiler/core/templates/icons/license.html @@ -0,0 +1 @@ + diff --git a/src/titiler/core/titiler/core/templates/icons/tag.html b/src/titiler/core/titiler/core/templates/icons/tag.html new file mode 100644 index 000000000..de48df803 --- /dev/null +++ b/src/titiler/core/titiler/core/templates/icons/tag.html @@ -0,0 +1 @@ + diff --git a/src/titiler/core/titiler/core/templates/map.html b/src/titiler/core/titiler/core/templates/map.html index 700b2dae8..ef369d5ee 100644 --- a/src/titiler/core/titiler/core/templates/map.html +++ b/src/titiler/core/titiler/core/templates/map.html @@ -1,124 +1,305 @@ - - - - - TiTiler Map Viewer - - - - - - - - - -
    - + + + + + TiTiler Map Viewer + + + + + + + + +
    +
    +
    +
    + + Development Seed + +
    +
    - - + + diff --git a/src/titiler/core/titiler/core/templates/tilematrixset.html b/src/titiler/core/titiler/core/templates/tilematrixset.html new file mode 100644 index 000000000..e711f5137 --- /dev/null +++ b/src/titiler/core/titiler/core/templates/tilematrixset.html @@ -0,0 +1,95 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

    {{ response.title or response.id }}

    + +
    +
    +
      + {% for key in ['title', 'description', 'id', 'uri', 'crs', 'wellKnownScaleSet', 'orderedAxes'] %} + {% if response[key] %} +
    • {{ key }}: {{ response[key] }}
    • + {% endif %} + {% endfor %} + +
    • Nb TileMatrices: {{ response.tileMatrices|count }}
    • +
    • Min level: {{ response.tileMatrices[0].id }}
    • +
    • Max level: {{ response.tileMatrices[-1].id }}
    • +
    • Origin: {{ response.tileMatrices[0].pointOfOrigin }}
    • +
    • Tile size: {{ [response.tileMatrices[0].tileWidth, response.tileMatrices[0].tileHeight] }}
    • + +
    +
    +
    +
    + Loading... +
    +
    +
    + + +{% include "footer.html" %} diff --git a/src/titiler/core/titiler/core/templates/tilematrixsets.html b/src/titiler/core/titiler/core/templates/tilematrixsets.html new file mode 100644 index 000000000..4d6912da6 --- /dev/null +++ b/src/titiler/core/titiler/core/templates/tilematrixsets.html @@ -0,0 +1,32 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

    {{ template.title }}

    + +

    This API implements TileMatrixSets that are listed below.

    + +

    Links

    +
      +{% for tms in response.tileMatrixSets %} +
    • {{ tms.id }}
    • +{% endfor %} +
    + +{% include "footer.html" %} diff --git a/src/titiler/core/titiler/core/templates/tileset.html b/src/titiler/core/titiler/core/templates/tileset.html new file mode 100644 index 000000000..b6d7a05f9 --- /dev/null +++ b/src/titiler/core/titiler/core/templates/tileset.html @@ -0,0 +1,126 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

    {{ response.title}}

    + +
    +
    + +

    Properties

    +
  • DataType: {{ response.dataType }}
  • +
  • CRS: {{ response.crs }}
  • + +

    TileMatrixSet Limits

    + + + + + + + + + + {% for tms in response.tileMatrixSetLimits %} + + + + + + + + {% endfor %} +
    LevelminTileRowmaxTileRowminTileColmaxTileCol
    {{ tms.tileMatrix }}{{ tms.minTileRow }}{{ tms.maxTileRow }}{{ tms.minTileCol }}{{ tms.maxTileCol }}
    + +

    Links

    +
      + {% for link in response.links %} + {% if link.rel != 'self' %} +
    • {{ link.title }}
    • + {% endif %} + {% endfor %} +
    + +
    +
    +
    + Loading... +
    +
    +
    + + +{% include "footer.html" %} diff --git a/src/titiler/core/titiler/core/templates/tilesets.html b/src/titiler/core/titiler/core/templates/tilesets.html new file mode 100644 index 000000000..3cb8e153e --- /dev/null +++ b/src/titiler/core/titiler/core/templates/tilesets.html @@ -0,0 +1,31 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

    {{ template.title }} TileSet list

    + +

    Links

    + + + +{% include "footer.html" %} diff --git a/src/titiler/core/titiler/core/templates/wmts.xml b/src/titiler/core/titiler/core/templates/wmts.xml deleted file mode 100644 index a42f54095..000000000 --- a/src/titiler/core/titiler/core/templates/wmts.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - "{{ title }}" - OGC WMTS - 1.0.0 - - - - - - - - - RESTful - - - - - - - - - - - - - RESTful - - - - - - - - - - {{ title }} - {{ layer_name }} - {{ title }} - - {{ bounds[0] }} {{ bounds[1] }} - {{ bounds[2] }} {{ bounds[3] }} - - - {{ media_type }} - - {{ tms.identifier }} - - - - - {{ tms.identifier }} - {{ tms.crs.srs }} - {% for item in tileMatrix %} - {{ item | safe }} - {% endfor %} - - - - diff --git a/src/titiler/core/titiler/core/utils.py b/src/titiler/core/titiler/core/utils.py new file mode 100644 index 000000000..aa9716f00 --- /dev/null +++ b/src/titiler/core/titiler/core/utils.py @@ -0,0 +1,500 @@ +"""titiler.core utilities.""" + +from __future__ import annotations + +import re +import warnings +from collections.abc import Callable, Sequence +from typing import Any, TypedDict, TypeVar, cast +from urllib.parse import urlencode + +import numpy +import pyproj +import rasterio +from fastapi import FastAPI +from fastapi.datastructures import QueryParams +from fastapi.dependencies.utils import get_dependant, request_params_to_args +from geojson_pydantic.geometries import MultiPolygon, Polygon +from morecantile import TileMatrixSet +from rasterio.dtypes import dtype_ranges +from rio_tiler.colormap import apply_cmap +from rio_tiler.errors import InvalidDatatypeWarning +from rio_tiler.models import ImageData +from rio_tiler.types import BBox, ColorMapType, IntervalTuple +from rio_tiler.utils import linear_rescale, render +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Route, request_response +from starlette.templating import Jinja2Templates, _TemplateResponse + +from titiler.core.resources.enums import ImageType, MediaType + + +def rescale_array( + array: numpy.ndarray, + mask: numpy.ndarray, + in_range: Sequence[IntervalTuple], + out_range: Sequence[IntervalTuple] = ((0, 255),), + out_dtype: str | numpy.number = "uint8", +) -> numpy.ndarray: + """Rescale data array""" + if len(array.shape) < 3: + array = numpy.expand_dims(array, axis=0) + + nbands = array.shape[0] + if len(in_range) != nbands: + in_range = ((in_range[0]),) * nbands + + if len(out_range) != nbands: + out_range = ((out_range[0]),) * nbands + + for bdx in range(nbands): + array[bdx] = numpy.where( + mask[bdx], + linear_rescale( + array[bdx], in_range=in_range[bdx], out_range=out_range[bdx] + ), + 0, + ) + + return array.astype(out_dtype) + + +def render_image( # noqa: C901 + image: ImageData, + colormap: ColorMapType | None = None, + output_format: ImageType | None = None, + add_mask: bool = True, + rescale: Sequence[IntervalTuple] | None = None, + color_formula: str | None = None, + **kwargs: Any, +) -> tuple[bytes, str]: + """convert image data to file. + + This is adapted from https://github.com/cogeotiff/rio-tiler/blob/066878704f841a332a53027b74f7e0a97f10f4b2/rio_tiler/models.py#L698-L764 + """ + if rescale: + image.rescale(rescale) + + if color_formula: + image.apply_color_formula(color_formula) + + data, mask = image.data.copy(), image.mask.copy() + input_range = dtype_ranges[str(data.dtype)] + output_range = image.dataset_statistics or (input_range,) + + if colormap: + data, alpha_from_cmap = apply_cmap(data, colormap) + output_range = (dtype_ranges[str(data.dtype)],) + # Combine both Mask from dataset and Alpha band from Colormap + mask = numpy.where( + mask != input_range[0], alpha_from_cmap, output_range[0][0] + ).astype(data.dtype) + + # format-specific valid dtypes + format_dtypes = { + ImageType.png: ["uint8", "uint16"], + ImageType.jpeg: ["uint8"], + ImageType.jpg: ["uint8"], + ImageType.webp: ["uint8"], + ImageType.jp2: ["uint8", "int16", "uint16"], + } + + # If output_format is not set, we choose between JPEG and PNG + if not output_format: + # Check if any alpha value == min datatype value (== Masked) + is_masked = (mask == dtype_ranges[str(mask.dtype)][0]).any() + output_format = ImageType.png if is_masked else ImageType.jpeg + # For automatic format we make sure the output datatype + # will be the same for both JPEG and PNG + format_dtypes[ImageType.png] = ["uint8"] + + valid_dtypes = format_dtypes.get(output_format, []) + if valid_dtypes and data.dtype not in valid_dtypes: + warnings.warn( + f"Invalid type: `{data.dtype}` for the `{output_format}` driver. " + "Data will be rescaled using min/max type bounds or dataset_statistics.", + InvalidDatatypeWarning, + stacklevel=1, + ) + data = rescale_array(data, mask, in_range=output_range) + + creation_options = {**kwargs, **output_format.profile} + if output_format.driver == "GTiff": + if "transform" not in creation_options: + creation_options.update({"transform": image.transform}) + if "crs" not in creation_options and image.crs: + creation_options.update({"crs": image.crs}) + + if add_mask: + content = render( + data, + mask, + img_format=output_format.driver, + **creation_options, + ) + else: + content = render( + data, + img_format=output_format.driver, + **creation_options, + ) + + return content, output_format.mediatype + + +def bounds_to_geometry(bounds: BBox) -> Polygon | MultiPolygon: + """Convert bounds to geometry. + + Note: if bounds are crossing the dateline separation line, a MultiPolygon geometry will be returned. + + """ + if bounds[0] > bounds[2]: + pl = Polygon.from_bounds(-180, bounds[1], bounds[2], bounds[3]) + pr = Polygon.from_bounds(bounds[0], bounds[1], 180, bounds[3]) + return MultiPolygon( + type="MultiPolygon", + coordinates=[pl.coordinates, pr.coordinates], + ) + return Polygon.from_bounds(*bounds) + + +T = TypeVar("T") + +ValidParams = dict[str, Any] +Errors = list[Any] + + +def get_dependency_query_params( + dependency: Callable, + params: QueryParams | dict, +) -> tuple[ValidParams, Errors]: + """Check QueryParams for Query dependency. + + 1. `get_dependant` is used to get the query-parameters required by the `callable` + 2. we use `request_params_to_args` to construct arguments needed to call the `callable` + 3. we call the `callable` and catch any errors + + Important: We assume the `callable` in not a co-routine. + """ + dep = get_dependant(path="", call=dependency) + + qp = ( + QueryParams(urlencode(params, doseq=True)) + if isinstance(params, dict) + else params + ) + return request_params_to_args(dep.query_params, qp) + + +def deserialize_query_params( + dependency: Callable[..., T], params: QueryParams | dict +) -> tuple[T, Errors]: + """Deserialize QueryParams for given dependency. + + Parse params as query params and deserialize with dependency. + + Important: We assume the `callable` in not a co-routine. + """ + values, errors = get_dependency_query_params(dependency, params) + return dependency(**values), errors + + +def extract_query_params( + dependencies: list[Callable], + params: QueryParams | dict, +) -> tuple[ValidParams, Errors]: + """Extract query params given list of dependencies.""" + values = {} + errors = [] + for dep in dependencies: + query_params, dep_errors = get_dependency_query_params(dep, params) + if query_params: + values.update(query_params) + errors += dep_errors + return values, errors + + +def check_query_params( + dependencies: list[Callable], params: QueryParams | dict +) -> bool: + """Check QueryParams for Query dependency. + + 1. `get_dependant` is used to get the query-parameters required by the `callable` + 2. we use `request_params_to_args` to construct arguments needed to call the `callable` + 3. we call the `callable` and catch any errors + + Important: We assume the `callable` in not a co-routine + + """ + qp = ( + QueryParams(urlencode(params, doseq=True)) + if isinstance(params, dict) + else params + ) + + for dependency in dependencies: + try: + dep = get_dependant(path="", call=dependency) + if dep.query_params: + # call the dependency with the query-parameters values + query_values, errors = request_params_to_args(dep.query_params, qp) + if errors: + return False + + _ = dependency(**query_values) + + except Exception: + return False + + return True + + +def accept_media_type(accept: str, mediatypes: list[MediaType]) -> MediaType | None: + """Return MediaType based on accept header and available mediatype. + + Links: + - https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept + + """ + accept_values = {} + for m in accept.replace(" ", "").split(","): + values = m.split(";") + if len(values) == 1: + name = values[0] + quality = 1.0 + else: + name = values[0] + groups = dict([param.split("=") for param in values[1:]]) # type: ignore + try: + q = groups.get("q") + quality = float(q) if q else 1.0 + except ValueError: + quality = 0 + + # if quality is 0 we ignore encoding + if quality: + accept_values[name] = quality + + # Create Preference matrix + media_preference = { + v: [n for (n, q) in accept_values.items() if q == v] + for v in sorted(set(accept_values.values()), reverse=True) + } + + # Loop through available compression and encoding preference + for _, pref in media_preference.items(): + for media in mediatypes: + if media.value in pref: + return media + + # If no specified encoding is supported but "*" is accepted, + # take one of the available compressions. + if "*" in accept_values and mediatypes: + return mediatypes[0] + + return None + + +def update_openapi(app: FastAPI) -> FastAPI: + """Update OpenAPI response content-type. + + This function modifies the openapi route to comply with the STAC API spec's required + content-type response header. + + Copied from https://github.com/stac-utils/stac-fastapi/blob/main/stac_fastapi/api/stac_fastapi/api/openapi.py + + MIT License + + Copyright (c) 2020 Arturo AI + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + # Find the route for the openapi_url in the app + openapi_route: Route = next( + cast(Route, route) + for route in app.router.routes + if route.path == app.openapi_url # type: ignore + ) + # Store the old endpoint function so we can call it from the patched function + old_endpoint = openapi_route.endpoint + + # Create a patched endpoint function that modifies the content type of the response + async def patched_openapi_endpoint(req: Request) -> Response: + # Get the response from the old endpoint function + response = await old_endpoint(req) + # Update the content type header in place + response.headers["content-type"] = ( + "application/vnd.oai.openapi+json;version=3.0" + ) + # Return the updated response + return response + + # When a Route is accessed the `handle` function calls `self.app`. Which is + # the endpoint function wrapped with `request_response`. So we need to wrap + # our patched function and replace the existing app with it. + openapi_route.app = request_response(patched_openapi_endpoint) + + # return the patched app + return app + + +def create_html_response( + request: Request, + data: Any, + template_name: str, + templates: Jinja2Templates, + title: str | None = None, + router_prefix: str | None = None, + **kwargs: Any, +) -> _TemplateResponse: + """Create Template response.""" + urlpath = request.url.path + if root_path := request.scope.get("root_path"): + urlpath = re.sub(r"^" + root_path, "", urlpath) + + if router_prefix: + urlpath = re.sub(r"^" + router_prefix, "", urlpath) + + crumbs = [] + baseurl = str(request.base_url).rstrip("/") + + if router_prefix: + baseurl += router_prefix + + crumbpath = str(baseurl) + if urlpath == "/": + urlpath = "" + + for crumb in urlpath.split("/"): + crumbpath = crumbpath.rstrip("/") + part = crumb + if part is None or part == "": + part = "Home" + crumbpath += f"/{crumb}" + crumbs.append({"url": crumbpath.rstrip("/"), "part": part.capitalize()}) + + return templates.TemplateResponse( + request, + name=f"{template_name}.html", + context={ + "response": data, + "template": { + "api_root": baseurl, + "params": request.query_params, + "title": title or template_name, + }, + "crumbs": crumbs, + "url": baseurl + urlpath, + "params": str(request.url.query), + **kwargs, + }, + ) + + +class TMSLimits(TypedDict): + """TileMatrixSet Limits model.""" + + tileMatrix: str + minTileRow: int + maxTileRow: int + minTileCol: int + maxTileCol: int + + +def tms_limits( + tms: TileMatrixSet, + bounds: tuple[float, float, float, float], + zooms: tuple[int, int] | None = None, + geographic_crs: pyproj.CRS | rasterio.crs.CRS | None = None, +) -> list[TMSLimits]: + """Generate TileMatrixSet limits for given bounds and zoom levels.""" + if geographic_crs: + geographic_crs = rio_crs_to_pyproj(geographic_crs) + + if zooms: + minzoom, maxzoom = zooms + else: + minzoom, maxzoom = tms.minzoom, tms.maxzoom + + tilematrix_limits: list[TMSLimits] = [] + for zoom in range(minzoom, maxzoom + 1): + matrix = tms.matrix(zoom) + ulTile = tms.tile(bounds[0], bounds[3], zoom, geographic_crs=geographic_crs) + lrTile = tms.tile(bounds[2], bounds[1], zoom, geographic_crs=geographic_crs) + minx, maxx = (min(ulTile.x, lrTile.x), max(ulTile.x, lrTile.x)) + miny, maxy = (min(ulTile.y, lrTile.y), max(ulTile.y, lrTile.y)) + tilematrix_limits.append( + { + "tileMatrix": matrix.id, + "minTileRow": max(miny, 0), + "maxTileRow": min(maxy, matrix.matrixHeight), + "minTileCol": max(minx, 0), + "maxTileCol": min(maxx, matrix.matrixWidth), + } + ) + + return tilematrix_limits + + +def tms_limits_to_xml(limits: list[TMSLimits]) -> list[str]: + """Convert TMS limits to XML.""" + xml_limits: list[str] = [] + for limit in limits: + xml_limits.append( + f""" + {limit['tileMatrix']} + {limit['minTileRow']} + {limit['maxTileRow']} + {limit['minTileCol']} + {limit['maxTileCol']} + """, + ) + + return xml_limits + + +def tms_to_xml(tms: TileMatrixSet, minzoom, maxzoom) -> list[str]: + """Convert TMS Matrices to XML.""" + xml_matrices: list[str] = [] + for zoom in range(minzoom, maxzoom + 1): + matrix = tms.matrix(zoom) + xml_matrices.append( + f""" + {matrix.id} + {matrix.scaleDenominator} + {matrix.pointOfOrigin[0]} {matrix.pointOfOrigin[1]} + {matrix.tileWidth} + {matrix.tileHeight} + {matrix.matrixWidth} + {matrix.matrixHeight} + """, + ) + + return xml_matrices + + +def rio_crs_to_pyproj(crs: pyproj.CRS | rasterio.crs.CRS) -> pyproj.CRS: + """Convert rasterio CRS to pyproj CRS.""" + if isinstance(crs, pyproj.CRS): + return crs + + with rasterio.Env(OSR_WKT_FORMAT="WKT2_2018"): + return pyproj.CRS.from_user_input(crs) diff --git a/src/titiler/core/titiler/core/validation.py b/src/titiler/core/titiler/core/validation.py new file mode 100644 index 000000000..5d42dae1b --- /dev/null +++ b/src/titiler/core/titiler/core/validation.py @@ -0,0 +1,131 @@ +"""Dependency validations.""" + +import re +from json import JSONDecodeError, loads + +from color_operations import parse_operations as parse_color_formula +from rasterio.crs import CRS + + +def validate_rescale(rescale_strs: list[str]) -> list[str]: + """ + Verify that rescale input matches an accepted pattern. + :param rescale_strs: Caller-provided rescale values. + :type rescale_strs: list[str] + :return: Caller-provided rescale values if validated, otherwise an exception is raised. + :rtype: list[str] + """ + validated_rescales: list[str] = [] + for rescale_str in rescale_strs: + error_text = "invalid rescale format" + rescale_parts = [ + re.sub(r"[\[|\]]", "", part) for part in rescale_str.split(",") + ] + if len(rescale_parts) == 2: + try: + # Regex validation adds risk, given the different string + # formats that can be parsed to float, so simply attempt + # to parse. + min = float(rescale_parts[0]) + max = float(rescale_parts[1]) + except ValueError as e: + error_text = f"{error_text}: {e}" + else: + validated_rescales.append(f"{min},{max}") + continue + raise ValueError(error_text) + return validated_rescales + + +def validate_crs(crs_str: str | None) -> str | None: + """ + Verify that crs input matches an accepted format. + :param crs_str: Caller-provided crs value. + :type crs_str: str | None + :return: Caller-provided crs value if accepted, otherwise an exception is raised. + :rtype: str | None + """ + if crs_str is None: + return None + if crs_str.startswith("[") and crs_str.endswith( + "]" + ): # this block lifted from OGCMapsParams + crs_str = crs_str[1:-1] + try: + # CRS does not provide a static `is_valid` check, and "many different kinds" of formats + # are supported, making regex impractical. + # (https://rasterio.readthedocs.io/en/latest/api/rasterio.crs.html#rasterio.crs.CRS.from_user_input) + CRS.from_user_input(crs_str) + except ValueError as e: + raise ValueError("invalid CRS format") from e + else: + return crs_str + + +def validate_json(json_str: str | None) -> str | None: + """ + Verify that json input can be parsed. + :param json_str: Caller-provided crs value. + :type json_str: str | None + :return: Caller-provided json_str value if valid, otherwise an exception is raised. + :rtype: str | None + """ + if json_str is None: + return None + try: + loads(json_str) + except JSONDecodeError as e: + raise ValueError("invalid JSON content") from e + else: + return json_str + + +def validate_color_formula(color_formula_str: str | None) -> str | None: + """ + Verify that color formula can be parsed. + :param color_formula_str: Caller-provided color formula value. + :type color_formula_str: str | None + :return: Caller-provided color formula value if valid, otherwise an exception is raised. + :rtype: str | None + """ + if color_formula_str is None: + return None + try: + parse_color_formula(color_formula_str) + except Exception as e: + raise ValueError("invalid color formula") from e + else: + return color_formula_str + + +def validate_bbox(bbox_str: str | None) -> str | None: + """ + Verify that bbox can be parsed. + :param bbox_str: Caller-provided bbox value. + :type bbox_str: str | None + :return: Caller-provided bbox_str value if valid, otherwise an exception is raised. + :rtype: str | None + """ + if bbox_str is None: + return None + if re.match(separated_parseable_floats_regex(count=4), bbox_str) or re.match( + separated_parseable_floats_regex(count=6), bbox_str + ): + return bbox_str + raise ValueError("invalid bbox content") + + +def separated_parseable_floats_regex(count: int, separator: str = ",") -> str: + """ + Generate a regular expression for n separated strings representing floats. + :param count: Number of separated float strings to match + :type count: int + :param separator: Character used to separate float strings. + :type separator: str + :return: Generated regular expression. + :rtype: str + """ + parseable_float_regex = r"\s*(-)?\d+((\.\d+)(e\d+)?)?\s*" + return "^{}$".format( + re.escape(separator).join([parseable_float_regex for _ in range(count)]) + ) diff --git a/src/titiler/extensions/LICENSE b/src/titiler/extensions/LICENSE new file mode 100644 index 000000000..eb4365951 --- /dev/null +++ b/src/titiler/extensions/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Development Seed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/titiler/extensions/README.md b/src/titiler/extensions/README.md index 4ae1309ea..0e86f240c 100644 --- a/src/titiler/extensions/README.md +++ b/src/titiler/extensions/README.md @@ -5,14 +5,14 @@ Extent TiTiler Tiler Factories ## Installation ```bash -$ pip install -U pip +python -m pip install -U pip # From Pypi -$ pip install titiler.extensions +python -m pip install titiler.extensions # Or from sources -$ git clone https://github.com/developmentseed/titiler.git -$ cd titiler && pip install -e titiler/core -e titiler/extensions +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core -e src/titiler/extensions ``` ## Available extensions @@ -58,19 +58,16 @@ tiler = TilerFactory( app.include_router(tiler.router, prefix="/cog") ``` -See [titiler.application](../application) for a full example. - - ## Create your own ```python from dataclasses import dataclass, field from typing import Tuple, List, Optional - +import rasterio from starlette.responses import Response from fastapi import Depends, FastAPI, Query -from titiler.core.factory import BaseTilerFactory, FactoryExtension, TilerFactory -from titiler.core.dependencies import RescalingParams +from titiler.core.factory import TilerFactory, FactoryExtension +from titiler.core.dependencies import ImageRenderingParams from titiler.core.factory import TilerFactory from titiler.core.resources.enums import ImageType @@ -82,8 +79,8 @@ class thumbnailExtension(FactoryExtension): # Set some options max_size: int = field(default=128) - # Register method is mandatory and must take a BaseTilerFactory object as input - def register(self, factory: BaseTilerFactory): + # Register method is mandatory and must take a TilerFactory object as input + def register(self, factory: TilerFactory): """Register endpoint to the tiler factory.""" # register an endpoint to the factory's router @@ -103,47 +100,37 @@ class thumbnailExtension(FactoryExtension): def thumbnail( # we can reuse the factory dependency src_path: str = Depends(factory.path_dependency), + reader_params=Depends(factory.reader_dependency), layer_params=Depends(factory.layer_dependency), dataset_params=Depends(factory.dataset_dependency), post_process=Depends(factory.process_dependency), - rescale: Optional[List[Tuple[float, ...]]] = Depends(RescalingParams), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(factory.colormap_dependency), render_params=Depends(factory.render_dependency), - reader_params=Depends(factory.reader_dependency), env=Depends(factory.environment_dependency), ): with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - im = src.preview( - max_size=self.max_size, - **layer_params, - **dataset_params, - ) + with factory.reader(src_path, **reader_params.as_dict()) as src: + image = src.preview( + max_size=self.max_size, + **layer_params.as_dict(), + **dataset_params.as_dict(), + ) if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) - - if color_formula: - image.apply_color_formula(color_formula) - format = ImageType.jpeg if image.mask.all() else ImageType.png - content = image.render( - img_format=format.driver, - colormap=colormap or dst_colormap, - **format.profile, - **render_params, + if post_process: + image = post_process(image) + + content, media_type = factory.render_func( + image, + colormap=colormap, + **render_params.as_dict(), ) - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) # Use it app = FastAPI() diff --git a/src/titiler/extensions/pyproject.toml b/src/titiler/extensions/pyproject.toml index 3c3972416..a9995fe10 100644 --- a/src/titiler/extensions/pyproject.toml +++ b/src/titiler/extensions/pyproject.toml @@ -1,8 +1,8 @@ [project] -name = "titiler.extensions" +name = "titiler-extensions" description = "Extensions for TiTiler Factories." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.11" authors = [ {name = "Vincent Sarago", email = "vincent@developmentseed.com"}, ] @@ -21,30 +21,35 @@ classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: GIS", ] dynamic = ["version"] dependencies = [ - "titiler.core==0.11.7" + "titiler-core==2.0.0b2", + "typing-extensions; python_version < '3.12'" ] [project.optional-dependencies] +cogeo = [ + "rio-cogeo>=7.0,<8.0", +] +stac = [ + "rio-stac>=0.12,<0.13", +] + +[dependency-groups] test = [ "pytest", "pytest-cov", "pytest-asyncio", "httpx", - "jsonschema>=3.0", -] -cogeo = [ - "rio-cogeo>=3.1,<4.0", -] -stac = [ - "rio-stac>=0.6,<0.7", + "pystac[validation]>=1.0.0,<2.0.0", + "owslib", ] [project.urls] @@ -54,14 +59,15 @@ Issues = "https://github.com/developmentseed/titiler/issues" Source = "https://github.com/developmentseed/titiler" Changelog = "https://developmentseed.org/titiler/release-notes/" -[build-system] -requires = ["pdm-pep517"] -build-backend = "pdm.pep517.api" - -[tool.pdm.version] -source = "file" +[tool.hatch.version] path = "titiler/extensions/__init__.py" -[tool.pdm.build] -includes = ["titiler/extensions"] -excludes = ["tests/", "**/.mypy_cache", "**/.DS_Store"] +[tool.hatch.build.targets.sdist] +only-include = ["titiler"] + +[tool.hatch.build.targets.wheel] +only-include = ["titiler"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" \ No newline at end of file diff --git a/src/titiler/extensions/tests/conftest.py b/src/titiler/extensions/tests/conftest.py index eb44cafa4..51e239533 100644 --- a/src/titiler/extensions/tests/conftest.py +++ b/src/titiler/extensions/tests/conftest.py @@ -1,6 +1,6 @@ """``pytest`` configuration.""" -from typing import Any, Dict +from typing import Any import pytest from rasterio.io import MemoryFile @@ -17,7 +17,7 @@ def set_env(monkeypatch): monkeypatch.setenv("AWS_CONFIG_FILE", "/tmp/noconfigheere") -def parse_img(content: bytes) -> Dict[Any, Any]: +def parse_img(content: bytes) -> dict[Any, Any]: """Read tile image and return metadata.""" with MemoryFile(content) as mem: with mem.open() as dst: diff --git a/src/titiler/extensions/tests/fixtures/render_item.json b/src/titiler/extensions/tests/fixtures/render_item.json new file mode 100644 index 000000000..48d340c22 --- /dev/null +++ b/src/titiler/extensions/tests/fixtures/render_item.json @@ -0,0 +1,358 @@ +{ + "type": "Feature", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json", + "https://stac-extensions.github.io/render/v2.0.0/schema.json", + "https://stac-extensions.github.io/virtual-assets/v1.0.0/schema.json", + "https://stac-extensions.github.io/web-map-links/v1.2.0/schema.json" + ], + "id": "LC08_L1TP_044033_20210305_20210312_01_T1", + "properties": { + "gsd": 30, + "platform": "LANDSAT_8", + "instruments": [ + "OLI", + "TIRS" + ], + "eo:cloud_cover": 7.41, + "proj:epsg": 32610, + "view:sun_azimuth": 149.10910644, + "view:sun_elevation": 40.48243563, + "view:off_nadir": 0.001, + "landsat:scene_id": "LC80440332021064LGN00", + "landsat:processing_level": "L1TP", + "landsat:collection_number": "01", + "landsat:collection_category": "T1", + "landsat:cloud_cover_land": 7.4, + "landsat:wrs_path": "44", + "landsat:wrs_row": "33", + "datetime": "2021-03-05T18:45:37.619485Z", + "created": "2021-03-16T01:40:56.703Z", + "updated": "2021-03-16T01:40:56.703Z", + "renders": { + "thumbnail": { + "title": "Thumbnail", + "assets": [ + "B4", + "B3", + "B2" + ], + "rescale": [ + [ + 0, + 150 + ] + ], + "colormap_name": "rainbow", + "resampling": "bilinear", + "bidx": [ + 1 + ], + "width": 1024, + "height": 1024, + "bands": [ + "B4", + "B3", + "B2" + ] + }, + "index": { + "title": "Thumbnail", + "assets": [ + "B5", + "B4" + ], + "expression": "(b1-b2)/(b1+b2)" + }, + "rgb_old": { + "title": "Thumbnail", + "assets": [ + "B4", + "B3", + "B2" + ], + "asset_bidx": [ + "B4|1", + "B3|1", + "B2|1" + ] + }, + "expression_old": { + "title": "expression", + "assets": [ + "B02" + ], + "asset_expression": [ + "B02|b1*2" + ] + }, + "missing_asset": { + "title": "bad_expression", + "expression": "b1*2" + }, + "ndvi": { + "title": "Normalized Difference Vegetation Index", + "assets": [ + "ndvi" + ], + "resampling": "average", + "colormap_name": "ylgn", + "extra_param": "that titiler does not know" + } + } + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.49680286164214, + 39.958062660227306 + ], + [ + -120.31547276090922, + 39.578858170656 + ], + [ + -120.82135075676177, + 37.82701417652536 + ], + [ + -122.9993441554352, + 38.2150173967007 + ], + [ + -122.49680286164214, + 39.958062660227306 + ] + ] + ] + }, + "links": [ + { + "href": "https://maps.example.com/xyz/{z}/{x}/{y}.png", + "rel": "xyz", + "type": "image/png", + "title": "RGB composite visualized through a XYZ" + }, + { + "rel": "xyz", + "type": "image/png", + "title": "NDVI", + "href": "https://api.cogeo.xyz/stac/preview.png?url=https://raw.githubusercontent.com/stac-extensions/raster/main/examples/item-landsat8.json&assets=B5&assets=B4&expression=(b1-b2)/(b1+b2)&max_size=512&width=512&resampling_method=average&rescale=-1,1&color_map=ylgn&return_mask=true", + "render": "ndvi" + }, + { + "rel": "collection", + "href": "https://landsat-stac.s3.amazonaws.com/collections/landsat-8-l1.json", + "type": "application/json", + "title": "The full collection" + } + ], + "assets": { + "index": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/index.html", + "type": "application/html", + "title": "HTML Page" + }, + "ANG": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_ANG.txt", + "type": "text/plain", + "title": "ANG Metadata", + "roles": [ + "metadata" + ] + }, + "MTL": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_MTL.txt", + "type": "text/plain", + "title": "MTL Metadata", + "roles": [ + "metadata" + ] + }, + "BQA": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_BQA.TIF", + "type": "image/tiff; application=geotiff", + "title": "Quality Band", + "roles": [ + "quality" + ] + }, + "B1": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B1.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B1", + "common_name": "coastal", + "center_wavelength": 0.48, + "full_width_half_max": 0.02 + } + ] + }, + "B2": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B2.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B2", + "common_name": "blue", + "center_wavelength": 0.44, + "full_width_half_max": 0.06 + } + ] + }, + "B3": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B3.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B3", + "common_name": "green", + "center_wavelength": 0.56, + "full_width_half_max": 0.06 + } + ] + }, + "B4": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B4.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B4", + "common_name": "red", + "center_wavelength": 0.65, + "full_width_half_max": 0.04 + } + ] + }, + "B5": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B5.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B5", + "common_name": "nir", + "center_wavelength": 0.86, + "full_width_half_max": 0.03 + } + ] + }, + "B6": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B6.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B6", + "common_name": "swir16", + "center_wavelength": 1.6, + "full_width_half_max": 0.08 + } + ] + }, + "B7": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B7.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B7", + "common_name": "swir22", + "center_wavelength": 2.2, + "full_width_half_max": 0.2 + } + ] + }, + "B8": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B8.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B8", + "common_name": "pan", + "center_wavelength": 0.59, + "full_width_half_max": 0.18 + } + ], + "gsd": 15 + }, + "B9": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B9.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B9", + "common_name": "cirrus", + "center_wavelength": 1.37, + "full_width_half_max": 0.02 + } + ] + }, + "B10": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B10.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B10", + "common_name": "lwir11", + "center_wavelength": 10.9, + "full_width_half_max": 0.8 + } + ], + "gsd": 100 + }, + "B11": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B11.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B11", + "common_name": "lwir12", + "center_wavelength": 12, + "full_width_half_max": 1 + } + ], + "gsd": 100 + }, + "ndvi": { + "roles": [ + "virtual", + "data", + "index" + ], + "type": "image/vnd.stac.geotiff; cloud-optimized=true", + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1#/assets/NDVI", + "vrt:hrefs": [ + { + "key": "B4", + "href": "#/assets/B4" + }, + { + "key": "B5", + "href": "#/assets/B5" + } + ], + "title": "Normalized Difference Vegetation Index", + "vrt:algorithm": "band_arithmetic", + "vrt:algorithm_opts": { + "expression": "(B05-B04)/(B05+B04)", + "rescale": [ + [ + -1, + 1 + ] + ] + } + } + }, + "bbox": [ + -123.00234, + 37.82405, + -120.31321, + 39.95894 + ], + "collection": "landsat-8-l1-c1" +} \ No newline at end of file diff --git a/src/titiler/extensions/tests/test_stac_render.py b/src/titiler/extensions/tests/test_stac_render.py new file mode 100644 index 000000000..16d6487a9 --- /dev/null +++ b/src/titiler/extensions/tests/test_stac_render.py @@ -0,0 +1,103 @@ +"""Test STAC Render extension.""" + +import os +from urllib.parse import unquote, urlparse + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from rio_tiler.io import STACReader + +from titiler.core.factory import MultiBaseTilerFactory +from titiler.extensions.render import stacRenderExtension +from titiler.extensions.wmts import wmtsExtension + +stac_item = os.path.join(os.path.dirname(__file__), "fixtures", "render_item.json") + + +def test_stacExtension(): + """Test stacExtension class.""" + + stac_tiler = MultiBaseTilerFactory(reader=STACReader) + + stac_tiler_plus_stac_render = MultiBaseTilerFactory( + reader=STACReader, extensions=[stacRenderExtension(), wmtsExtension()] + ) + # Check that we added two routes (/renders & /renders/{render_id}) and `/WMTSCapabilities.xml` + assert ( + len(stac_tiler_plus_stac_render.router.routes) + == len(stac_tiler.router.routes) + 3 + ) + + app = FastAPI() + app.include_router(stac_tiler_plus_stac_render.router) + with TestClient(app) as client: + response = client.get("/renders", params={"url": stac_item}) + assert response.status_code == 200 + body = response.json() + assert body["renders"] + assert body["links"] + + self_link = body["links"][0] + assert self_link["href"] == response.url + assert self_link["rel"] == "self" + + assert "ndvi" in body["renders"] + assert "thumbnail" in body["renders"] + + expected_params = { + "title": "Normalized Difference Vegetation Index", + "assets": ["ndvi"], + "resampling": "average", + "colormap_name": "ylgn", + "extra_param": "that titiler does not know", + } + assert body["renders"]["ndvi"]["params"] == expected_params + + links = body["renders"]["ndvi"]["links"] + assert len(links) == 3 + + hrefs = {unquote(urlparse(link["href"]).path) for link in links} + expected_hrefs = { + "/renders/ndvi", + "/WMTSCapabilities.xml", + "/{tileMatrixSetId}/tilejson.json", + } + assert hrefs == expected_hrefs + + response = client.get("/renders/unknown", params={"url": stac_item}) + assert response.status_code == 404 + body = response.json() + assert body == {"detail": "Render not found"} + + response = client.get("/renders/ndvi", params={"url": stac_item}) + assert response.status_code == 200 + body = response.json() + assert body["params"] + assert body["links"] + hrefs = {unquote(urlparse(link["href"]).path) for link in links} + assert hrefs == expected_hrefs + assert body["params"] == expected_params + + response = client.get("/renders/rgb_old", params={"url": stac_item}) + assert response.status_code == 200 + body = response.json() + assert body["valid"] + assert body["params"] == { + "title": "Thumbnail", + "assets": ["B4|bidx=1", "B3|bidx=1", "B2|bidx=1"], + } + + response = client.get("/renders/expression_old", params={"url": stac_item}) + assert response.status_code == 200 + body = response.json() + assert body["valid"] + assert body["params"] == { + "title": "expression", + "assets": ["B02|expression=b1*2"], + } + + response = client.get("/renders/missing_asset", params={"url": stac_item}) + assert response.status_code == 200 + body = response.json() + assert not body["valid"] + assert body["params"] == {"title": "bad_expression", "expression": "b1*2"} diff --git a/src/titiler/extensions/tests/test_viewer.py b/src/titiler/extensions/tests/test_viewer.py index 2a56f119b..18a4cd929 100644 --- a/src/titiler/extensions/tests/test_viewer.py +++ b/src/titiler/extensions/tests/test_viewer.py @@ -25,7 +25,9 @@ def test_cogViewerExtension(): def test_stacViewerExtension(): """Test stacViewerExtension class.""" tiler = MultiBaseTilerFactory(reader=STACReader) - tiler_plus_viewer = MultiBaseTilerFactory(extensions=[stacViewerExtension()]) + tiler_plus_viewer = MultiBaseTilerFactory( + reader=STACReader, extensions=[stacViewerExtension()] + ) assert len(tiler_plus_viewer.router.routes) == len(tiler.router.routes) + 1 app = FastAPI() diff --git a/src/titiler/extensions/tests/test_wms.py b/src/titiler/extensions/tests/test_wms.py index 993817f1f..c7aa2f6de 100644 --- a/src/titiler/extensions/tests/test_wms.py +++ b/src/titiler/extensions/tests/test_wms.py @@ -386,3 +386,53 @@ def test_wmsExtension_GetMap(): -52.301598718454485, 74.66298001264106, ] + + +def test_wmsExtension_GetFeatureInfo(): + """Test wmsValidateExtension class for GetFeatureInfo request.""" + tiler_plus_wms = TilerFactory(extensions=[wmsExtension()]) + + app = FastAPI() + app.include_router(tiler_plus_wms.router) + + with TestClient(app) as client: + # Setup the basic GetFeatureInfo request + params = { + "VERSION": "1.3.0", + "REQUEST": "GetFeatureInfo", + "LAYERS": cog, + "QUERY_LAYERS": cog, + "BBOX": "500975.102,8182890.453,501830.647,8183959.884", + "CRS": "EPSG:32621", + "WIDTH": 334, + "HEIGHT": 333, + "INFO_FORMAT": "text/html", + "I": "0", + "J": "0", + } + + response = client.get("/wms", params=params) + + assert response.status_code == 200 + assert response.content == b"2800" + + params = { + "VERSION": "1.3.0", + "REQUEST": "GetFeatureInfo", + "LAYERS": cog, + "QUERY_LAYERS": cog, + "BBOX": "500975.102,8182890.453,501830.647,8183959.884", + "CRS": "EPSG:32621", + "WIDTH": 334, + "HEIGHT": 333, + "INFO_FORMAT": "text/html", + "I": "333", + "J": "332", + } + + response = client.get("/wms", params=params) + + assert response.status_code == 200 + assert response.content == b"3776" + + # Add additional assertions to check the text response diff --git a/src/titiler/extensions/tests/test_wmts.py b/src/titiler/extensions/tests/test_wmts.py new file mode 100644 index 000000000..0aa5d2e8b --- /dev/null +++ b/src/titiler/extensions/tests/test_wmts.py @@ -0,0 +1,133 @@ +"""test WMTS extension.""" + +import io +import os + +import rasterio +from fastapi import FastAPI +from owslib.wmts import WebMapTileService +from starlette.testclient import TestClient + +from titiler.core.factory import TilerFactory +from titiler.extensions import wmtsExtension + +from .conftest import parse_img + +DATA_DIR = os.path.join(os.path.dirname(__file__), "fixtures") + + +def test_wmtsExtension(): + """Test wmtsExtension class.""" + tiler = TilerFactory() + tiler_plus_wmts = TilerFactory(extensions=[wmtsExtension()]) + assert len(tiler_plus_wmts.router.routes) == len(tiler.router.routes) + 1 + + app = FastAPI() + app.include_router(tiler_plus_wmts.router) + with TestClient(app) as client: + response = client.get(f"/WMTSCapabilities.xml?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/xml" + meta = parse_img(response.content) + assert meta["driver"] == "WMTS" + + wmts = WebMapTileService( + f"/WMTSCapabilities.xml?url={DATA_DIR}/cog.tif", xml=response.content + ) + assert wmts.version == "1.0.0" + assert len(wmts.contents) == 13 # 1 render x 13 TMS + assert f"{DATA_DIR}/cog.tif_WebMercatorQuad_default" in wmts.contents + assert f"{DATA_DIR}/cog.tif_WorldCRS84Quad_default" in wmts.contents + + layer = wmts.contents[f"{DATA_DIR}/cog.tif_WebMercatorQuad_default"] + assert ["5", "6", "7", "8", "9"] == list( + layer.tilematrixsetlinks["WebMercatorQuad"].tilematrixlimits + ) + + assert "WebMercatorQuad" in wmts.tilematrixsets + assert "WorldCRS84Quad" in wmts.tilematrixsets + + assert wmts.provider.name == "TiTiler" + assert wmts.provider.url == "https://developmentseed.org/titiler/" + + # Validate it's a good WMTS + with rasterio.open(io.BytesIO(response.content)) as src: + assert not src.crs + assert src.profile["driver"] == "WMTS" + assert len(src.subdatasets) == 13 + sds_names = [s.split(",layer=")[1] for s in src.subdatasets] + assert f"{DATA_DIR}/cog.tif_WebMercatorQuad_default" in sds_names + + with rasterio.open( + io.BytesIO(response.content), + layer=f"{DATA_DIR}/cog.tif_WebMercatorQuad_default", + ) as sds: + assert sds.crs == "epsg:3857" + + +def test_wmtsExtension_with_renders(): + """Test wmtsExtension class with Renders.""" + tiler_plus_wmts = TilerFactory( + extensions=[ + wmtsExtension( + get_renders=lambda obj: { + "one_band_limit": { + "tilematrixsets": { + "WebMercatorQuad": [0, 1], + }, + "spatial_extent": (-180, -90, 180, 90), + "bidx": [1, 2, 3], + "rescale": [[0, 10000], [0, 5000], [0, 1000]], + }, + "one_band": { + "bidx": [1], + "rescale": ["0,10000"], + }, + }, + ) + ], + ) + + app = FastAPI() + app.include_router(tiler_plus_wmts.router) + with TestClient(app) as client: + response = client.get(f"/WMTSCapabilities.xml?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/xml" + meta = parse_img(response.content) + assert meta["driver"] == "WMTS" + + wmts = WebMapTileService( + f"/WMTSCapabilities.xml?url={DATA_DIR}/cog.tif", xml=response.content + ) + assert wmts.version == "1.0.0" + assert len(wmts.contents) == 39 # (2 renders + default) x 13 TMS + + assert f"{DATA_DIR}/cog.tif_WebMercatorQuad_default" in wmts.contents + assert f"{DATA_DIR}/cog.tif_WorldCRS84Quad_default" in wmts.contents + + layer = wmts.contents[f"{DATA_DIR}/cog.tif_WebMercatorQuad_default"] + assert ["5", "6", "7", "8", "9"] == list( + layer.tilematrixsetlinks["WebMercatorQuad"].tilematrixlimits + ) + + assert f"{DATA_DIR}/cog.tif_WebMercatorQuad_one_band_limit" in wmts.contents + assert f"{DATA_DIR}/cog.tif_WorldCRS84Quad_one_band_limit" in wmts.contents + + layer = wmts.contents[f"{DATA_DIR}/cog.tif_WebMercatorQuad_one_band_limit"] + assert ["0", "1"] == list( + layer.tilematrixsetlinks["WebMercatorQuad"].tilematrixlimits + ) + assert ( + layer.tilematrixsetlinks["WebMercatorQuad"].tilematrixlimits["0"].mintilerow + == 0 + ) + + assert f"{DATA_DIR}/cog.tif_WebMercatorQuad_one_band" in wmts.contents + assert f"{DATA_DIR}/cog.tif_WorldCRS84Quad_one_band" in wmts.contents + + assert "WebMercatorQuad" in wmts.tilematrixsets + assert "WorldCRS84Quad" in wmts.tilematrixsets + + assert wmts.provider.name == "TiTiler" + assert wmts.provider.url == "https://developmentseed.org/titiler/" diff --git a/src/titiler/extensions/titiler/extensions/__init__.py b/src/titiler/extensions/titiler/extensions/__init__.py index 18c28ff64..186ad76ae 100644 --- a/src/titiler/extensions/titiler/extensions/__init__.py +++ b/src/titiler/extensions/titiler/extensions/__init__.py @@ -1,8 +1,10 @@ """titiler.extensions""" -__version__ = "0.11.7" +__version__ = "2.0.0b2" from .cogeo import cogValidateExtension # noqa +from .render import stacRenderExtension # noqa from .stac import stacExtension # noqa from .viewer import cogViewerExtension, stacViewerExtension # noqa from .wms import wmsExtension # noqa +from .wmts import wmtsExtension # noqa diff --git a/src/titiler/extensions/titiler/extensions/cogeo.py b/src/titiler/extensions/titiler/extensions/cogeo.py index 5b0193060..d2bad458d 100644 --- a/src/titiler/extensions/titiler/extensions/cogeo.py +++ b/src/titiler/extensions/titiler/extensions/cogeo.py @@ -1,34 +1,44 @@ """rio-cogeo Extension.""" -from dataclasses import dataclass +from typing import Annotated +from attrs import define from fastapi import Depends, Query -from titiler.core.factory import BaseTilerFactory, FactoryExtension +from titiler.core.factory import FactoryExtension, TilerFactory +from titiler.core.resources.responses import JSONResponse try: from rio_cogeo.cogeo import cog_info from rio_cogeo.models import Info except ImportError: # pragma: nocover cog_info = None # type: ignore - Info = None + Info = None # type: ignore -@dataclass +@define class cogValidateExtension(FactoryExtension): """Add /validate endpoint to a COG TilerFactory.""" - def register(self, factory: BaseTilerFactory): + def register(self, factory: TilerFactory): # type: ignore [override] """Register endpoint to the tiler factory.""" assert ( cog_info is not None - ), "'rio_cogeo' must be installed to use CogValidateExtension" - - @factory.router.get("/validate", response_model=Info) + ), "'rio-cogeo' must be installed to use CogValidateExtension" + + @factory.router.get( + "/validate", + response_model=Info, + response_class=JSONResponse, + operation_id=f"{factory.operation_prefix}validate", + ) def validate( - src_path: str = Depends(factory.path_dependency), - strict: bool = Query(False, description="Treat warnings as errors"), + src_path=Depends(factory.path_dependency), + strict: Annotated[ + bool, + Query(description="Treat warnings as errors"), + ] = False, ): """Validate a COG""" return cog_info(src_path, strict=strict) diff --git a/src/titiler/extensions/titiler/extensions/py.typed b/src/titiler/extensions/titiler/extensions/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/titiler/extensions/titiler/extensions/render.py b/src/titiler/extensions/titiler/extensions/render.py new file mode 100644 index 000000000..8d1c342a3 --- /dev/null +++ b/src/titiler/extensions/titiler/extensions/render.py @@ -0,0 +1,211 @@ +"""STAC Render Extension. + +Implements support for reading and applying Item level render extension. +See: https://github.com/stac-extensions/render +""" + +from typing import Annotated, Any +from urllib.parse import urlencode + +from attrs import define +from fastapi import Depends, HTTPException, Path, Request +from pydantic import BaseModel +from rio_tiler.utils import cast_to_sequence +from starlette.routing import NoMatchFound + +from titiler.core.factory import FactoryExtension, MultiBaseTilerFactory +from titiler.core.models.OGC import Link +from titiler.core.utils import check_query_params + + +class RenderItem(BaseModel, extra="allow"): + """Render item for stac render extension.""" + + title: str | None = None + assets: list[str] | None = None + expression: str | None = None + rescale: list[Annotated[list[float], 2]] | None = None + nodata: float | None = None + colormap_name: str | None = None + colormap: dict | None = None + color_formula: str | None = None + resampling: str | None = None + minmax_zoom: Annotated[list[int], 2] | None = None + + +class RenderItemWithLinks(BaseModel): + """Same as RenderItem with url and params.""" + + valid: bool + params: RenderItem + links: list[Link] + + +class RenderItemList(BaseModel): + """List of Render Items with links.""" + + renders: dict[str, RenderItemWithLinks] + links: list[Link] + + +def _adapt_render_for_v2(render: dict) -> None: + if assets := render.get("assets"): + assets_with_options: dict[str, list] = { + asset: [] for asset in cast_to_sequence(assets) + } + + # adapt for titiler V2 + if asset_bidx := render.pop("asset_bidx", None): + asset_bidx = cast_to_sequence(asset_bidx) + for v in asset_bidx: + asset, bidx = v.split("|") + if asset in assets_with_options: + assets_with_options[asset].append(f"bidx={bidx}") + + # asset_expression + if asset_expr := render.pop("asset_expression", None): + asset_expr = cast_to_sequence(asset_expr) + for v in asset_expr: + asset, expr = v.split("|") + if asset in assets_with_options: + assets_with_options[asset].append(f"expression={expr}") + + new_assets = [] + for asset, options in assets_with_options.items(): + if options: + asset = asset + "|" + "|".join(options) + new_assets.append(asset) + render["assets"] = new_assets + + +@define +class stacRenderExtension(FactoryExtension): + """Add /renders endpoint to a STAC TilerFactory.""" + + def register(self, factory: MultiBaseTilerFactory): # type: ignore [override] + """Register endpoint to the tiler factory.""" + + def _prepare_render_item( + render_id: str, + render: dict, + request: Request, + src_path: str, + ) -> dict: + """Prepare single render item.""" + links: list[dict[str, Any]] = [ + { + "href": factory.url_for( + request, + "STAC Renders metadata", + render_id=render_id, + ) + + "?" + + urlencode({"url": src_path}), + "rel": "self", + "type": "application/json", + "title": f"STAC Renders metadata for {render_id}", + } + ] + + _adapt_render_for_v2(render) + + # List of dependencies a `/tile` URL should validate + # Note: Those dependencies should only require Query() inputs + tile_dependencies = [ + factory.reader_dependency, + factory.tile_dependency, + factory.layer_dependency, + factory.dataset_dependency, + factory.process_dependency, + # Image rendering Dependencies + factory.colormap_dependency, + factory.render_dependency, + ] + + if check_query_params(tile_dependencies, render): # type: ignore[arg-type] + query_string = urlencode({"url": src_path, **render}, doseq=True) + links.append( + { + "href": factory.url_for( + request, + "tilejson", + tileMatrixSetId="{tileMatrixSetId}", + ) + + "?" + + query_string, + "rel": "tilesets-map", + "title": f"tilejson file for {render_id}", + "templated": True, + } + ) + try: + links.append( + { + "href": factory.url_for( + request, + "wmts", + ) + + "?" + + query_string, + "rel": "tilesets-map", + "title": f"WMTS service for {render_id}", + "templated": True, + }, + ) + except NoMatchFound: + pass + else: + return {"valid": False, "params": render, "links": links} + + return {"valid": True, "params": render, "links": links} + + @factory.router.get( + "/renders", + response_model=RenderItemList, + response_model_exclude_none=True, + name="List STAC Renders metadata", + operation_id=f"{factory.operation_prefix}getRenderList", + ) + def render_list(request: Request, src_path=Depends(factory.path_dependency)): + with factory.reader(src_path) as src: + renders = src.item.properties.get("renders", {}) + + prepared_renders = { + render_id: _prepare_render_item(render_id, render, request, src_path) + for render_id, render in renders.items() + } + return { + "renders": prepared_renders, + "links": [ + { + "href": str(request.url), + "rel": "self", + "type": "application/json", + "title": "List STAC Renders metadata", + }, + ], + } + + @factory.router.get( + "/renders/{render_id}", + response_model=RenderItemWithLinks, + response_model_exclude_none=True, + name="STAC Renders metadata", + operation_id=f"{factory.operation_prefix}getRender", + ) + def render( + request: Request, + render_id: str = Path( + description="render id", + ), + src_path=Depends(factory.path_dependency), + ): + with factory.reader(src_path) as src: + renders = src.item.properties.get("renders", {}) + + if render_id not in renders: + raise HTTPException(status_code=404, detail="Render not found") + + return _prepare_render_item( + render_id, renders[render_id], request, src_path + ) diff --git a/src/titiler/extensions/titiler/extensions/stac.py b/src/titiler/extensions/titiler/extensions/stac.py index 6848c7a35..96ffb8091 100644 --- a/src/titiler/extensions/titiler/extensions/stac.py +++ b/src/titiler/extensions/titiler/extensions/stac.py @@ -1,21 +1,18 @@ """rio-stac Extension.""" import sys -from dataclasses import dataclass -from typing import Any, Dict, List, Literal, Optional +from typing import Annotated, Any, Literal +from attrs import define from fastapi import Depends, Query -from titiler.core.factory import BaseTilerFactory, FactoryExtension +from titiler.core.factory import FactoryExtension, TilerFactory -# Avoids a Pydantic error: -# TypeError: You should use `typing_extensions.TypedDict` instead of `typing.TypedDict` with Python < 3.9.2. -# Without it, there is no way to differentiate required and optional fields when subclassed. -# Ref: https://github.com/pydantic/pydantic/pull/3374 -if sys.version_info < (3, 9, 2): - from typing_extensions import TypedDict -else: +# Ref: https://docs.pydantic.dev/2.12/errors/usage_errors/#typed-dict-version +if sys.version_info >= (3, 12): from typing import TypedDict +else: + from typing_extensions import TypedDict try: import pystac @@ -32,21 +29,21 @@ class Item(TypedDict, total=False): type: str stac_version: str - stac_extensions: Optional[List[str]] + stac_extensions: list[str] | None id: str - geometry: Dict[str, Any] - bbox: List[float] - properties: Dict[str, Any] - links: List[Dict[str, Any]] - assets: Dict[str, Any] + geometry: dict[str, Any] + bbox: list[float] + properties: dict[str, Any] + links: list[dict[str, Any]] + assets: dict[str, Any] collection: str -@dataclass +@define class stacExtension(FactoryExtension): """Add /stac endpoint to a COG TilerFactory.""" - def register(self, factory: BaseTilerFactory): + def register(self, factory: TilerFactory): # type: ignore [override] """Register endpoint to the tiler factory.""" assert ( @@ -56,58 +53,93 @@ def register(self, factory: BaseTilerFactory): media = [m.value for m in pystac.MediaType] + ["auto"] - @factory.router.get("/stac", response_model=Item, name="Create STAC Item") + @factory.router.get( + "/stac", + response_model=Item, + name="Create STAC Item", + operation_id=f"{factory.operation_prefix}createSTAC", + ) def create_stac( - src_path: str = Depends(factory.path_dependency), - datetime: Optional[str] = Query( - None, - description="The date and time of the assets, in UTC (e.g 2020-01-01, 2020-01-01T01:01:01).", - ), - extensions: Optional[List[str]] = Query( - None, description="STAC extension URL the Item implements." - ), - collection: Optional[str] = Query( - None, description="The Collection ID that this item belongs to." - ), - collection_url: Optional[str] = Query( - None, description="Link to the STAC Collection." - ), + src_path=Depends(factory.path_dependency), + datetime: Annotated[ + str | None, + Query( + description="The date and time of the assets, in UTC (e.g 2020-01-01, 2020-01-01T01:01:01).", + ), + ] = None, + extensions: Annotated[ + list[str] | None, + Query(description="STAC extension URL the Item implements."), + ] = None, + collection: Annotated[ + str | None, + Query(description="The Collection ID that this item belongs to."), + ] = None, + collection_url: Annotated[ + str | None, + Query(description="Link to the STAC Collection."), + ] = None, # properties: Optional[Dict] = Query(None, description="Additional properties to add in the item."), - id: Optional[str] = Query( - None, - description="Id to assign to the item (default to the source basename).", - ), - asset_name: Optional[str] = Query( - "data", description="asset name for the source (default to 'data')." - ), - asset_roles: Optional[List[str]] = Query( - None, description="list of asset's roles." - ), - asset_media_type: Literal[tuple(media)] = Query( # type: ignore - "auto", description="Asset's media type" - ), - asset_href: Optional[str] = Query( - None, description="Asset's URI (default to source's path)" - ), - with_proj: bool = Query( - True, description="Add the `projection` extension and properties." - ), - with_raster: bool = Query( - True, description="Add the `raster` extension and properties." - ), - with_eo: bool = Query( - True, description="Add the `eo` extension and properties." - ), - max_size: Optional[int] = Query( - 1024, - gt=0, - description="Limit array size from which to get the raster statistics.", - ), + id: Annotated[ + str | None, + Query( + description="Id to assign to the item (default to the source basename)." + ), + ] = None, + asset_name: Annotated[ + str, + Query(description="asset name for the source (default to 'data')."), + ] = "data", + asset_roles: Annotated[ + list[str] | None, + Query(description="list of asset's roles."), + ] = None, + asset_media_type: Annotated[ # type: ignore + Literal[tuple(media)], + Query(description="Asset's media type"), + ] = "auto", + asset_href: Annotated[ + str | None, + Query(description="Asset's URI (default to source's path)"), + ] = None, + with_proj: Annotated[ + bool, + Query(description="Add the `projection` extension and properties."), + ] = True, + with_raster: Annotated[ + bool, + Query(description="Add the `raster` extension and properties."), + ] = True, + with_eo: Annotated[ + bool, + Query(description="Add the `eo` extension and properties."), + ] = True, + max_size: Annotated[ + int, + Query( + gt=0, + description="Limit array size from which to get the raster statistics.", + ), + ] = 1024, + geom_densify_pts: Annotated[ + int, + Query( + alias="geometry_densify", + ge=0, + description="Number of points to add to each edge to account for nonlinear edges transformation.", + ), + ] = 0, + geom_precision: Annotated[ + int, + Query( + alias="geometry_precision", + ge=-1, + description="Round geometry coordinates to this number of decimal.", + ), + ] = -1, ): """Create STAC item.""" - properties = ( - {} - ) # or properties = properties or {} if we add properties in Query + properties = {} # or properties = properties or {} if we add properties in Query dt = None if datetime: @@ -138,4 +170,6 @@ def create_stac( with_raster=with_raster, with_eo=with_eo, raster_max_size=max_size, + geom_densify_pts=geom_densify_pts, + geom_precision=geom_precision, ).to_dict() diff --git a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html index d5e3c448f..54870204a 100644 --- a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html +++ b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html @@ -4,13 +4,20 @@ TiTiler - Cloud Optimized GeoTIFF Viewer - - - - - - - + + + + + + + {{ media_type }} + + {{ layer.tms_identifier }} + + {% for limit in layer.tms_limits %} + + {{ limit.tileMatrix }} + {{ limit.minTileRow }} + {{ limit.maxTileRow }} + {{ limit.minTileCol }} + {{ limit.maxTileCol }} + + {% endfor %} + + + + + {%- endfor %} + {% for tms in tileMatrixSets -%} + + {{ tms.id }} + {{ tms.crs }} + {% for matrix in tms.matrices %} + + {{ matrix.id }} + {{ matrix.scaleDenominator }} + {{ matrix.pointOfOrigin[0] }} {{ matrix.pointOfOrigin[1] }} + {{ matrix.tileWidth }} + {{ matrix.tileHeight }} + {{ matrix.matrixWidth }} + {{ matrix.matrixHeight }} + + {% endfor %} + + {%- endfor %} + + + diff --git a/src/titiler/extensions/titiler/extensions/viewer.py b/src/titiler/extensions/titiler/extensions/viewer.py index cdd14192b..cb9a0ab7b 100644 --- a/src/titiler/extensions/titiler/extensions/viewer.py +++ b/src/titiler/extensions/titiler/extensions/viewer.py @@ -1,62 +1,75 @@ """titiler Viewer Extensions.""" -from dataclasses import dataclass - import jinja2 +from attrs import define from starlette.requests import Request from starlette.responses import HTMLResponse from starlette.templating import Jinja2Templates -from titiler.core.factory import BaseTilerFactory, FactoryExtension +from titiler.core.factory import FactoryExtension, TilerFactory -DEFAULT_TEMPLATES = Jinja2Templates( - directory="", +jinja2_env = jinja2.Environment( + autoescape=jinja2.select_autoescape(["html"]), loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), -) # type:ignore +) +DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) -@dataclass +@define class cogViewerExtension(FactoryExtension): """Add /viewer endpoint to the TilerFactory.""" templates: Jinja2Templates = DEFAULT_TEMPLATES - def register(self, factory: BaseTilerFactory): + def register(self, factory: TilerFactory): # type: ignore [override] """Register endpoint to the tiler factory.""" - @factory.router.get("/viewer", response_class=HTMLResponse) + @factory.router.get( + "/viewer", + response_class=HTMLResponse, + operation_id=f"{factory.operation_prefix}getViewer", + ) def cog_viewer(request: Request): """COG Viewer.""" return self.templates.TemplateResponse( + request, name="cog_viewer.html", context={ - "request": request, - "tilejson_endpoint": factory.url_for(request, "tilejson"), - "info_endpoint": factory.url_for(request, "info"), + "tilejson_endpoint": factory.url_for( + request, "tilejson", tileMatrixSetId="WebMercatorQuad" + ), + "info_endpoint": factory.url_for(request, "info_geojson"), "statistics_endpoint": factory.url_for(request, "statistics"), + "viewer_enabled": getattr(factory, "add_viewer", False), }, media_type="text/html", ) -@dataclass +@define class stacViewerExtension(FactoryExtension): """Add /viewer endpoint to the TilerFactory.""" templates: Jinja2Templates = DEFAULT_TEMPLATES - def register(self, factory: BaseTilerFactory): + def register(self, factory: TilerFactory): # type: ignore [override] """Register endpoint to the tiler factory.""" - @factory.router.get("/viewer", response_class=HTMLResponse) + @factory.router.get( + "/viewer", + response_class=HTMLResponse, + operation_id=f"{factory.operation_prefix}getViewer", + ) def stac_viewer(request: Request): """STAC Viewer.""" return self.templates.TemplateResponse( + request, name="stac_viewer.html", context={ - "request": request, - "tilejson_endpoint": factory.url_for(request, "tilejson"), - "info_endpoint": factory.url_for(request, "info"), + "tilejson_endpoint": factory.url_for( + request, "tilejson", tileMatrixSetId="WebMercatorQuad" + ), + "info_endpoint": factory.url_for(request, "info_geojson"), "statistics_endpoint": factory.url_for(request, "asset_statistics"), }, media_type="text/html", diff --git a/src/titiler/extensions/titiler/extensions/wms.py b/src/titiler/extensions/titiler/extensions/wms.py index 95948cec1..ee627c694 100644 --- a/src/titiler/extensions/titiler/extensions/wms.py +++ b/src/titiler/extensions/titiler/extensions/wms.py @@ -1,29 +1,33 @@ """wms Extension.""" -from dataclasses import dataclass, field +from dataclasses import dataclass from enum import Enum -from typing import Any, Dict, List, Optional, Tuple +from typing import Any from urllib.parse import urlencode import jinja2 import numpy import rasterio -from fastapi import Depends, HTTPException, Query +from attrs import define, field +from fastapi import Depends, HTTPException from rasterio.crs import CRS +from rio_tiler.constants import WGS84_CRS +from rio_tiler.models import ImageData from rio_tiler.mosaic import mosaic_reader from rio_tiler.mosaic.methods.base import MosaicMethodBase from starlette.requests import Request from starlette.responses import Response from starlette.templating import Jinja2Templates -from titiler.core.dependencies import RescalingParams -from titiler.core.factory import BaseTilerFactory, FactoryExtension +from titiler.core.dependencies import RenderingParams +from titiler.core.factory import FactoryExtension, TilerFactory from titiler.core.resources.enums import ImageType, MediaType -DEFAULT_TEMPLATES = Jinja2Templates( - directory="", +jinja2_env = jinja2.Environment( + autoescape=jinja2.select_autoescape(["xml"]), loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), -) # type:ignore +) +DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) class WMSMediaType(str, Enum): @@ -37,28 +41,30 @@ class WMSMediaType(str, Enum): webp = "image/webp" +@dataclass class OverlayMethod(MosaicMethodBase): """Overlay data on top.""" - def feed(self, tile): - """Add data to tile.""" - if self.tile is None: - self.tile = tile + def feed(self, array: numpy.ma.MaskedArray): + """Add data to the mosaic array.""" + if self.mosaic is None: # type: ignore + self.mosaic = array - pidex = self.tile.mask & ~tile.mask + else: + pidex = self.mosaic.mask & ~array.mask - mask = numpy.where(pidex, tile.mask, self.tile.mask) - self.tile = numpy.ma.where(pidex, tile, self.tile) - self.tile.mask = mask + mask = numpy.where(pidex, array.mask, self.mosaic.mask) + self.mosaic = numpy.ma.where(pidex, array, self.mosaic) + self.mosaic.mask = mask -@dataclass +@define class wmsExtension(FactoryExtension): """Add /wms endpoint to a TilerFactory.""" - supported_crs: List[str] = field(default_factory=lambda: ["EPSG:4326"]) - supported_format: List[str] = field( - default_factory=lambda: [ + supported_crs: list[str] = field(default=["EPSG:4326"]) + supported_format: list[str] = field( + default=[ "image/png", "image/jpeg", "image/jpg", @@ -67,12 +73,10 @@ class wmsExtension(FactoryExtension): "image/tiff; application=geotiff", ] ) - supported_version: List[str] = field( - default_factory=lambda: ["1.0.0", "1.1.1", "1.3.0"] - ) + supported_version: list[str] = field(default=["1.0.0", "1.1.1", "1.3.0"]) templates: Jinja2Templates = DEFAULT_TEMPLATES - def register(self, factory: BaseTilerFactory): # noqa: C901 + def register(self, factory: TilerFactory): # type: ignore [override] # noqa: C901 """Register endpoint to the tiler factory.""" @factory.router.get( @@ -92,6 +96,7 @@ def register(self, factory: BaseTilerFactory): # noqa: C901 }, }, }, + operation_id=f"{factory.operation_prefix}getWMS", openapi_extra={ "parameters": [ { @@ -99,10 +104,7 @@ def register(self, factory: BaseTilerFactory): # noqa: C901 "schema": { "title": "Request name", "type": "string", - "enum": [ - "GetCapabilities", - "GetMap", - ], + "enum": ["GetCapabilities", "GetMap", "GetFeatureInfo"], }, "name": "REQUEST", "in": "query", @@ -145,6 +147,7 @@ def register(self, factory: BaseTilerFactory): # noqa: C901 "title": "Output format of service metadata/map", "type": "string", "enum": [ + "text/html", "application/xml", "image/png", "image/jpeg", @@ -201,6 +204,24 @@ def register(self, factory: BaseTilerFactory): # noqa: C901 "name": "HEIGHT", "in": "query", }, + { + "required": False, + "schema": { + "title": "I Coordinate in pixels of feature in Map CS.", + "type": "integer", + }, + "name": "i", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "J Coordinate in pixels of feature in Map CS.", + "type": "integer", + }, + "name": "j", + "in": "query", + }, # Non-Used { "required": False, @@ -264,17 +285,12 @@ def register(self, factory: BaseTilerFactory): # noqa: C901 def wms( # noqa: C901 request: Request, # vendor (titiler) parameters + reader_params=Depends(factory.reader_dependency), layer_params=Depends(factory.layer_dependency), dataset_params=Depends(factory.dataset_dependency), post_process=Depends(factory.process_dependency), - rescale: Optional[List[Tuple[float, ...]]] = Depends(RescalingParams), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(factory.colormap_dependency), - reader_params=Depends(factory.reader_dependency), + render_params=Depends(RenderingParams), env=Depends(factory.environment_dependency), ): """Return a WMS query for a single COG. @@ -355,17 +371,21 @@ def wms( # noqa: C901 wms_url += f"?{urlencode(qs)}" # Grab information from each layer provided - layers_dict: Dict[str, Any] = {} + layers_dict: dict[str, Any] = {} for layer in layers: layers_dict[layer] = {} with rasterio.Env(**env): - with factory.reader(layer, **reader_params) as src_dst: + with factory.reader( + layer, **reader_params.as_dict() + ) as src_dst: layers_dict[layer]["srs"] = f"EPSG:{src_dst.crs.to_epsg()}" layers_dict[layer]["bounds"] = src_dst.bounds - layers_dict[layer][ - "bounds_wgs84" - ] = src_dst.geographic_bounds - layers_dict[layer]["abstract"] = src_dst.info().json() + layers_dict[layer]["bounds_wgs84"] = ( + src_dst.get_geographic_bounds(WGS84_CRS) + ) + layers_dict[layer]["abstract"] = ( + src_dst.info().model_dump_json() + ) # Build information for the whole service minx, miny, maxx, maxy = zip( @@ -373,8 +393,9 @@ def wms( # noqa: C901 ) return self.templates.TemplateResponse( - f"wms_{version}.xml", - { + request, + name=f"wms_{version}.xml", + context={ "request": request, "request_url": wms_url, "formats": self.supported_format, @@ -391,7 +412,11 @@ def wms( # noqa: C901 ) # GetMap: Return an image chip - if request_type.lower() == "getmap": + def get_map_data( # noqa: C901 + req: dict, + req_keys: set, + request_type: str, + ) -> tuple[ImageData, str | None, bool]: # Required parameters: # - VERSION # - REQUEST=GetMap, @@ -404,23 +429,12 @@ def wms( # noqa: C901 # - FORMAT # Optional parameters: TRANSPARENT, BGCOLOR, EXCEPTIONS, TIME, ELEVATION, ... - # List of required parameters (styles and crs are excluded) - req_keys = { - "version", - "request", - "layers", - "bbox", - "width", - "height", - "format", - } - intrs = set(req.keys()).intersection(req_keys) missing_keys = req_keys.difference(intrs) if len(missing_keys) > 0: raise HTTPException( status_code=400, - detail=f"Missing 'GetMap' parameters: {missing_keys}", + detail=f"Missing '{request_type}' parameters: {missing_keys}", ) version = req["version"] @@ -480,54 +494,99 @@ def wms( # noqa: C901 detail=f"Invalid 'TRANSPARENT' parameter: {transparent}. Should be one of ['FALSE', 'TRUE'].", ) - if req["format"] not in self.supported_format: - raise HTTPException( - status_code=400, - detail=f"Invalid 'FORMAT' parameter: {req['format']}. Should be one of {self.supported_format}.", - ) - format = ImageType(WMSMediaType(req["format"]).name) + if format := req.get("format", None): + if format not in self.supported_format: + raise HTTPException( + status_code=400, + detail=f"Invalid 'FORMAT' parameter: {format}. Should be one of {self.supported_format}.", + ) + format = ImageType(WMSMediaType(format).name) height, width = int(req["height"]), int(req["width"]) def _reader(src_path: str): with rasterio.Env(**env): - with factory.reader(src_path, **reader_params) as src_dst: + with factory.reader( + src_path, **reader_params.as_dict() + ) as src_dst: return src_dst.part( bbox, width=width, height=height, dst_crs=crs, bounds_crs=crs, - **layer_params, - **dataset_params, + **layer_params.as_dict(), + **dataset_params.as_dict(), ) - image, assets_used = mosaic_reader( + image, _ = mosaic_reader( layers, _reader, pixel_selection=OverlayMethod(), ) + return image, format, transparent - if post_process: - image = post_process(image) + if request_type.lower() == "getmap": + # List of required parameters (styles and crs are excluded) + req_keys = { + "version", + "request", + "layers", + "bbox", + "width", + "height", + "format", + } - if rescale: - image.rescale(rescale) + image, format, transparent = get_map_data(req, req_keys, request_type) - if color_formula: - image.apply_color_formula(color_formula) + if post_process: + image = post_process(image) - content = image.render( - img_format=format.driver, + content, media_type = factory.render_func( + image, + output_format=format, colormap=colormap, add_mask=transparent, - **format.profile, + **render_params.as_dict(), ) - - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) elif request_type.lower() == "getfeatureinfo": - return Response("Not Implemented", 400) + # Required parameters: + # - VERSION + # - REQUEST=GetFeatureInfo + # - LAYERS + # - CRS or SRS + # - WIDTH + # - HEIGHT + # - QUERY_LAYERS + # - I (Pixel column) + # - J (Pixel row) + # Optional parameters: INFO_FORMAT, FEATURE_COUNT, ... + + req_keys = { + "version", + "request", + "layers", + "width", + "height", + "query_layers", + "i", + "j", + } + image, _, _ = get_map_data(req, req_keys, request_type) + i = int(req["i"]) + j = int(req["j"]) + + html_content = "" + bands_info = [] + for band in range(image.count): + pixel_value = image.data[band, j, i] + bands_info.append(pixel_value) + + html_content = ",".join([str(band_info) for band_info in bands_info]) + return Response(html_content, 200) else: raise HTTPException( diff --git a/src/titiler/extensions/titiler/extensions/wmts.py b/src/titiler/extensions/titiler/extensions/wmts.py new file mode 100644 index 000000000..76b98a90b --- /dev/null +++ b/src/titiler/extensions/titiler/extensions/wmts.py @@ -0,0 +1,243 @@ +"""TiTiler WMTS Extension.""" + +import warnings +from collections.abc import Callable +from typing import Annotated, Any +from urllib.parse import urlencode + +import jinja2 +import rasterio +from attrs import define, field +from fastapi import Depends, HTTPException, Query +from morecantile.models import crs_axis_inverted +from rasterio.crs import CRS +from rio_tiler.constants import WGS84_CRS +from rio_tiler.io import BaseReader +from rio_tiler.utils import CRS_to_urn +from starlette.datastructures import QueryParams +from starlette.requests import Request +from starlette.templating import Jinja2Templates + +from titiler.core.factory import FactoryExtension, TilerFactory +from titiler.core.resources.enums import ImageType +from titiler.core.resources.responses import XMLResponse +from titiler.core.utils import check_query_params, rio_crs_to_pyproj, tms_limits + +jinja2_env = jinja2.Environment( + autoescape=jinja2.select_autoescape(["xml"]), + loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), +) +DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) + + +@define +class wmtsExtension(FactoryExtension): + """RESTful WMTS service Extension for TilerFactory.""" + + # Geographic Coordinate Reference System. + crs: CRS = field(default=WGS84_CRS) + + templates: Jinja2Templates = field(default=DEFAULT_TEMPLATES) + + get_renders: Callable[[BaseReader], dict[str, dict[str, Any]]] = field( + default=lambda obj: {} + ) + + def register(self, factory: TilerFactory): # type: ignore [override] # noqa: C901 + """Register extension's endpoints.""" + + @factory.router.get( + "/WMTSCapabilities.xml", + response_class=XMLResponse, + responses={ + 200: { + "content": {"application/xml": {}}, + "description": "Return RESTful WMTS service capabilities document.", + } + }, + operation_id=f"{factory.operation_prefix}getWMTS", + ) + def wmts( # noqa: C901 + request: Request, + tile_format: Annotated[ + ImageType, + Query(description="Output image type. Default is png."), + ] = ImageType.png, + use_epsg: Annotated[ + bool, + Query( + description="Use EPSG code, not opengis.net, for the ows:SupportedCRS in the TileMatrixSet (set to True to enable ArcMap compatability)" + ), + ] = False, + src_path=Depends(factory.path_dependency), + reader_params=Depends(factory.reader_dependency), + env=Depends(factory.environment_dependency), + ): + """OGC RESTful WMTS endpoint.""" + with rasterio.Env(**env): + with factory.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(self.crs) + default_renders = self.get_renders(src_dst) + + # List of dependencies a `/tile` URL should validate + # Note: Those dependencies should only require Query() inputs + tile_dependencies: list[Callable] = [ + factory.reader_dependency, + factory.tile_dependency, + factory.layer_dependency, + factory.dataset_dependency, + factory.process_dependency, + factory.colormap_dependency, + factory.render_dependency, + ] + renders: list[dict[str, Any]] = [] + + ########################################## + # 1. Create layers from `renders` metadata + for name, values in default_renders.items(): + values.pop("tilesize", None) # Ensure tilesize is not overridden + if check_query_params(tile_dependencies, values): + renders.append( + { + "name": name, + "query_string": urlencode(values, doseq=True) + if values + else None, + "tilematrixsets": values.get("tilematrixsets", {}), + "spatial_extent": values.get("spatial_extent", None), + } + ) + else: + warnings.warn( + f"Cannot construct URL for layer `{name}`", + UserWarning, + stacklevel=2, + ) + + ####################################### + # 2. Create layer from query-parameters + qs_key_to_remove = [ + "tile_format", + "use_epsg", + # Make sure tilesize is not ovewrriden from WMTS request + "tilesize", + # OGC WMTS parameters to ignore + "service", + "request", + "acceptversions", + "sections", + "updatesequence", + "acceptformats", + ] + + qs = urlencode( + [ + (key, value) + for (key, value) in request.query_params._list + if key.lower() not in qs_key_to_remove + ], + doseq=True, + ) + + if check_query_params(tile_dependencies, QueryParams(qs)): + renders.append({"name": "default", "query_string": qs}) + + ################################################# + # 3. if there is no layers we raise and exception + if not renders: + raise HTTPException( + status_code=400, + detail="Could not find any valid layers in metadata or construct one from Query Parameters.", + ) + + layers: list[dict[str, Any]] = [] + title = src_path if isinstance(src_path, str) else "TiTiler" + for render in renders: + for tms_id in factory.supported_tms.list(): + tms = factory.supported_tms.get(tms_id) + try: + with factory.reader( + src_path, tms=tms, **reader_params.as_dict() + ) as src_dst: + if zooms := render.get("tilematrixsets", {}).get(tms_id): + minzoom, maxzoom = zooms + else: + minzoom = src_dst.minzoom + maxzoom = src_dst.maxzoom + + # NOTE: Custom TiTiler Render key in form of {"spatial_extent": (minx, miny, maxx, maxy)} + # NOTE: We assume the spatial_extent is always in WGS84 + if render.get("spatial_extent"): + bbox = render["spatial_extent"] + crs = WGS84_CRS + else: + bbox = bounds + crs = self.crs + + tilematrixset_limits = tms_limits( + tms, + bbox, + zooms=(minzoom, maxzoom), + geographic_crs=crs, + ) + + except Exception as e: # noqa + pass + + route_params = { + "z": "{TileMatrix}", + "x": "{TileCol}", + "y": "{TileRow}", + "format": tile_format.value, + "tileMatrixSetId": tms_id, + } + + bbox_crs_type = "WGS84BoundingBox" + bbox_crs_uri = "urn:ogc:def:crs:OGC:2:84" + if crs != WGS84_CRS: + bbox_crs_type = "BoundingBox" + bbox_crs_uri = CRS_to_urn(crs) # type: ignore + # WGS88BoundingBox is always xy ordered, but BoundingBox must match the CRS order + proj_crs = rio_crs_to_pyproj(crs) + if crs_axis_inverted(proj_crs): + # match the bounding box coordinate order to the CRS + bbox = [bbox[1], bbox[0], bbox[3], bbox[2]] + + layers.append( + { + "title": f"{title}_{tms_id}_{render['name']}", + "identifier": f"{title}_{tms_id}_{render['name']}", + "tms_identifier": tms_id, + "tms_limits": tilematrixset_limits, + "tiles_url": factory.url_for( + request, "tile", **route_params + ), + "query_string": render["query_string"], + "bbox_crs_type": bbox_crs_type, + "bbox_crs_uri": bbox_crs_uri, + "bbox": bbox, + } + ) + + tileMatrixSets: list[dict[str, Any]] = [] + for tms_id in factory.supported_tms.list(): + tms = factory.supported_tms.get(tms_id) + if use_epsg: + supported_crs = f"EPSG:{tms.crs.to_epsg()}" + else: + supported_crs = tms.crs.srs + + tileMatrixSets.append( + {"id": tms_id, "crs": supported_crs, "matrices": tms.tileMatrices} + ) + + return self.templates.TemplateResponse( + request, + name="wmts.xml", + context={ + "layers": layers, + "tileMatrixSets": tileMatrixSets, + "media_type": tile_format.mediatype, + }, + media_type="application/xml", + ) diff --git a/src/titiler/mosaic/LICENSE b/src/titiler/mosaic/LICENSE new file mode 100644 index 000000000..eb4365951 --- /dev/null +++ b/src/titiler/mosaic/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Development Seed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/titiler/mosaic/README.md b/src/titiler/mosaic/README.md index 010c46fc3..72113a72e 100644 --- a/src/titiler/mosaic/README.md +++ b/src/titiler/mosaic/README.md @@ -1,18 +1,23 @@ ## titiler.mosaic -Adds support for MosaicJSON in Titiler. + + +Adds support for Mosaic in Titiler. `Mosaic's` backend needs to be built on top of rio-tiler's Mosaic Backend https://cogeotiff.github.io/rio-tiler/advanced/mosaic_backend/ ## Installation ```bash -$ pip install -U pip +python -m pip install -U pip # From Pypi -$ pip install titiler.mosaic +python -m pip install titiler.mosaic # Or from sources -$ git clone https://github.com/developmentseed/titiler.git -$ cd titiler && pip install -e titiler/core -e titiler/mosaic +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core -e src/titiler/mosaic + +# install cogeo-mosaic for MosaicJSON support +python -m pip install cogeo-mosaic ``` ## How To @@ -21,30 +26,30 @@ $ cd titiler && pip install -e titiler/core -e titiler/mosaic from fastapi import FastAPI from titiler.mosaic.factory import MosaicTilerFactory +from cogeo_mosaic.backends import MosaicBackend + # Create a FastAPI application app = FastAPI( - description="A lightweight Cloud Optimized GeoTIFF tile server", + description="A Mosaic tile server", ) -# Create a set of MosaicJSON endpoints -mosaic = MosaicTilerFactory() +# Create a set of Mosaic endpoints using MosaicJSON backend from cogeo-mosaic project +mosaic = MosaicTilerFactory(backend=MosaicBackend) # Register the Mosaic endpoints to the application app.include_router(mosaic.router, tags=["MosaicJSON"]) ``` -See [titiler.application](../application) for a full example. - ## Package structure ``` titiler/ └── mosaic/ ├── tests/ - Tests suite - └── titiler/mosaic/ - `core` namespace package - ├── resources/ - | ├── enums.py - mosaicJSON enumerations - ├── errors.py - cogeo-mosaic known errors - ├── factory.py - MosaicJSON endpoints factory - └── version.py - version + └── titiler/mosaic/ - `mosaic` namespace package + ├── models/ + | └── responses.py - mosaic response models + ├── errors.py - mosaic known errors + ├── extensions.py - extensions + └── factory.py - Mosaic endpoints factory ``` diff --git a/src/titiler/mosaic/pyproject.toml b/src/titiler/mosaic/pyproject.toml index 5bcb697c3..1a750e96f 100644 --- a/src/titiler/mosaic/pyproject.toml +++ b/src/titiler/mosaic/pyproject.toml @@ -1,8 +1,8 @@ [project] -name = "titiler.mosaic" +name = "titiler-mosaic" description = "cogeo-mosaic (MosaicJSON) plugin for TiTiler." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.11" authors = [ {name = "Vincent Sarago", email = "vincent@developmentseed.com"}, ] @@ -21,24 +21,30 @@ classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: GIS", ] dynamic = ["version"] dependencies = [ - "titiler.core==0.11.7", - "cogeo-mosaic>=5.0,<5.2", + "titiler-core==2.0.0b2", ] [project.optional-dependencies] +mosaicjson = [ + "cogeo-mosaic>=9.0,<10.0", +] + +[dependency-groups] test = [ "pytest", "pytest-cov", "pytest-asyncio", "httpx", + "owslib", ] [project.urls] @@ -48,14 +54,15 @@ Issues = "https://github.com/developmentseed/titiler/issues" Source = "https://github.com/developmentseed/titiler" Changelog = "https://developmentseed.org/titiler/release-notes/" -[build-system] -requires = ["pdm-pep517"] -build-backend = "pdm.pep517.api" - -[tool.pdm.version] -source = "file" +[tool.hatch.version] path = "titiler/mosaic/__init__.py" -[tool.pdm.build] -includes = ["titiler/mosaic"] -excludes = ["tests/", "**/.mypy_cache", "**/.DS_Store"] +[tool.hatch.build.targets.sdist] +only-include = ["titiler"] + +[tool.hatch.build.targets.wheel] +only-include = ["titiler"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/src/titiler/mosaic/tests/conftest.py b/src/titiler/mosaic/tests/conftest.py index caa12a195..61442364c 100644 --- a/src/titiler/mosaic/tests/conftest.py +++ b/src/titiler/mosaic/tests/conftest.py @@ -1,7 +1,7 @@ """``pytest`` configuration.""" import os -from typing import Any, Dict +from typing import Any import pytest from rasterio.io import MemoryFile @@ -20,7 +20,7 @@ def set_env(monkeypatch): monkeypatch.setenv("AWS_CONFIG_FILE", "/tmp/noconfigheere") -def parse_img(content: bytes) -> Dict[Any, Any]: +def parse_img(content: bytes) -> dict[Any, Any]: """Read tile image and return metadata.""" with MemoryFile(content) as mem: with mem.open() as dst: diff --git a/src/titiler/mosaic/tests/test_factory.py b/src/titiler/mosaic/tests/test_factory.py index abd514f0b..504691a4d 100644 --- a/src/titiler/mosaic/tests/test_factory.py +++ b/src/titiler/mosaic/tests/test_factory.py @@ -5,23 +5,33 @@ from contextlib import contextmanager from dataclasses import dataclass from io import BytesIO -from typing import Optional +from typing import Annotated, Any +from unittest.mock import patch import attr +import morecantile import numpy from cogeo_mosaic.backends import FileBackend +from cogeo_mosaic.backends import MosaicBackend as MosaicJSONBackend from cogeo_mosaic.mosaic import MosaicJSON -from fastapi import FastAPI +from fastapi import FastAPI, Query +from owslib.wmts import WebMapTileService +from rio_tiler.mosaic.methods import PixelSelectionMethod from starlette.testclient import TestClient from titiler.core.dependencies import DefaultDependency +from titiler.core.errors import add_exception_handlers from titiler.core.resources.enums import OptionalHeader +from titiler.mosaic.errors import MOSAIC_STATUS_CODES +from titiler.mosaic.extensions.mosaicjson import MosaicJSONExtension +from titiler.mosaic.extensions.wmts import wmtsExtension from titiler.mosaic.factory import MosaicTilerFactory -from titiler.mosaic.resources.enums import PixelSelectionMethod -from .conftest import DATA_DIR +from .conftest import DATA_DIR, parse_img assets = [os.path.join(DATA_DIR, asset) for asset in ["cog1.tif", "cog2.tif"]] +DEFAULT_TMS = morecantile.tms +NB_DEFAULT_TMS = len(DEFAULT_TMS.list()) @contextmanager @@ -42,15 +52,45 @@ def tmpmosaic(): def test_MosaicTilerFactory(): """Test MosaicTilerFactory class.""" mosaic = MosaicTilerFactory( + backend=MosaicJSONBackend, optional_headers=[OptionalHeader.x_assets], router_prefix="mosaic", ) - assert len(mosaic.router.routes) == 23 + assert len(mosaic.router.routes) == 12 + + @dataclass + class MosaicJSONAccessor(DefaultDependency): + """MosaicJSON Accessor Options.""" + + reverse: Annotated[ + bool, + Query(), + ] = False + + mosaic = MosaicTilerFactory( + backend=MosaicJSONBackend, + assets_accessor_dependency=MosaicJSONAccessor, + optional_headers=[ + OptionalHeader.x_assets, + OptionalHeader.server_timing, + ], + extensions=[MosaicJSONExtension(), wmtsExtension()], + add_statistics=True, + add_part=True, + router_prefix="mosaic", + ) + assert len(mosaic.router.routes) == 21 app = FastAPI() app.include_router(mosaic.router, prefix="/mosaic") client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + + response = client.get("/docs") + assert response.status_code == 200 + with tmpmosaic() as mosaic_file: response = client.get( "/mosaic/", @@ -66,13 +106,6 @@ def test_MosaicTilerFactory(): assert response.status_code == 200 assert response.json()["mosaicjson"] - response = client.get( - "/mosaic/bounds", - params={"url": mosaic_file}, - ) - assert response.status_code == 200 - assert response.json()["bounds"] - response = client.get( "/mosaic/info", params={"url": mosaic_file}, @@ -93,14 +126,82 @@ def test_MosaicTilerFactory(): params={"url": mosaic_file}, ) assert response.status_code == 200 + assert response.json()["coordinates"] + v = response.json()["assets"] + # one asset found + assert len(v) == 1 + assert v[0]["name"] + # 3 bands + assert len(v[0]["values"]) == 3 + assert v[0]["band_names"] == ["b1", "b2", "b3"] + assert v[0]["band_descriptions"] + + response = client.get( + "/mosaic/point/-73,45", + params={"url": mosaic_file}, + ) + assert response.status_code == 200 + assert response.json()["coordinates"] + v = response.json()["assets"] + # two asset found + assert len(v) == 2 + names = [asset["name"] for asset in v] + assert names == assets + + response = client.get( + "/mosaic/point/-73,45", + params={"url": mosaic_file, "reverse": True}, + ) + assert response.status_code == 200 + assert response.json()["coordinates"] + v = response.json()["assets"] + # two asset found + assert len(v) == 2 + names = [asset["name"] for asset in v] + assert names == list(reversed(assets)) + + # Masked values + response = client.get( + "/mosaic/point/-75.759,46.3847", + params={"url": mosaic_file}, + ) + assert response.status_code == 200 + assert response.json()["coordinates"] + v = response.json()["assets"] + # one asset found + assert len(v) == 1 + assert v[0]["name"] + # 3 bands + assert len(v[0]["values"]) == 3 + assert v[0]["values"][0] is None + assert v[0]["band_names"] == ["b1", "b2", "b3"] + assert v[0]["band_descriptions"] + + response = client.get( + "/mosaic/point/-7903683.846322423,5780349.220256353", + params={"url": mosaic_file, "coord_crs": "epsg:3857"}, + ) + assert response.status_code == 200 - response = client.get("/mosaic/tiles/7/37/45", params={"url": mosaic_file}) + response = client.get( + "/mosaic/tiles/WebMercatorQuad/7/37/45", params={"url": mosaic_file} + ) + assert response.status_code == 200 + assert response.headers["X-Assets"] + assert response.headers["Server-Timing"] + assert response.headers["Content-Crs"] + assert response.headers["Content-Bbox"] + + response = client.get( + "/mosaic/tiles/WGS1984Quad/8/148/61", params={"url": mosaic_file} + ) assert response.status_code == 200 assert response.headers["X-Assets"] # Buffer response = client.get( - "/mosaic/tiles/7/37/45.npy", params={"url": mosaic_file, "buffer": 10} + "/mosaic/tiles/WebMercatorQuad/7/37/45.npy", + params={"url": mosaic_file, "buffer": 10}, ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -108,7 +209,7 @@ def test_MosaicTilerFactory(): assert npy_tile.shape == (4, 276, 276) # mask + data response = client.get( - "/mosaic/tilejson.json", + "/mosaic/WebMercatorQuad/tilejson.json", params={ "url": mosaic_file, "tile_format": "png", @@ -119,39 +220,37 @@ def test_MosaicTilerFactory(): assert response.status_code == 200 body = response.json() assert ( - "http://testserver/mosaic/tiles/WebMercatorQuad/{z}/{x}/{y}@1x.png?url=" + "http://testserver/mosaic/tiles/WebMercatorQuad/{z}/{x}/{y}.png?url=" in body["tiles"][0] ) assert body["minzoom"] == 6 assert body["maxzoom"] == 9 response = client.get( - "/mosaic/tilejson.json", + "/mosaic/WebMercatorQuad/tilejson.json", params={ "url": mosaic_file, "tile_format": "png", "minzoom": 6, "maxzoom": 9, - "TileMatrixSetId": "WebMercatorQuad", + "tileMatrixSetId": "WebMercatorQuad", }, ) assert response.status_code == 200 body = response.json() assert ( - "http://testserver/mosaic/tiles/WebMercatorQuad/{z}/{x}/{y}@1x.png?url=" + "http://testserver/mosaic/tiles/WebMercatorQuad/{z}/{x}/{y}.png?url=" in body["tiles"][0] ) assert body["minzoom"] == 6 assert body["maxzoom"] == 9 - assert "TileMatrixSetId" not in body["tiles"][0] + assert "tileMatrixSetId" not in body["tiles"][0] response = client.get( "/mosaic/WMTSCapabilities.xml", params={ "url": mosaic_file, "tile_format": "png", - "minzoom": 6, - "maxzoom": 9, }, ) assert response.status_code == 200 @@ -159,12 +258,12 @@ def test_MosaicTilerFactory(): response = client.post( "/mosaic/validate", - json=MosaicJSON.from_urls(assets).dict(), + json=MosaicJSON.from_urls(assets).model_dump(), ) assert response.status_code == 200 response = client.get( - "/mosaic/7/36/45/assets", + "/mosaic/tiles/WebMercatorQuad/7/36/45/assets", params={"url": mosaic_file}, ) assert response.status_code == 200 @@ -172,7 +271,10 @@ def test_MosaicTilerFactory(): filepath.split("/")[-1] in ["cog1.tif"] for filepath in response.json() ) - response = client.get("/mosaic/-71,46/assets", params={"url": mosaic_file}) + response = client.get( + "/mosaic/tiles/WGS1984Quad/8/148/61/assets", + params={"url": mosaic_file}, + ) assert response.status_code == 200 assert all( filepath.split("/")[-1] in ["cog1.tif", "cog2.tif"] @@ -180,7 +282,29 @@ def test_MosaicTilerFactory(): ) response = client.get( - "/mosaic/-75.9375,43.06888777416962,-73.125,45.089035564831015/assets", + "/mosaic/point/-71,46/assets", params={"url": mosaic_file} + ) + assert response.status_code == 200 + assert response.json() == assets + + response = client.get( + "/mosaic/point/-71,46/assets", params={"url": mosaic_file, "reverse": True} + ) + assert response.status_code == 200 + assert response.json() == list(reversed(assets)) + + response = client.get( + "/mosaic/point/-7903683.846322423,5780349.220256353/assets", + params={"url": mosaic_file, "coord_crs": "epsg:3857"}, + ) + assert response.status_code == 200 + assert all( + filepath.split("/")[-1] in ["cog1.tif", "cog2.tif"] + for filepath in response.json() + ) + + response = client.get( + "/mosaic/bbox/-75.9375,43.06888777416962,-73.125,45.089035564831015/assets", params={"url": mosaic_file}, ) assert response.status_code == 200 @@ -190,12 +314,153 @@ def test_MosaicTilerFactory(): ) response = client.get( - "/mosaic/10,10,11,11/assets", + "/mosaic/bbox/-8453323.83211421,5322463.153553393,-8140237.76425813,5635549.221409473/assets", + params={"url": mosaic_file, "coord_crs": "epsg:3857"}, + ) + assert response.status_code == 200 + assert all( + filepath.split("/")[-1] in ["cog1.tif", "cog2.tif"] + for filepath in response.json() + ) + + response = client.get( + "/mosaic/bbox/10,10,11,11/assets", params={"url": mosaic_file}, ) assert response.status_code == 200 assert response.json() == [] + # OGC Tileset + response = client.get(f"/mosaic/tiles?url={mosaic_file}") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp["tilesets"]) == NB_DEFAULT_TMS + + first_tms = resp["tilesets"][0] + first_id = DEFAULT_TMS.list()[0] + assert first_id in first_tms["title"] + assert len(first_tms["links"]) == 2 # no link to the tms definition + + response = client.get( + f"/mosaic/tiles?url={mosaic_file}", headers={"Accept": "text/html"} + ) + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + + response = client.get("/mosaic/tiles", params={"url": mosaic_file, "f": "html"}) + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + + response = client.get(f"/mosaic/tiles/WebMercatorQuad?url={mosaic_file}") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + # covers only 3 zoom levels + assert len(resp["tileMatrixSetLimits"]) == 3 + + response = client.get( + f"/mosaic/tiles/WebMercatorQuad?url={mosaic_file}", + headers={"Accept": "text/html"}, + ) + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + + response = client.get( + "/mosaic/tiles/WebMercatorQuad", params={"url": mosaic_file, "f": "html"} + ) + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + + response = client.get( + "/mosaic/bbox/-74,45,-73,46.png", + params={"url": mosaic_file, "dst_crs": "EPSG:3857"}, + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + assert response.headers["X-Assets"] + assert response.headers["Server-Timing"] + assert response.headers["Content-Crs"] + assert response.headers["Content-Bbox"] + + meta = parse_img(response.content) + assert meta["width"] == 258 + assert meta["height"] == 367 + + response = client.get( + "/mosaic/bbox/-74,45,-73,46/100x50.png", + params={"url": mosaic_file}, + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + assert response.headers["X-Assets"] + assert response.headers["Server-Timing"] + assert response.headers["Content-Crs"] + assert response.headers["Content-Bbox"] + + meta = parse_img(response.content) + assert meta["width"] == 100 + assert meta["height"] == 50 + + # test /feature + + feat = { + "type": "Polygon", + "coordinates": [ + [ + [-74, 45], + [-74, 46], + [-73, 46], + [-73, 45], + [-74, 45], + ] + ], + } + response = client.post( + "/mosaic/feature", + params={"url": mosaic_file, "dst_crs": "EPSG:3857"}, + json={"type": "Feature", "properties": {}, "geometry": feat}, + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + assert response.headers["X-Assets"] + assert response.headers["Server-Timing"] + assert response.headers["Content-Crs"] + assert response.headers["Content-Bbox"] + meta = parse_img(response.content) + assert meta["width"] == 258 + assert meta["height"] == 367 + + response = client.post( + "/mosaic/feature", + params={"url": mosaic_file, "max_size": 200, "dst_crs": "EPSG:3857"}, + json={"type": "Feature", "properties": {}, "geometry": feat}, + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + meta = parse_img(response.content) + assert meta["width"] == 141 + assert meta["height"] == 200 + + response = client.post( + "/mosaic/feature.png", + params={"url": mosaic_file}, + json={"type": "Feature", "properties": {}, "geometry": feat}, + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + + response = client.post( + "/mosaic/feature/150x50.png", + params={"url": mosaic_file}, + json={"type": "Feature", "properties": {}, "geometry": feat}, + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + meta = parse_img(response.content) + assert meta["width"] == 150 + assert meta["height"] == 50 + @dataclass class BackendParams(DefaultDependency): @@ -205,25 +470,10 @@ class BackendParams(DefaultDependency): maxzoom: int = 8 -@attr.s -class CustomFileBackend(FileBackend): - """Fake backend to prove we can overwrite min/max zoom.""" - - minzoom: Optional[int] = attr.ib(default=None) - maxzoom: Optional[int] = attr.ib(default=None) - - def __attrs_post_init__(self): - """Post Init: if not passed in init, try to read from self.input.""" - self.mosaic_def = self.mosaic_def or self._read() - self.minzoom = self.minzoom or self.mosaic_def.minzoom - self.maxzoom = self.maxzoom or self.mosaic_def.maxzoom - self.bounds = self.mosaic_def.bounds - - def test_MosaicTilerFactory_BackendParams(): """Test MosaicTilerFactory factory with Backend dependency.""" mosaic = MosaicTilerFactory( - reader=CustomFileBackend, + backend=FileBackend, backend_dependency=BackendParams, router_prefix="/mosaic", ) @@ -233,7 +483,7 @@ def test_MosaicTilerFactory_BackendParams(): with tmpmosaic() as mosaic_file: response = client.get( - "/mosaic/tilejson.json", + "/mosaic/WebMercatorQuad/tilejson.json", params={"url": mosaic_file}, ) assert response.json()["minzoom"] == 4 @@ -248,9 +498,10 @@ def _multiply_by_two(data, mask): def test_MosaicTilerFactory_PixelSelectionParams(): """Test MosaicTilerFactory factory with a customized default PixelSelectionMethod.""" - mosaic = MosaicTilerFactory(router_prefix="/mosaic") + mosaic = MosaicTilerFactory(backend=MosaicJSONBackend, router_prefix="/mosaic") mosaic_highest = MosaicTilerFactory( - pixel_selection_dependency=lambda: PixelSelectionMethod.highest.method(), + backend=MosaicJSONBackend, + pixel_selection_dependency=lambda: PixelSelectionMethod.highest.value, router_prefix="/mosaic_highest", ) @@ -260,14 +511,17 @@ def test_MosaicTilerFactory_PixelSelectionParams(): client = TestClient(app) with tmpmosaic() as mosaic_file: - response = client.get("/mosaic/tiles/7/37/45.npy", params={"url": mosaic_file}) + response = client.get( + "/mosaic/tiles/WebMercatorQuad/7/37/45.npy", params={"url": mosaic_file} + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" npy_tile = numpy.load(BytesIO(response.content)) assert npy_tile.shape == (4, 256, 256) # mask + data response = client.get( - "/mosaic_highest/tiles/7/37/45.npy", params={"url": mosaic_file} + "/mosaic_highest/tiles/WebMercatorQuad/7/37/45.npy", + params={"url": mosaic_file}, ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -277,23 +531,321 @@ def test_MosaicTilerFactory_PixelSelectionParams(): assert (npy_tile != npy_tile_highest).any() -def test_MosaicTilerFactory_strict_zoom(monkeypatch): +@patch("titiler.mosaic.factory.MOSAIC_STRICT_ZOOM", new=True) +def test_MosaicTilerFactory_strict_zoom(): """Test MosaicTilerFactory factory with STRICT Zoom Mode""" - monkeypatch.setenv("MOSAIC_STRICT_ZOOM", True) - - mosaic = MosaicTilerFactory() + mosaic = MosaicTilerFactory(backend=MosaicJSONBackend) app = FastAPI() app.include_router(mosaic.router) with TestClient(app) as client: with tmpmosaic() as mosaic_file: - response = client.get("/tiles/7/37/45.png", params={"url": mosaic_file}) + response = client.get( + "/tiles/WebMercatorQuad/7/37/45.png", params={"url": mosaic_file} + ) assert response.status_code == 200 - response = client.get("/tiles/6/18/22.png", params={"url": mosaic_file}) + response = client.get( + "/tiles/WebMercatorQuad/6/18/22.png", params={"url": mosaic_file} + ) assert response.status_code == 400 assert "Invalid ZOOM level 6" in response.text - response = client.get("/tiles/11/594/734.png", params={"url": mosaic_file}) + response = client.get( + "/tiles/WebMercatorQuad/11/594/734.png", params={"url": mosaic_file} + ) assert response.status_code == 400 assert "Invalid ZOOM level 11" in response.text + + +@dataclass +class AssetsAccessParams(DefaultDependency): + """Backend options to overwrite min/max zoom.""" + + limit: Annotated[int, Query()] = 10 + + +@attr.s +class CustomBackend(FileBackend): + """Custom FileBackend.""" + + def get_assets( + self, + x: int, + y: int, + z: int, + limit: int | None = None, + **kwargs: Any, + ) -> list[str]: + """Find assets.""" + assets = super().get_assets(x, y, z) + + if limit and len(assets) > limit: + return assets[:limit] + + return assets + + +def test_MosaicTilerFactory_asset_accessor(): + """Test MosaicTilerFactory factory with Backend dependency.""" + mosaic = MosaicTilerFactory( + backend=CustomBackend, + router_prefix="/mosaic", + assets_accessor_dependency=AssetsAccessParams, + ) + app = FastAPI() + app.include_router(mosaic.router, prefix="/mosaic") + client = TestClient(app) + + with tmpmosaic() as mosaic_file: + response = client.get( + "/mosaic/tiles/WGS1984Quad/8/148/61/assets", + params={"url": mosaic_file}, + ) + assert response.status_code == 200 + assert len(response.json()) == 2 + + response = client.get( + "/mosaic/tiles/WGS1984Quad/8/148/61/assets", + params={"url": mosaic_file, "limit": 1}, + ) + assert response.status_code == 200 + assert len(response.json()) == 1 + + +def test_ogc_maps_mosaic(): + """Test TilerFactory class.""" + mosaic = MosaicTilerFactory( + backend=MosaicJSONBackend, + add_ogc_maps=True, + ) + app = FastAPI() + app.include_router(mosaic.router) + add_exception_handlers(app, MOSAIC_STATUS_CODES) + + assert ( + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/core" in mosaic.conforms_to + ) + + with TestClient(app) as client: + with tmpmosaic() as mosaic_file: + # Conformance Class “Core” + response = client.get("/map", params={"url": mosaic_file}) + assert response.status_code == 501 + + # Conformance Class “Spatial Subsetting” + # /conf/spatial-subsetting/bbox-crs + response = client.get( + "/map", + params={ + "url": mosaic_file, + "bbox": "-74,45,-73,46", + }, + ) + headers = response.headers + # Default CRS for Mosaic is EPSG:4326 + assert ( + headers["Content-Crs"] == "" + ) + assert headers["Content-Bbox"] == "-74.0,45.0,-73.0,46.0" + assert headers["content-type"] == "image/png" + + response = client.get( + "/map", + params={ + "url": mosaic_file, + # tile 9-151-184 + "bbox": "-8218509.281222152,5557277.704445455,-8140237.7642581295,5635549.221409475", + "bbox-crs": "http://www.opengis.net/def/crs/EPSG/0/3857", + }, + ) + headers = response.headers + assert headers["Content-Crs"] == "" + assert ( + headers["Content-Bbox"] + == "-73.828125,44.590467181308846,-73.125,45.08903556483102" + ) + assert headers["content-type"] == "image/png" + + response = client.get( + "/map", + params={ + "url": mosaic_file, + # tile 9-151-184 + "bbox": "-8218509.281222152,5557277.704445455,-8140237.7642581295,5635549.221409475", + "bbox-crs": "[EPSG:3857]", + }, + ) + headers = response.headers + assert headers["Content-Crs"] == "" + assert ( + headers["Content-Bbox"] + == "-73.828125,44.590467181308846,-73.125,45.08903556483102" + ) + assert headers["content-type"] == "image/png" + + response = client.get( + "/map", + params={ + "url": mosaic_file, + # tile 9-151-184 + "bbox": "-8218509.281222152,5557277.704445455,-8140237.7642581295,5635549.221409475", + "bbox-crs": "[EPSG:3857]", + "crs": "[EPSG:3857]", + }, + ) + assert response.status_code == 200 + headers = response.headers + assert headers["Content-Crs"] == "" + assert ( + headers["Content-Bbox"] + == "-8218509.281222152,5557277.704445455,-8140237.7642581295,5635549.221409475" + ) + assert headers["content-type"] == "image/png" + + response = client.get( + "/map", + params={ + "url": mosaic_file, + # tile 4-4-5 + "bbox": "-10018754.171394622,5009377.085697312,-7514065.628545966,7514065.628545966", + "bbox-crs": "[EPSG:3857]", + "crs": "[EPSG:3857]", + }, + ) + assert response.status_code == 200 + headers = response.headers + assert headers["Content-Crs"] == "" + assert ( + headers["Content-Bbox"] + == "-10018754.171394622,5009377.085697312,-7514065.628545966,7514065.628545966" + ) + assert headers["content-type"] == "image/png" + meta = parse_img(response.content) + assert meta["width"] == 1024 # default max size + assert meta["height"] == 1024 + + response = client.get( + "/map", + params={ + "url": mosaic_file, + "bbox": "-74,45,-73,46", + }, + headers={"Accept": "image/jpeg"}, + ) + headers = response.headers + assert headers["content-type"] == "image/jpeg" + + response = client.get( + "/map", + params={ + "url": mosaic_file, + "bbox": "-74,45,-73,46", + "f": "tiff", + }, + ) + headers = response.headers + assert headers["content-type"] == "image/tiff; application=geotiff" + + # Conformance Class “Scaling” + response = client.get( + "/map", + params={ + "url": mosaic_file, + "bbox": "-74,45,-73,46", + "width": 256, + }, + ) + assert response.status_code == 200 + headers = response.headers + assert headers["content-type"] == "image/png" + meta = parse_img(response.content) + assert meta["width"] == 256 + assert meta["height"] == 256 + + response = client.get( + "/map", + params={ + "url": mosaic_file, + "bbox": "-74,45,-73,46", + "width": -256, + }, + ) + assert response.status_code == 422 + + # /req/scaling/height-definition + response = client.get( + "/map", + params={ + "url": mosaic_file, + "bbox": "-74,45,-73,46", + "height": 256, + }, + ) + assert response.status_code == 200 + headers = response.headers + assert headers["content-type"] == "image/png" + meta = parse_img(response.content) + assert meta["width"] == 256 + assert meta["height"] == 256 + + response = client.get( + "/map", + params={ + "url": mosaic_file, + "height": -256, + }, + ) + assert response.status_code == 422 + + +def test_wmts_extension_mosaic(): + """Test wmtsExtension extension.""" + mosaic = MosaicTilerFactory( + backend=MosaicJSONBackend, + extensions=[ + wmtsExtension( + get_renders=lambda obj: { + "one_band_limit": { + "tilematrixsets": { + "WebMercatorQuad": [0, 1], + }, + "spatial_extent": (-180, -90, 180, 90), + "bidx": [1, 2, 3], + "rescale": [[0, 10000], [0, 5000], [0, 1000]], + }, + "one_band": { + "bidx": [1], + "rescale": ["0,10000"], + }, + }, + ) + ], + ) + app = FastAPI() + app.include_router(mosaic.router) + add_exception_handlers(app, MOSAIC_STATUS_CODES) + + with TestClient(app) as client: + with tmpmosaic() as mosaic_file: + response = client.get(f"/WMTSCapabilities.xml?url={mosaic_file}") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/xml" + meta = parse_img(response.content) + assert meta["driver"] == "WMTS" + + wmts = WebMapTileService( + f"/WMTSCapabilities.xml?url={mosaic_file}", xml=response.content + ) + assert wmts.version == "1.0.0" + assert len(wmts.contents) == 39 # (2 renders + default) x 13 TMS + + layer = wmts.contents[f"{mosaic_file}_WebMercatorQuad_default"] + assert ["7", "8", "9"] == list( + layer.tilematrixsetlinks["WebMercatorQuad"].tilematrixlimits + ) + + layer = wmts.contents[f"{mosaic_file}_WebMercatorQuad_one_band_limit"] + assert ["0", "1"] == list( + layer.tilematrixsetlinks["WebMercatorQuad"].tilematrixlimits + ) diff --git a/src/titiler/mosaic/titiler/mosaic/__init__.py b/src/titiler/mosaic/titiler/mosaic/__init__.py index 29d6d4db2..4e2dca887 100644 --- a/src/titiler/mosaic/titiler/mosaic/__init__.py +++ b/src/titiler/mosaic/titiler/mosaic/__init__.py @@ -1,6 +1,6 @@ """titiler.mosaic""" -__version__ = "0.11.7" +__version__ = "2.0.0b2" from . import errors, factory # noqa from .factory import MosaicTilerFactory # noqa diff --git a/src/titiler/mosaic/titiler/mosaic/errors.py b/src/titiler/mosaic/titiler/mosaic/errors.py index 12cbc28e0..15e3ae881 100644 --- a/src/titiler/mosaic/titiler/mosaic/errors.py +++ b/src/titiler/mosaic/titiler/mosaic/errors.py @@ -1,18 +1,10 @@ """Titiler mosaic errors.""" -from cogeo_mosaic.errors import ( - MosaicAuthError, - MosaicError, - MosaicNotFoundError, - NoAssetFoundError, -) -from rio_tiler.errors import EmptyMosaicError +from rio_tiler.errors import EmptyMosaicError, NoAssetFoundError from starlette import status MOSAIC_STATUS_CODES = { - MosaicAuthError: status.HTTP_401_UNAUTHORIZED, - EmptyMosaicError: status.HTTP_404_NOT_FOUND, - MosaicNotFoundError: status.HTTP_404_NOT_FOUND, - NoAssetFoundError: status.HTTP_404_NOT_FOUND, - MosaicError: status.HTTP_424_FAILED_DEPENDENCY, + EmptyMosaicError: status.HTTP_204_NO_CONTENT, + NoAssetFoundError: status.HTTP_204_NO_CONTENT, + NotImplementedError: status.HTTP_501_NOT_IMPLEMENTED, } diff --git a/src/titiler/mosaic/titiler/mosaic/extensions/__init__.py b/src/titiler/mosaic/titiler/mosaic/extensions/__init__.py new file mode 100644 index 000000000..a5d0a4d4c --- /dev/null +++ b/src/titiler/mosaic/titiler/mosaic/extensions/__init__.py @@ -0,0 +1 @@ +"""titiler mosaic extensions module.""" diff --git a/src/titiler/mosaic/titiler/mosaic/extensions/mosaicjson.py b/src/titiler/mosaic/titiler/mosaic/extensions/mosaicjson.py new file mode 100644 index 000000000..7c70af004 --- /dev/null +++ b/src/titiler/mosaic/titiler/mosaic/extensions/mosaicjson.py @@ -0,0 +1,63 @@ +"""titiler.mosaic extensions.""" + +import logging + +import rasterio +from attrs import define +from fastapi import Depends + +from titiler.core.factory import FactoryExtension +from titiler.mosaic.factory import MosaicTilerFactory + +logger = logging.getLogger(__name__) + +try: + from cogeo_mosaic.mosaic import MosaicJSON +except ImportError: # pragma: nocover + MosaicJSON = None # type: ignore + + +@define +class MosaicJSONExtension(FactoryExtension): + """Add MosaicJSON specific endpoints""" + + def register(self, factory: MosaicTilerFactory): # type: ignore [override] # noqa: C901 + """Register endpoint to the tiler factory.""" + + ############################################################################ + # /read + ############################################################################ + + @factory.router.get( + "/", + response_model=MosaicJSON, + response_model_exclude_none=True, + responses={200: {"description": "Return MosaicJSON definition"}}, + operation_id=f"{factory.operation_prefix}getMosaicJSON", + ) + def read( + src_path=Depends(factory.path_dependency), + backend_params=Depends(factory.backend_dependency), + reader_params=Depends(factory.reader_dependency), + env=Depends(factory.environment_dependency), + ): + """Read a MosaicJSON""" + with rasterio.Env(**env): + logger.info( + f"opening data with backend: {factory.backend} and reader {factory.dataset_reader}" + ) + with factory.backend( + src_path, + reader=factory.dataset_reader, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), + ) as src_dst: + return src_dst.mosaic_def + + @factory.router.post( + "/validate", + operation_id=f"{factory.operation_prefix}validate", + ) + def validate(body: MosaicJSON): + """Validate a MosaicJSON""" + return True diff --git a/src/titiler/mosaic/titiler/mosaic/extensions/templates/wmts.xml b/src/titiler/mosaic/titiler/mosaic/extensions/templates/wmts.xml new file mode 100644 index 000000000..687b77aec --- /dev/null +++ b/src/titiler/mosaic/titiler/mosaic/extensions/templates/wmts.xml @@ -0,0 +1,90 @@ + + + Web Map Tile Service by TiTiler + OGC WMTS + 1.0.0 + + + TiTiler + + + + + + + + + + + RESTful + + + + + + + + + + + + + RESTful + + + + + + + + + {% for layer in layers -%} + + {{ layer.title }} + {{ layer.identifier }} + + {{ layer.bbox[0] }} {{ layer.bbox[1] }} + {{ layer.bbox[2] }} {{ layer.bbox[3] }} + + + {{ media_type }} + + {{ layer.tms_identifier }} + + {% for limit in layer.tms_limits %} + + {{ limit.tileMatrix }} + {{ limit.minTileRow }} + {{ limit.maxTileRow }} + {{ limit.minTileCol }} + {{ limit.maxTileCol }} + + {% endfor %} + + + + + {%- endfor %} + {% for tms in tileMatrixSets -%} + + {{ tms.id }} + {{ tms.crs }} + {% for matrix in tms.matrices %} + + {{ matrix.id }} + {{ matrix.scaleDenominator }} + {{ matrix.pointOfOrigin[0] }} {{ matrix.pointOfOrigin[1] }} + {{ matrix.tileWidth }} + {{ matrix.tileHeight }} + {{ matrix.matrixWidth }} + {{ matrix.matrixHeight }} + + {% endfor %} + + {%- endfor %} + + + diff --git a/src/titiler/mosaic/titiler/mosaic/extensions/wmts.py b/src/titiler/mosaic/titiler/mosaic/extensions/wmts.py new file mode 100644 index 000000000..bc9c578b1 --- /dev/null +++ b/src/titiler/mosaic/titiler/mosaic/extensions/wmts.py @@ -0,0 +1,265 @@ +"""titiler.mosaic wmts extensions.""" + +import warnings +from collections.abc import Callable +from typing import Annotated, Any +from urllib.parse import urlencode + +import jinja2 +import rasterio +from attrs import define, field +from fastapi import Depends, HTTPException, Query +from morecantile.models import crs_axis_inverted +from rasterio.crs import CRS +from rio_tiler.constants import WGS84_CRS +from rio_tiler.mosaic.backend import BaseBackend +from rio_tiler.utils import CRS_to_urn +from starlette.datastructures import QueryParams +from starlette.requests import Request +from starlette.templating import Jinja2Templates + +from titiler.core.factory import FactoryExtension +from titiler.core.resources.enums import ImageType +from titiler.core.resources.responses import XMLResponse +from titiler.core.utils import check_query_params, rio_crs_to_pyproj, tms_limits +from titiler.mosaic.factory import MosaicTilerFactory + +jinja2_env = jinja2.Environment( + autoescape=jinja2.select_autoescape(["xml"]), + loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), +) +DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) + + +@define +class wmtsExtension(FactoryExtension): + """RESTful WMTS service Extension for MosaicTilerFactory.""" + + # Geographic Coordinate Reference System. + crs: CRS = field(default=WGS84_CRS) + + templates: Jinja2Templates = field(default=DEFAULT_TEMPLATES) + + get_renders: Callable[[BaseBackend], dict[str, dict[str, Any]]] = field( + default=lambda obj: {} + ) + + def register(self, factory: MosaicTilerFactory): # type: ignore [override] # noqa: C901 + """Register endpoint to the tiler factory.""" + + @factory.router.get( + "/WMTSCapabilities.xml", + response_class=XMLResponse, + responses={ + 200: { + "content": {"application/xml": {}}, + "description": "Return RESTful WMTS service capabilities document.", + } + }, + operation_id=f"{factory.operation_prefix}getWMTS", + ) + def wmts( # noqa: C901 + request: Request, + tile_format: Annotated[ + ImageType, + Query(description="Output image type. Default is png."), + ] = ImageType.png, + use_epsg: Annotated[ + bool, + Query( + description="Use EPSG code, not opengis.net, for the ows:SupportedCRS in the TileMatrixSet (set to True to enable ArcMap compatability)" + ), + ] = False, + src_path=Depends(factory.path_dependency), + backend_params=Depends(factory.backend_dependency), + reader_params=Depends(factory.reader_dependency), + env=Depends(factory.environment_dependency), + ): + """OGC RESTful WMTS endpoint.""" + with rasterio.Env(**env): + with factory.backend( + src_path, + reader=factory.dataset_reader, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), + ) as src_dst: + bounds = src_dst.get_geographic_bounds(self.crs) + default_renders = self.get_renders(src_dst) + + # List of dependencies a `/tile` URL should validate + # Note: Those dependencies should only require Query() inputs + tile_dependencies: list[Callable] = [ + factory.layer_dependency, + factory.dataset_dependency, + factory.pixel_selection_dependency, + factory.process_dependency, + factory.colormap_dependency, + factory.render_dependency, + factory.tile_dependency, + factory.assets_accessor_dependency, + factory.reader_dependency, + factory.backend_dependency, + ] + renders: list[dict[str, Any]] = [] + + ########################################## + # 1. Create layers from `renders` metadata + for name, values in default_renders.items(): + values.pop("tilesize", None) # Ensure tilesize is not overridden + if check_query_params(tile_dependencies, values): + renders.append( + { + "name": name, + "query_string": urlencode(values, doseq=True) + if values + else None, + "tilematrixsets": values.get("tilematrixsets", {}), + "spatial_extent": values.get("spatial_extent", None), + } + ) + else: + warnings.warn( + f"Cannot construct URL for layer `{name}`", + UserWarning, + stacklevel=2, + ) + + ####################################### + # 2. Create layer from query-parameters + qs_key_to_remove = [ + "tile_format", + "use_epsg", + # Make sure tilesize is not ovewrriden from WMTS request + "tilesize", + # OGC WMTS parameters to ignore + "service", + "request", + "acceptversions", + "sections", + "updatesequence", + "acceptformats", + ] + + qs = urlencode( + [ + (key, value) + for (key, value) in request.query_params._list + if key.lower() not in qs_key_to_remove + ], + doseq=True, + ) + + if check_query_params(tile_dependencies, QueryParams(qs)): + renders.append({"name": "default", "query_string": qs}) + + ################################################# + # 3. if there is no layers we raise and exception + if not renders: + raise HTTPException( + status_code=400, + detail="Could not find any valid layers in metadata or construct one from Query Parameters.", + ) + + layers: list[dict[str, Any]] = [] + title = src_path if isinstance(src_path, str) else "TiTiler Mosaic" + + for render in renders: + for tms_id in factory.supported_tms.list(): + tms = factory.supported_tms.get(tms_id) + try: + with factory.backend( + src_path, + tms=tms, + reader=factory.dataset_reader, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), + ) as src_dst: + # NOTE: Custom TiTiler Render key in form of {"tilematrixsets": {"{TMS_ID}": (minzoom, maxzoom)}} + if zooms := render.get("tilematrixsets", {}).get( + tms_id + ): + minzoom, maxzoom = zooms + else: + minzoom = src_dst.minzoom + maxzoom = src_dst.maxzoom + + # NOTE: Custom TiTiler Render key in form of {"spatial_extent": (minx, miny, maxx, maxy)} + # NOTE: We assume the spatial_extent is always in WGS84 + if render.get("spatial_extent"): + bbox = render["spatial_extent"] + crs = WGS84_CRS + else: + bbox = bounds + crs = self.crs + + tilematrixset_limits = tms_limits( + tms, + bbox, + zooms=(minzoom, maxzoom), + geographic_crs=crs, + ) + + except Exception as e: # noqa + pass + + route_params = { + "z": "{TileMatrix}", + "x": "{TileCol}", + "y": "{TileRow}", + "format": tile_format.value, + "tileMatrixSetId": tms_id, + } + + bbox_crs_type = "WGS84BoundingBox" + bbox_crs_uri = "urn:ogc:def:crs:OGC:2:84" + if crs != WGS84_CRS: + bbox_crs_type = "BoundingBox" + bbox_crs_uri = CRS_to_urn(crs) # type: ignore + # WGS88BoundingBox is always xy ordered, but BoundingBox must match the CRS order + proj_crs = rio_crs_to_pyproj(crs) + if crs_axis_inverted(proj_crs): + # match the bounding box coordinate order to the CRS + bbox = [bbox[1], bbox[0], bbox[3], bbox[2]] + + layers.append( + { + "title": f"{title}_{tms_id}_{render['name']}", + "identifier": f"{title}_{tms_id}_{render['name']}", + "tms_identifier": tms_id, + "tms_limits": tilematrixset_limits, + "tiles_url": factory.url_for( + request, "tile", **route_params + ), + "query_string": render["query_string"], + "bbox_crs_type": bbox_crs_type, + "bbox_crs_uri": bbox_crs_uri, + "bbox": bbox, + } + ) + + tileMatrixSets: list[dict[str, Any]] = [] + for tms_id in factory.supported_tms.list(): + tms = factory.supported_tms.get(tms_id) + if use_epsg: + supported_crs = f"EPSG:{tms.crs.to_epsg()}" + else: + supported_crs = tms.crs.srs + + tileMatrixSets.append( + { + "id": tms_id, + "crs": supported_crs, + "matrices": tms.tileMatrices, + } + ) + + return self.templates.TemplateResponse( + request, + name="wmts.xml", + context={ + "layers": layers, + "tileMatrixSets": tileMatrixSets, + "media_type": tile_format.mediatype, + }, + media_type="application/xml", + ) diff --git a/src/titiler/mosaic/titiler/mosaic/factory.py b/src/titiler/mosaic/titiler/mosaic/factory.py index 2a618266c..f6f771d68 100644 --- a/src/titiler/mosaic/titiler/mosaic/factory.py +++ b/src/titiler/mosaic/titiler/mosaic/factory.py @@ -1,290 +1,629 @@ """TiTiler.mosaic Router factories.""" +import logging import os -from dataclasses import dataclass -from typing import Callable, Dict, Literal, Optional, Type, Union +from collections.abc import Callable +from typing import Annotated, Any, Literal from urllib.parse import urlencode import rasterio -from cogeo_mosaic.backends import BaseBackend, MosaicBackend -from cogeo_mosaic.models import Info as mosaicInfo -from cogeo_mosaic.mosaic import MosaicJSON -from fastapi import Depends, HTTPException, Path, Query -from geojson_pydantic.features import Feature +from attrs import define, field +from fastapi import Body, Depends, HTTPException, Path, Query +from geojson_pydantic.features import Feature, FeatureCollection from geojson_pydantic.geometries import Polygon -from morecantile import tms +from morecantile import tms as morecantile_tms from morecantile.defaults import TileMatrixSets -from rio_tiler.constants import MAX_THREADS -from rio_tiler.io import BaseReader, MultiBandReader, MultiBaseReader, Reader -from rio_tiler.models import Bounds +from pydantic import Field +from rio_tiler.constants import MAX_THREADS, WGS84_CRS +from rio_tiler.io import BaseReader, MultiBaseReader, Reader +from rio_tiler.mosaic.backend import BaseBackend, MosaicInfo +from rio_tiler.mosaic.methods import PixelSelectionMethod from rio_tiler.mosaic.methods.base import MosaicMethodBase +from rio_tiler.types import ColorMapType +from rio_tiler.utils import CRS_to_uri from starlette.requests import Request from starlette.responses import HTMLResponse, Response +from starlette.routing import NoMatchFound -from titiler.core.dependencies import DefaultDependency -from titiler.core.factory import BaseTilerFactory, img_endpoint_params +from titiler.core.algorithm import BaseAlgorithm +from titiler.core.algorithm import algorithms as available_algorithms +from titiler.core.dependencies import ( + BidxExprParams, + ColorMapParams, + CoordCRSParams, + CRSParams, + DatasetParams, + DefaultDependency, + DstCRSParams, + HistogramParams, + ImageRenderingParams, + OGCMapsParams, + PartFeatureParams, + StatisticsParams, + TileParams, +) +from titiler.core.factory import BaseFactory, img_endpoint_params from titiler.core.models.mapbox import TileJSON +from titiler.core.models.OGC import TileSet, TileSetList +from titiler.core.models.responses import StatisticsGeoJSON from titiler.core.resources.enums import ImageType, MediaType, OptionalHeader -from titiler.core.resources.responses import GeoJSONResponse, JSONResponse, XMLResponse +from titiler.core.resources.responses import GeoJSONResponse, JSONResponse +from titiler.core.utils import ( + accept_media_type, + bounds_to_geometry, + create_html_response, + render_image, + tms_limits, +) from titiler.mosaic.models.responses import Point -from titiler.mosaic.resources.enums import PixelSelectionMethod -# BaseBackend does not support other TMS than WebMercator -mosaic_tms = TileMatrixSets({"WebMercatorQuad": tms.get("WebMercatorQuad")}) +MOSAIC_THREADS = int(os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS)) +MOSAIC_STRICT_ZOOM = str(os.getenv("MOSAIC_STRICT_ZOOM", False)).lower() in [ + "true", + "yes", +] + +logger = logging.getLogger(__name__) def PixelSelectionParams( - pixel_selection: PixelSelectionMethod = Query( - PixelSelectionMethod.first, - description="Pixel selection method.", - ) + pixel_selection: Annotated[ # type: ignore + Literal[tuple([e.name for e in PixelSelectionMethod])], + Query(description="Pixel selection method."), + ] = "first", ) -> MosaicMethodBase: """ Returns the mosaic method used to combine datasets together. """ - return pixel_selection.method() + return PixelSelectionMethod[pixel_selection].value() -@dataclass -class MosaicTilerFactory(BaseTilerFactory): - """ - MosaicTiler Factory. +def DatasetPathParams(url: Annotated[str, Query(description="Mosaic URL")]) -> str: + """Create dataset path from args""" + return url - The main difference with titiler.endpoint.factory.TilerFactory is that this factory - needs the `reader` to be of `cogeo_mosaic.backends.BaseBackend` type (e.g MosaicBackend) and a `dataset_reader` (BaseReader). - """ - reader: Type[BaseBackend] = MosaicBackend - dataset_reader: Union[ - Type[BaseReader], - Type[MultiBaseReader], - Type[MultiBandReader], - ] = Reader +@define(kw_only=True) +class MosaicTilerFactory(BaseFactory): + """MosaicTiler Factory.""" + + backend: type[BaseBackend] + backend_dependency: type[DefaultDependency] = DefaultDependency + + dataset_reader: type[BaseReader] | type[MultiBaseReader] = Reader + reader_dependency: type[DefaultDependency] = DefaultDependency - backend_dependency: Type[DefaultDependency] = DefaultDependency + # Path Dependency + path_dependency: Callable[..., Any] = DatasetPathParams + + # Backend.get_assets() Options + assets_accessor_dependency: type[DefaultDependency] = DefaultDependency + + # Indexes/Expression Dependencies + layer_dependency: type[DefaultDependency] = BidxExprParams + + # Rasterio Dataset Options (nodata, unscale, resampling, reproject) + dataset_dependency: type[DefaultDependency] = DatasetParams + + # Tile/Tilejson Dependencies + tile_dependency: type[DefaultDependency] = TileParams + + # Post Processing Dependencies (algorithm) + process_dependency: Callable[..., BaseAlgorithm | None] = ( + available_algorithms.dependency + ) + + # Statistics/Histogram Dependencies + stats_dependency: type[DefaultDependency] = StatisticsParams + histogram_dependency: type[DefaultDependency] = HistogramParams + + # Crop endpoints Dependencies + img_part_dependency: type[DefaultDependency] = PartFeatureParams + + # Image rendering Dependencies + colormap_dependency: Callable[..., ColorMapType | None] = ColorMapParams + render_dependency: type[DefaultDependency] = ImageRenderingParams pixel_selection_dependency: Callable[..., MosaicMethodBase] = PixelSelectionParams - supported_tms: TileMatrixSets = mosaic_tms - default_tms: str = "WebMercatorQuad" + # GDAL ENV dependency + environment_dependency: Callable[..., dict] = field(default=lambda: {}) + + supported_tms: TileMatrixSets = morecantile_tms + + render_func: Callable[..., tuple[bytes, str]] = render_image + + optional_headers: list[OptionalHeader] = field(factory=list) # Add/Remove some endpoints add_viewer: bool = True + add_statistics: bool = False + add_part: bool = False + add_ogc_maps: bool = False - def register_routes(self): - """ - This Method register routes to the router. - - Because we wrap the endpoints in a class we cannot define the routes as - methods (because of the self argument). The HACK is to define routes inside - the class method and register them after the class initialization. + conforms_to: set[str] = field( + factory=lambda: { + # https://docs.ogc.org/is/20-057/20-057.html#toc30 + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tileset", + # https://docs.ogc.org/is/20-057/20-057.html#toc34 + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tilesets-list", + # https://docs.ogc.org/is/20-057/20-057.html#toc65 + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/core", + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/png", + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/jpeg", + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tiff", + } + ) - """ + def register_routes(self): + """This Method register routes to the router.""" - self.read() - self.bounds() self.info() + self.tilesets() self.tile() + if self.add_viewer: + self.map_viewer() self.tilejson() - self.wmts() self.point() - self.validate() self.assets() - # Optional Routes - if self.add_viewer: - self.map_viewer() + if self.add_part: + self.part() + + if self.add_statistics: + self.statistics() + + if self.add_ogc_maps: + self.ogc_maps() ############################################################################ - # /read + # /info ############################################################################ - def read(self): - """Register / (Get) Read endpoint.""" + def info(self): + """Register /info endpoint""" @self.router.get( - "/", - response_model=MosaicJSON, - response_model_exclude_none=True, - responses={200: {"description": "Return MosaicJSON definition"}}, + "/info", + responses={ + 200: { + "description": "Return info about the MosaicJSON", + "model": MosaicInfo, + } + }, + operation_id=f"{self.operation_prefix}getInfo", ) - def read( + def info( src_path=Depends(self.path_dependency), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): - """Read a MosaicJSON""" + """Return basic info.""" with rasterio.Env(**env): - with self.reader( + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( src_path, reader=self.dataset_reader, - reader_options={**reader_params}, - **backend_params, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), ) as src_dst: - return src_dst.mosaic_def - - ############################################################################ - # /bounds - ############################################################################ - def bounds(self): - """Register /bounds endpoint.""" + return src_dst.info() @self.router.get( - "/bounds", - response_model=Bounds, - responses={200: {"description": "Return the bounds of the MosaicJSON"}}, + "/info.geojson", + response_model=Feature, + response_model_exclude_none=True, + response_class=GeoJSONResponse, + responses={ + 200: { + "content": {"application/geo+json": {}}, + "description": "Return mosaic's basic info as a GeoJSON feature.", + "model": Feature[Polygon, MosaicInfo], + } + }, + operation_id=f"{self.operation_prefix}getInfoGeoJSON", ) - def bounds( + def info_geojson( src_path=Depends(self.path_dependency), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): - """Return the bounds of the MosaicJSON.""" + """Return mosaic's basic info as a GeoJSON feature.""" with rasterio.Env(**env): - with self.reader( + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( src_path, reader=self.dataset_reader, - reader_options={**reader_params}, - **backend_params, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), ) as src_dst: - return {"bounds": src_dst.geographic_bounds} + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + geometry = bounds_to_geometry(bounds) + + return Feature( + type="Feature", + bbox=bounds, + geometry=geometry, + properties=src_dst.info(), + ) ############################################################################ - # /info + # /tileset ############################################################################ - def info(self): - """Register /info endpoint""" + def tilesets(self): # noqa: C901 + """Register OGC tilesets endpoints.""" @self.router.get( - "/info", - response_model=mosaicInfo, - responses={200: {"description": "Return info about the MosaicJSON"}}, + "/tiles", + response_model=TileSetList, + response_class=JSONResponse, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "application/json": {}, + "text/html": {}, + } + } + }, + summary="Retrieve a list of available raster tilesets for the specified dataset.", + operation_id=f"{self.operation_prefix}getTileSetList", ) - def info( + async def tileset_list( + request: Request, src_path=Depends(self.path_dependency), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, ): - """Return basic info.""" + """Retrieve a list of available raster tilesets for the specified dataset.""" with rasterio.Env(**env): - with self.reader( + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( src_path, reader=self.dataset_reader, - reader_options={**reader_params}, - **backend_params, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), ) as src_dst: - return src_dst.info() + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + + collection_bbox = { + "lowerLeft": [bounds[0], bounds[1]], + "upperRight": [bounds[2], bounds[3]], + "crs": CRS_to_uri(crs or WGS84_CRS), + } + + qs = [ + (key, value) + for (key, value) in request.query_params._list + if key.lower() not in ["crs"] + ] + query_string = f"?{urlencode(qs)}" if qs else "" + + tilesets: list[dict[str, Any]] = [] + for tms in self.supported_tms.list(): + tileset: dict[str, Any] = { + "title": f"tileset tiled using {tms} TileMatrixSet", + "dataType": "map", + "crs": self.supported_tms.get(tms).crs, + "boundingBox": collection_bbox, + "links": [ + { + "href": self.url_for( + request, "tileset", tileMatrixSetId=tms + ) + + query_string, + "rel": "self", + "type": "application/json", + "title": f"Tileset tiled using {tms} TileMatrixSet", + }, + { + "href": self.url_for( + request, + "tile", + tileMatrixSetId=tms, + z="{z}", + x="{x}", + y="{y}", + ) + + query_string, + "rel": "tile", + "title": "Templated link for retrieving Raster tiles", + }, + ], + } + + try: + tileset["links"].append( + { + "href": str( + request.url_for("tilematrixset", tileMatrixSetId=tms) + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type": "application/json", + "title": f"Definition of '{tms}' tileMatrixSet", + } + ) + except NoMatchFound: + pass + + tilesets.append(tileset) + + data = TileSetList.model_validate({"tilesets": tilesets}) + + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) + + if output_type == MediaType.html: + return create_html_response( + request, + data.model_dump(exclude_none=True, mode="json"), + title="Tilesets", + template_name="tilesets", + templates=self.templates, + ) + + return data @self.router.get( - "/info.geojson", - response_model=Feature[Polygon, mosaicInfo], + "/tiles/{tileMatrixSetId}", + response_model=TileSet, + response_class=JSONResponse, response_model_exclude_none=True, - response_class=GeoJSONResponse, responses={ 200: { - "content": {"application/geo+json": {}}, - "description": "Return mosaic's basic info as a GeoJSON feature.", + "content": { + "application/json": {}, + "text/html": {}, + } } }, + summary="Retrieve the raster tileset metadata for the specified dataset and tiling scheme (tile matrix set).", + operation_id=f"{self.operation_prefix}getTileSet", ) - def info_geojson( + async def tileset( + request: Request, + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], src_path=Depends(self.path_dependency), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, ): - """Return mosaic's basic info as a GeoJSON feature.""" + """Retrieve the raster tileset metadata for the specified dataset and tiling scheme (tile matrix set).""" + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader( + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( src_path, + tms=tms, reader=self.dataset_reader, - reader_options={**reader_params}, - **backend_params, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), ) as src_dst: - info = src_dst.info() - return Feature( - type="Feature", - geometry=Polygon.from_bounds(*info.bounds), - properties=info, + bounds = src_dst.get_geographic_bounds(tms.rasterio_geographic_crs) + minzoom = src_dst.minzoom + maxzoom = src_dst.maxzoom + + collection_bbox = { + "lowerLeft": [bounds[0], bounds[1]], + "upperRight": [bounds[2], bounds[3]], + "crs": CRS_to_uri(tms.rasterio_geographic_crs), + } + + tilematrix_limits = tms_limits( + tms, + bounds, + zooms=(minzoom, maxzoom), ) + query_string = ( + f"?{urlencode(request.query_params._list)}" + if request.query_params._list + else "" + ) + + links = [ + { + "href": self.url_for( + request, + "tileset", + tileMatrixSetId=tileMatrixSetId, + ), + "rel": "self", + "type": "application/json", + "title": f"Tileset tiled using {tileMatrixSetId} TileMatrixSet", + }, + { + "href": self.url_for( + request, + "tile", + tileMatrixSetId=tileMatrixSetId, + z="{z}", + x="{x}", + y="{y}", + ) + + query_string, + "rel": "tile", + "title": "Templated link for retrieving Raster tiles", + "templated": True, + }, + ] + try: + links.append( + { + "href": str( + request.url_for( + "tilematrixset", tileMatrixSetId=tileMatrixSetId + ) + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type": "application/json", + "title": f"Definition of '{tileMatrixSetId}' tileMatrixSet", + } + ) + except NoMatchFound: + pass + + if self.add_viewer: + links.append( + { + "href": self.url_for( + request, + "map_viewer", + tileMatrixSetId=tileMatrixSetId, + ) + + query_string, + "type": "text/html", + "rel": "data", + "title": f"Map viewer for '{tileMatrixSetId}' tileMatrixSet", + } + ) + + data = TileSet.model_validate( + { + "title": f"tileset tiled using {tileMatrixSetId} TileMatrixSet", + "dataType": "map", + "crs": tms.crs, + "boundingBox": collection_bbox, + "links": links, + "tileMatrixSetLimits": tilematrix_limits, + } + ) + + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) + + if output_type == MediaType.html: + return create_html_response( + request, + data, + title=tileMatrixSetId, + template_name="tileset", + templates=self.templates, + ) + + return data + ############################################################################ # /tiles ############################################################################ def tile(self): # noqa: C901 """Register /tiles endpoints.""" - @self.router.get(r"/tiles/{z}/{x}/{y}", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) - @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params - ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}", + operation_id=f"{self.operation_prefix}getTile", + **img_endpoint_params, ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}", + operation_id=f"{self.operation_prefix}getTileWithFormat", **img_endpoint_params, ) def tile( - z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), - x: int = Path(..., description="Mercator tiles's column"), - y: int = Path(..., description="Mercator tiles's row"), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), # noqa - scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - format: ImageType = Query( - None, description="Output image type. Default is auto." - ), + z: Annotated[ + int, + Path( + description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.", + ), + ], + x: Annotated[ + int, + Path( + description="Column (X) index of the tile on the selected TileMatrix. It cannot exceed the MatrixHeight-1 for the selected TileMatrix.", + ), + ], + y: Annotated[ + int, + Path( + description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.", + ), + ], + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + format: Annotated[ + ImageType | None, + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, + tilesize: Annotated[ + int | None, + Query(gt=0, description="Tilesize in pixels."), + ] = None, src_path=Depends(self.path_dependency), + backend_params=Depends(self.backend_dependency), + reader_params=Depends(self.reader_dependency), + assets_accessor_params=Depends(self.assets_accessor_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), pixel_selection=Depends(self.pixel_selection_dependency), - buffer: Optional[float] = Query( - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * tile_buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), + tile_params=Depends(self.tile_dependency), post_process=Depends(self.process_dependency), - rescale=Depends(self.rescale_dependency), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), - backend_params=Depends(self.backend_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Create map tile from a COG.""" - threads = int(os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS)) - - strict_zoom = str(os.getenv("MOSAIC_STRICT_ZOOM", False)).lower() in [ - "true", - "yes", - ] - + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader( + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( src_path, + tms=tms, reader=self.dataset_reader, - reader_options={**reader_params}, - **backend_params, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), ) as src_dst: - - if strict_zoom and (z < src_dst.minzoom or z > src_dst.maxzoom): + if MOSAIC_STRICT_ZOOM and ( + z < src_dst.minzoom or z > src_dst.maxzoom + ): raise HTTPException( 400, f"Invalid ZOOM level {z}. Should be between {src_dst.minzoom} and {src_dst.maxzoom}", @@ -294,95 +633,91 @@ def tile( x, y, z, + tilesize=tilesize, + search_options=assets_accessor_params.as_dict(), pixel_selection=pixel_selection, - tilesize=scale * 256, - threads=threads, - buffer=buffer, - **layer_params, - **dataset_params, + threads=MOSAIC_THREADS, + **tile_params.as_dict(), + **layer_params.as_dict(), + **dataset_params.as_dict(), ) if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) - - if color_formula: - image.apply_color_formula(color_formula) - - if colormap: - image = image.apply_colormap(colormap) - - if not format: - format = ImageType.jpeg if image.mask.all() else ImageType.png - - content = image.render( - img_format=format.driver, - **format.profile, - **render_params, + content, media_type = self.render_func( + image, + output_format=format, + colormap=colormap, + **render_params.as_dict(), ) - headers: Dict[str, str] = {} + headers: dict[str, str] = {} if OptionalHeader.x_assets in self.optional_headers: headers["X-Assets"] = ",".join(assets) - return Response(content, media_type=format.mediatype, headers=headers) + if image.bounds is not None: + headers["Content-Bbox"] = ",".join(map(str, image.bounds)) + if uri := CRS_to_uri(image.crs): + headers["Content-Crs"] = f"<{uri}>" + + if ( + OptionalHeader.server_timing in self.optional_headers + and image.metadata.get("timings") + ): + headers["Server-Timing"] = ", ".join( + [f"{name};dur={time}" for (name, time) in image.metadata["timings"]] + ) + + return Response(content, media_type=media_type, headers=headers) def tilejson(self): # noqa: C901 """Add tilejson endpoint.""" @self.router.get( - "/tilejson.json", - response_model=TileJSON, - responses={200: {"description": "Return a tilejson"}}, - response_model_exclude_none=True, - ) - @self.router.get( - "/{TileMatrixSetId}/tilejson.json", + "/{tileMatrixSetId}/tilejson.json", response_model=TileJSON, responses={200: {"description": "Return a tilejson"}}, response_model_exclude_none=True, + operation_id=f"{self.operation_prefix}getTileJSON", ) def tilejson( request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), # noqa + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + tilesize: Annotated[ + int | None, + Query(gt=0, description="Tilesize in pixels. Default to 512."), + ] = None, + tile_format: Annotated[ + ImageType | None, + Query( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, + minzoom: Annotated[ + int | None, + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + int | None, + Query(description="Overwrite default maxzoom."), + ] = None, src_path=Depends(self.path_dependency), - tile_format: Optional[ImageType] = Query( - None, description="Output image type. Default is auto." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - pixel_selection=Depends(self.pixel_selection_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * tile_buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), + assets_accessor_params=Depends(self.assets_accessor_dependency), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + pixel_selection=Depends(self.pixel_selection_dependency), + tile_params=Depends(self.tile_dependency), + post_process=Depends(self.process_dependency), + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), env=Depends(self.environment_dependency), ): """Return TileJSON document for a COG.""" @@ -390,8 +725,7 @@ def tilejson( "z": "{z}", "x": "{x}", "y": "{y}", - "scale": tile_scale, - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } if tile_format: route_params["format"] = tile_format.value @@ -400,342 +734,689 @@ def tilejson( qs_key_to_remove = [ "tilematrixsetid", "tile_format", - "tile_scale", "minzoom", "maxzoom", ] - qs = [ + qs: list[tuple[str, Any]] = [ (key, value) for (key, value) in request.query_params._list if key.lower() not in qs_key_to_remove ] + if "tilesize" not in request.query_params: + qs.append(("tilesize", 512)) + if qs: tiles_url += f"?{urlencode(qs)}" + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader( + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( src_path, + tms=tms, reader=self.dataset_reader, - reader_options={**reader_params}, - **backend_params, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), ) as src_dst: - center = list(src_dst.mosaic_def.center) - if minzoom is not None: - center[-1] = minzoom + bounds = src_dst.get_geographic_bounds(tms.rasterio_geographic_crs) + minzoom = minzoom if minzoom is not None else src_dst.minzoom + maxzoom = maxzoom if maxzoom is not None else src_dst.maxzoom + center = ( + (bounds[0] + bounds[2]) / 2, + (bounds[1] + bounds[3]) / 2, + minzoom, + ) return { - "bounds": src_dst.bounds, - "center": tuple(center), - "minzoom": minzoom if minzoom is not None else src_dst.minzoom, - "maxzoom": maxzoom if maxzoom is not None else src_dst.maxzoom, + "bounds": bounds, + "center": center, + "minzoom": minzoom, + "maxzoom": maxzoom, "tiles": [tiles_url], + "attribution": os.environ.get("TITILER_DEFAULT_ATTRIBUTION"), } def map_viewer(self): # noqa: C901 - """Register /map endpoint.""" + """Register /map.html endpoint.""" - @self.router.get("/map", response_class=HTMLResponse) - @self.router.get("/{TileMatrixSetId}/map", response_class=HTMLResponse) + @self.router.get( + "/{tileMatrixSetId}/map.html", + response_class=HTMLResponse, + operation_id=f"{self.operation_prefix}getMapViewer", + ) def map_viewer( request: Request, + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + tile_format: Annotated[ + ImageType | None, + Query( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, + tilesize: Annotated[ + int, + Query(gt=0, description="Tilesize in pixels. Default to 256."), + ] = 256, + minzoom: Annotated[ + int | None, + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + int | None, + Query(description="Overwrite default maxzoom."), + ] = None, src_path=Depends(self.path_dependency), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), - tile_format: Optional[ImageType] = Query( - None, description="Output image type. Default is auto." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - pixel_selection=Depends(self.pixel_selection_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * tile_buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa - backend_params=Depends(self.backend_dependency), # noqa - reader_params=Depends(self.reader_dependency), # noqa - env=Depends(self.environment_dependency), # noqa + backend_params=Depends(self.backend_dependency), + reader_params=Depends(self.reader_dependency), + assets_accessor_params=Depends(self.assets_accessor_dependency), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + pixel_selection=Depends(self.pixel_selection_dependency), + tile_params=Depends(self.tile_dependency), + post_process=Depends(self.process_dependency), + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), + env=Depends(self.environment_dependency), ): """Return TileJSON document for a dataset.""" tilejson_url = self.url_for( - request, "tilejson", TileMatrixSetId=TileMatrixSetId + request, + "tilejson", + tileMatrixSetId=tileMatrixSetId, ) - if request.query_params._list: - tilejson_url += f"?{urlencode(request.query_params._list)}" + qs = list(request.query_params._list) + if "tilesize" not in request.query_params: + qs.append(("tilesize", tilesize)) + + tilejson_url += f"?{urlencode(qs)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) return self.templates.TemplateResponse( + request, name="map.html", context={ - "request": request, "tilejson_endpoint": tilejson_url, "tms": tms, - "resolutions": [tms._resolution(matrix) for matrix in tms], + "resolutions": [matrix.cellSize for matrix in tms], }, media_type="text/html", ) - def wmts(self): # noqa: C901 - """Add wmts endpoint.""" + ############################################################################ + # /point (Optional) + ############################################################################ + def point(self): + """Register /point endpoint.""" - @self.router.get("/WMTSCapabilities.xml", response_class=XMLResponse) @self.router.get( - "/{TileMatrixSetId}/WMTSCapabilities.xml", response_class=XMLResponse + "/point/{lon},{lat}", + response_model=Point, + response_class=JSONResponse, + responses={200: {"description": "Return a value for a point"}}, + operation_id=f"{self.operation_prefix}getDataForPoint", ) - def wmts( - request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), # noqa + def point( + lon: Annotated[float, Path(description="Longitude")], + lat: Annotated[float, Path(description="Latitude")], src_path=Depends(self.path_dependency), - tile_format: ImageType = Query( - ImageType.png, description="Output image type. Default is png." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - pixel_selection=Depends(self.pixel_selection_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * tile_buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), + assets_accessor_params=Depends(self.assets_accessor_dependency), + coord_crs=Depends(CoordCRSParams), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), env=Depends(self.environment_dependency), ): - """OGC WMTS endpoint.""" - route_params = { - "z": "{TileMatrix}", - "x": "{TileCol}", - "y": "{TileRow}", - "scale": tile_scale, - "format": tile_format.value, - "TileMatrixSetId": TileMatrixSetId, + """Get Point value for a Mosaic.""" + with rasterio.Env(**env): + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( + src_path, + reader=self.dataset_reader, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), + ) as src_dst: + values = src_dst.point( + lon, + lat, + coord_crs=coord_crs or WGS84_CRS, + search_options=assets_accessor_params.as_dict(), + threads=MOSAIC_THREADS, + **layer_params.as_dict(), + **dataset_params.as_dict(), + ) + + return { + "coordinates": [lon, lat], + "assets": [ + { + "name": asset_name, + "values": pt.array.tolist(), + "band_names": pt.band_names, + "band_descriptions": pt.band_descriptions, + } + for asset_name, pt in values + ], } - tiles_url = self.url_for(request, "tile", **route_params) - qs_key_to_remove = [ - "tilematrixsetid", - "tile_format", - "tile_scale", - "minzoom", - "maxzoom", - "service", - "request", - ] - qs = [ - (key, value) - for (key, value) in request.query_params._list - if key.lower() not in qs_key_to_remove - ] - if qs: - tiles_url += f"?{urlencode(qs)}" + def statistics(self): + """Register /statistics endpoint.""" + + @self.router.post( + "/statistics", + response_model=StatisticsGeoJSON, + response_model_exclude_none=True, + response_class=GeoJSONResponse, + responses={ + 200: { + "content": {"application/geo+json": {}}, + "description": "Return statistics for geojson features.", + } + }, + operation_id=f"{self.operation_prefix}postStatisticsForGeoJSON", + ) + def geojson_statistics( + geojson: Annotated[ + FeatureCollection | Feature, + Body(description="GeoJSON Feature or FeatureCollection."), + ], + src_path=Depends(self.path_dependency), + backend_params=Depends(self.backend_dependency), + reader_params=Depends(self.reader_dependency), + assets_accessor_params=Depends(self.assets_accessor_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + pixel_selection=Depends(self.pixel_selection_dependency), + image_params=Depends(self.img_part_dependency), + post_process=Depends(self.process_dependency), + stats_params=Depends(self.stats_dependency), + histogram_params=Depends(self.histogram_dependency), + env=Depends(self.environment_dependency), + ): + """Get Statistics from a geojson feature or featureCollection.""" + fc = geojson + if isinstance(fc, Feature): + fc = FeatureCollection(type="FeatureCollection", features=[geojson]) - tms = self.supported_tms.get(TileMatrixSetId) with rasterio.Env(**env): - with self.reader( + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( src_path, reader=self.dataset_reader, - reader_options={**reader_params}, - **backend_params, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), ) as src_dst: - bounds = src_dst.geographic_bounds - minzoom = minzoom if minzoom is not None else src_dst.minzoom - maxzoom = maxzoom if maxzoom is not None else src_dst.maxzoom + for i, feature in enumerate(fc.features): + shape = feature.model_dump(exclude_none=True) - tileMatrix = [] - for zoom in range(minzoom, maxzoom + 1): - matrix = tms.matrix(zoom) - tm = f""" - - {matrix.identifier} - {matrix.scaleDenominator} - {matrix.topLeftCorner[0]} {matrix.topLeftCorner[1]} - {matrix.tileWidth} - {matrix.tileHeight} - {matrix.matrixWidth} - {matrix.matrixHeight} - """ - tileMatrix.append(tm) + logger.info(f"feature {i}: reading data") + image, assets = src_dst.feature( + shape, + shape_crs=coord_crs or WGS84_CRS, + dst_crs=dst_crs, + align_bounds_with_dataset=True, + search_options=assets_accessor_params.as_dict(), + pixel_selection=pixel_selection, + threads=MOSAIC_THREADS, + **layer_params.as_dict(), + **dataset_params.as_dict(), + **image_params.as_dict(), + ) - return self.templates.TemplateResponse( - "wmts.xml", - { - "request": request, - "tiles_endpoint": tiles_url, - "bounds": bounds, - "tileMatrix": tileMatrix, - "tms": tms, - "title": "Mosaic", - "layer_name": "mosaic", - "media_type": tile_format.mediatype, - }, - media_type=MediaType.xml.value, - ) + coverage_array = image.get_coverage_array( + shape, + shape_crs=coord_crs or WGS84_CRS, + ) - ############################################################################ - # /point (Optional) - ############################################################################ - def point(self): - """Register /point endpoint.""" + if post_process: + logger.info(f"feature {i}: post processing image") + image = post_process(image) + + logger.info(f"feature {i}: calculating statistics") + stats = image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), + coverage=coverage_array, + ) + + feature.properties = feature.properties or {} + feature.properties.update({"statistics": stats}) + feature.properties.update({"used_assets": assets}) + + return fc.features[0] if isinstance(geojson, Feature) else fc + + def part(self): # noqa: C901 + """Register /bbox and /feature endpoint.""" + # GET endpoints @self.router.get( - r"/point/{lon},{lat}", - response_model=Point, - response_class=JSONResponse, - responses={200: {"description": "Return a value for a point"}}, + "/bbox/{minx},{miny},{maxx},{maxy}.{format}", + operation_id=f"{self.operation_prefix}getDataForBoundingBoxWithFormat", + **img_endpoint_params, ) - def point( - response: Response, - lon: float = Path(..., description="Longitude"), - lat: float = Path(..., description="Latitude"), + @self.router.get( + "/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}", + operation_id=f"{self.operation_prefix}getDataForBoundingBoxWithSizesAndFormat", + **img_endpoint_params, + ) + def bbox_image( + minx: Annotated[float, Path(description="Bounding box min X")], + miny: Annotated[float, Path(description="Bounding box min Y")], + maxx: Annotated[float, Path(description="Bounding box max X")], + maxy: Annotated[float, Path(description="Bounding box max Y")], + format: Annotated[ + ImageType, + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." + ), + ], src_path=Depends(self.path_dependency), + backend_params=Depends(self.backend_dependency), + reader_params=Depends(self.reader_dependency), + assets_accessor_params=Depends(self.assets_accessor_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), + pixel_selection=Depends(self.pixel_selection_dependency), + image_params=Depends(self.img_part_dependency), + post_process=Depends(self.process_dependency), + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), + env=Depends(self.environment_dependency), + ): + """Create image from a bbox.""" + with rasterio.Env(**env): + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( + src_path, + reader=self.dataset_reader, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), + ) as src_dst: + image, assets = src_dst.part( + [minx, miny, maxx, maxy], + dst_crs=dst_crs, + bounds_crs=coord_crs or WGS84_CRS, + search_options=assets_accessor_params.as_dict(), + pixel_selection=pixel_selection, + threads=MOSAIC_THREADS, + **layer_params.as_dict(), + **dataset_params.as_dict(), + **image_params.as_dict(), + ) + dst_colormap = getattr(src_dst, "colormap", None) + + if post_process: + image = post_process(image) + + content, media_type = self.render_func( + image, + output_format=format, + colormap=colormap or dst_colormap, + **render_params.as_dict(), + ) + + headers: dict[str, str] = {} + if OptionalHeader.x_assets in self.optional_headers: + headers["X-Assets"] = ",".join(assets) + + if image.bounds is not None: + headers["Content-Bbox"] = ",".join(map(str, image.bounds)) + if uri := CRS_to_uri(image.crs): + headers["Content-Crs"] = f"<{uri}>" + + if ( + OptionalHeader.server_timing in self.optional_headers + and image.metadata.get("timings") + ): + headers["Server-Timing"] = ", ".join( + [f"{name};dur={time}" for (name, time) in image.metadata["timings"]] + ) + + return Response(content, media_type=media_type, headers=headers) + + @self.router.post( + "/feature", + operation_id=f"{self.operation_prefix}postDataForGeoJSON", + **img_endpoint_params, + ) + @self.router.post( + "/feature.{format}", + operation_id=f"{self.operation_prefix}postDataForGeoJSONWithFormat", + **img_endpoint_params, + ) + @self.router.post( + "/feature/{width}x{height}.{format}", + operation_id=f"{self.operation_prefix}postDataForGeoJSONWithSizesAndFormat", + **img_endpoint_params, + ) + def feature_image( + geojson: Annotated[Feature, Body(description="GeoJSON Feature.")], + src_path=Depends(self.path_dependency), + format: Annotated[ + ImageType | None, + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." + ), + ] = None, backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), + assets_accessor_params=Depends(self.assets_accessor_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + image_params=Depends(self.img_part_dependency), + pixel_selection=Depends(self.pixel_selection_dependency), + post_process=Depends(self.process_dependency), + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), env=Depends(self.environment_dependency), ): - """Get Point value for a Mosaic.""" - threads = int(os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS)) - + """Create image from a geojson feature.""" with rasterio.Env(**env): - with self.reader( + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( src_path, reader=self.dataset_reader, - reader_options={**reader_params}, - **backend_params, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), ) as src_dst: - values = src_dst.point( - lon, - lat, - threads=threads, - **layer_params, - **dataset_params, + image, assets = src_dst.feature( + geojson.model_dump(exclude_none=True), + shape_crs=coord_crs or WGS84_CRS, + dst_crs=dst_crs, + search_options=assets_accessor_params.as_dict(), + pixel_selection=pixel_selection, + threads=MOSAIC_THREADS, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), ) - return { - "coordinates": [lon, lat], - "values": [ - (src, pts.data.tolist(), pts.band_names) for src, pts in values - ], - } + if post_process: + image = post_process(image) - def validate(self): - """Register /validate endpoint.""" + content, media_type = self.render_func( + image, + output_format=format, + colormap=colormap, + **render_params.as_dict(), + ) - @self.router.post("/validate") - def validate(body: MosaicJSON): - """Validate a MosaicJSON""" - return True + headers: dict[str, str] = {} + if OptionalHeader.x_assets in self.optional_headers: + headers["X-Assets"] = ",".join(assets) + + if image.bounds is not None: + headers["Content-Bbox"] = ",".join(map(str, image.bounds)) + if uri := CRS_to_uri(image.crs): + headers["Content-Crs"] = f"<{uri}>" + + if ( + OptionalHeader.server_timing in self.optional_headers + and image.metadata.get("timings") + ): + headers["Server-Timing"] = ", ".join( + [f"{name};dur={time}" for (name, time) in image.metadata["timings"]] + ) + + return Response(content, media_type=media_type, headers=headers) def assets(self): """Register /assets endpoint.""" @self.router.get( - r"/{minx},{miny},{maxx},{maxy}/assets", + "/bbox/{minx},{miny},{maxx},{maxy}/assets", responses={200: {"description": "Return list of COGs in bounding box"}}, + operation_id=f"{self.operation_prefix}getAssetsForBoundingBox", ) def assets_for_bbox( + minx: Annotated[float, Path(description="Bounding box min X")], + miny: Annotated[float, Path(description="Bounding box min Y")], + maxx: Annotated[float, Path(description="Bounding box max X")], + maxy: Annotated[float, Path(description="Bounding box max Y")], src_path=Depends(self.path_dependency), - minx: float = Query(None, description="Left side of bounding box"), - miny: float = Query(None, description="Bottom of bounding box"), - maxx: float = Query(None, description="Right side of bounding box"), - maxy: float = Query(None, description="Top of bounding box"), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), + assets_accessor_params=Depends(self.assets_accessor_dependency), + coord_crs=Depends(CoordCRSParams), env=Depends(self.environment_dependency), ): """Return a list of assets which overlap a bounding box""" with rasterio.Env(**env): - with self.reader( + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( src_path, reader=self.dataset_reader, - reader_options={**reader_params}, - **backend_params, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), ) as src_dst: - return src_dst.assets_for_bbox(minx, miny, maxx, maxy) + return src_dst.assets_for_bbox( + minx, + miny, + maxx, + maxy, + coord_crs=coord_crs or WGS84_CRS, + **assets_accessor_params.as_dict(), + ) @self.router.get( - r"/{lng},{lat}/assets", + "/point/{lon},{lat}/assets", responses={200: {"description": "Return list of COGs"}}, + operation_id=f"{self.operation_prefix}getAssetsForPoint", ) def assets_for_lon_lat( + lon: Annotated[float, Path(description="Longitude")], + lat: Annotated[float, Path(description="Latitude")], src_path=Depends(self.path_dependency), - lng: float = Query(None, description="Longitude"), - lat: float = Query(None, description="Latitude"), + coord_crs=Depends(CoordCRSParams), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), + assets_accessor_params=Depends(self.assets_accessor_dependency), env=Depends(self.environment_dependency), ): """Return a list of assets which overlap a point""" with rasterio.Env(**env): - with self.reader( + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( src_path, reader=self.dataset_reader, - reader_options={**reader_params}, - **backend_params, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), ) as src_dst: - return src_dst.assets_for_point(lng, lat) + return src_dst.assets_for_point( + lon, + lat, + coord_crs=coord_crs or WGS84_CRS, + **assets_accessor_params.as_dict(), + ) @self.router.get( - r"/{z}/{x}/{y}/assets", + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}/assets", responses={200: {"description": "Return list of COGs"}}, + operation_id=f"{self.operation_prefix}getAssetsForTile", ) def assets_for_tile( - z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), - x: int = Path(..., description="Mercator tiles's column"), - y: int = Path(..., description="Mercator tiles's row"), + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + z: Annotated[ + int, + Path( + description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.", + ), + ], + x: Annotated[ + int, + Path( + description="Column (X) index of the tile on the selected TileMatrix. It cannot exceed the MatrixHeight-1 for the selected TileMatrix.", + ), + ], + y: Annotated[ + int, + Path( + description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.", + ), + ], src_path=Depends(self.path_dependency), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), + assets_accessor_params=Depends(self.assets_accessor_dependency), env=Depends(self.environment_dependency), ): """Return a list of assets which overlap a given tile""" + tms = self.supported_tms.get(tileMatrixSetId) + with rasterio.Env(**env): + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( + src_path, + tms=tms, + reader=self.dataset_reader, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), + ) as src_dst: + return src_dst.assets_for_tile( + x, + y, + z, + **assets_accessor_params.as_dict(), + ) + + ############################################################################ + # OGC Maps (Optional) + ############################################################################ + def ogc_maps(self): # noqa: C901 + """Register OGC Maps /map` endpoint.""" + + self.conforms_to.update( + { + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/core", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/crs", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/scaling", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/scaling/width-definition", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/scaling/height-definition", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/spatial-subsetting", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/spatial-subsetting/bbox-definition", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/spatial-subsetting/bbox-crs", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/spatial-subsetting/crs-curie", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/png", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/jpeg", + "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/tiff", + } + ) + + @self.router.get( + "/map", + operation_id=f"{self.operation_prefix}getMap", + **img_endpoint_params, + ) + def get_map( + src_path=Depends(self.path_dependency), + ogc_params=Depends(OGCMapsParams), + backend_params=Depends(self.backend_dependency), + reader_params=Depends(self.reader_dependency), + assets_accessor_params=Depends(self.assets_accessor_dependency), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + pixel_selection=Depends(self.pixel_selection_dependency), + post_process=Depends(self.process_dependency), + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), + env=Depends(self.environment_dependency), + ) -> Response: + """OGC Maps API.""" with rasterio.Env(**env): - with self.reader( + logger.info( + f"opening data with backend: {self.backend} and reader {self.dataset_reader}" + ) + with self.backend( src_path, reader=self.dataset_reader, - reader_options={**reader_params}, - **backend_params, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), ) as src_dst: - return src_dst.assets_for_tile(x, y, z) + if ogc_params.bbox is not None: + image, assets = src_dst.part( + ogc_params.bbox, + dst_crs=ogc_params.crs or src_dst.crs, + bounds_crs=ogc_params.bbox_crs or WGS84_CRS, + search_options=assets_accessor_params.as_dict(), + pixel_selection=pixel_selection, + threads=MOSAIC_THREADS, + width=ogc_params.width, + height=ogc_params.height, + max_size=ogc_params.max_size, + **layer_params.as_dict(), + **dataset_params.as_dict(), + ) + + else: + # NOTE: Defaults backends do not support preview + image, assets = src_dst.preview( + search_options=assets_accessor_params.as_dict(), + pixel_selection=pixel_selection, + threads=MOSAIC_THREADS, + width=ogc_params.width, + height=ogc_params.height, + max_size=ogc_params.max_size, + dst_crs=ogc_params.crs or src_dst.crs, + **layer_params.as_dict(), + **dataset_params.as_dict(), + ) + dst_colormap = getattr(src_dst, "colormap", None) + + if post_process: + image = post_process(image) + + content, media_type = self.render_func( + image, + output_format=ogc_params.format, + colormap=colormap or dst_colormap, + **render_params.as_dict(), + ) + + headers: dict[str, str] = {} + if OptionalHeader.x_assets in self.optional_headers: + headers["X-Assets"] = ",".join(assets) + + if image.bounds is not None: + headers["Content-Bbox"] = ",".join(map(str, image.bounds)) + if uri := CRS_to_uri(image.crs): + headers["Content-Crs"] = f"<{uri}>" + + if ( + OptionalHeader.server_timing in self.optional_headers + and image.metadata.get("timings") + ): + headers["Server-Timing"] = ", ".join( + [f"{name};dur={time}" for (name, time) in image.metadata["timings"]] + ) + + return Response(content, media_type=media_type, headers=headers) diff --git a/src/titiler/mosaic/titiler/mosaic/models/responses.py b/src/titiler/mosaic/titiler/mosaic/models/responses.py index 1ad45c778..82749e9d1 100644 --- a/src/titiler/mosaic/titiler/mosaic/models/responses.py +++ b/src/titiler/mosaic/titiler/mosaic/models/responses.py @@ -1,10 +1,17 @@ """TiTiler.mosaic response models.""" -from typing import List, Tuple - from pydantic import BaseModel +class AssetPoint(BaseModel): + """Model for Point value per asset""" + + name: str + values: list[float | None] + band_names: list[str] + band_descriptions: list[str] | None = None + + class Point(BaseModel): """ Point model. @@ -13,5 +20,5 @@ class Point(BaseModel): """ - coordinates: List[float] - values: List[Tuple[str, List[float], List[str]]] + coordinates: list[float] + assets: list[AssetPoint] diff --git a/src/titiler/mosaic/titiler/mosaic/py.typed b/src/titiler/mosaic/titiler/mosaic/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/titiler/mosaic/titiler/mosaic/resources/__init__.py b/src/titiler/mosaic/titiler/mosaic/resources/__init__.py deleted file mode 100644 index 60f13dcb0..000000000 --- a/src/titiler/mosaic/titiler/mosaic/resources/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""titiler.core.resources.""" diff --git a/src/titiler/mosaic/titiler/mosaic/resources/enums.py b/src/titiler/mosaic/titiler/mosaic/resources/enums.py deleted file mode 100644 index 53822891d..000000000 --- a/src/titiler/mosaic/titiler/mosaic/resources/enums.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Titiler.mosaic Enums.""" - -from enum import Enum -from types import DynamicClassAttribute - -from rio_tiler.mosaic.methods import defaults - - -class PixelSelectionMethod(str, Enum): - """rio-tiler.mosaic pixel selection methods""" - - first = "first" - highest = "highest" - lowest = "lowest" - mean = "mean" - median = "median" - stdev = "stdev" - - @DynamicClassAttribute - def method(self): - """Return rio-tiler-mosaic pixel selection class""" - return getattr(defaults, f"{self._value_.title()}Method") diff --git a/src/titiler/xarray/LICENSE b/src/titiler/xarray/LICENSE new file mode 100644 index 000000000..eb4365951 --- /dev/null +++ b/src/titiler/xarray/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Development Seed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/titiler/xarray/README.md b/src/titiler/xarray/README.md new file mode 100644 index 000000000..cab2aa033 --- /dev/null +++ b/src/titiler/xarray/README.md @@ -0,0 +1,108 @@ +## titiler.xarray + +Adds support for Xarray Dataset (NetCDF/Zarr) in Titiler. + +## Installation + +```bash +python -m pip install -U pip + +# From Pypi +python -m pip install "titiler.xarray[full]" + +# Or from sources +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core -e "src/titiler/xarray" +``` + +#### Installation options + +Default installation for `titiler.xarray` DOES NOT include `fsspec` or any storage's specific dependencies (e.g `s3fs`) nor `engine` dependencies (`zarr`, `h5netcdf`). This is to ease the customization and deployment of user's applications. If you want to use the default's dataset reader you will need to at least use the `[minimal]` dependencies (e.g `python -m pip install "titiler.xarray[minimal]"`). + +Here is the list of available options: + +- **fs**: `h5netcdf`, `fsspec`, `s3fs`, `aiohttp`, `gcsfs` + +#### Dependencies + +Titiler.xarray follows [SPEC 0](https://scientific-python.org/specs/spec-0000/), similar to [xarray](https://docs.xarray.dev/en/v2025.09.0/getting-started-guide/installing.html#minimum-dependency-versions). + +## How To + +```python +from fastapi import FastAPI + +from titiler.xarray.extensions import VariablesExtension +from titiler.xarray.factory import TilerFactory + +app = FastAPI( + openapi_url="/api", + docs_url="/api.html", + description="""Xarray based tiles server for MultiDimensional dataset (Zarr/NetCDF). + +--- + +**Documentation**: https://developmentseed.org/titiler/ + +**Source Code**: https://github.com/developmentseed/titiler + +--- + """, +) + +md = TilerFactory( + router_prefix="/md", + extensions=[ + VariablesExtension(), + ], +) +app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"]) +``` + +## Package structure + +``` +titiler/ + └── xarray/ + ├── tests/ - Tests suite + └── titiler/xarray/ - `xarray` namespace package + ├── dependencies.py - titiler-xarray dependencies + ├── extensions.py - titiler-xarray extensions + ├── main.py - main fastapi application + ├── io.py - titiler-xarray Readers + └── factory.py - endpoints factory +``` + +## Custom Dataset Opener + +A default Dataset IO is provided within `titiler.xarray.io.Reader` class with only support for Zarr dataset (via xarray+zarr-python). + +For other dataset (e.g NetCDF), you can use `titiler.xarray.io.FsReader` which use the optional dependencies (`fsspec`, `netcdf5`). + +``` +python -m pip install "titiler.xarray[fs]" +``` + +Example of application with `fsspec` reader: + +```python +from fastapi import FastAPI +from titiler.xarray.extensions import VariablesExtension +from titiler.xarray.factory import TilerFactory +from titiler.xarray.io import FsReader + +# Create FastAPI application +app = FastAPI(openapi_url="/api", docs_url="/api.html") + +# Create custom endpoints with the FsReader +md = TilerFactory( + reader=FsReader, + router_prefix="/md", + extensions=[ + # we also want to use the simple opener for the Extension + VariablesExtension(dataset_opener=xarray.open_dataset), + ], +) + +app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"]) +``` diff --git a/src/titiler/xarray/examples/local.py b/src/titiler/xarray/examples/local.py new file mode 100644 index 000000000..a377ee0e7 --- /dev/null +++ b/src/titiler/xarray/examples/local.py @@ -0,0 +1,112 @@ +# /// script +# dependencies = [ +# "titiler.xarray[full]", +# "starlette_cramjam", +# "uvicorn", +# ] +# /// +"""Example of Application.""" + +from datetime import datetime + +import numpy +import xarray +from fastapi import FastAPI +from rio_tiler.io.xarray import XarrayReader +from starlette.middleware.cors import CORSMiddleware +from starlette_cramjam.middleware import CompressionMiddleware + +from titiler.core.dependencies import DefaultDependency +from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers +from titiler.core.factory import AlgorithmFactory, ColorMapFactory, TMSFactory +from titiler.core.middleware import CacheControlMiddleware +from titiler.xarray.factory import TilerFactory + + +def XarrayDataArray() -> xarray.DataArray: + """Custom Dependency which return a DataArray.""" + arr = numpy.linspace(1, 1000, 1000 * 2000).reshape(1, 1000, 2000) + data = xarray.DataArray( + arr, + dims=("time", "y", "x"), + coords={ + "x": numpy.arange(-170, 170, 0.17), + "y": numpy.arange(-80, 80, 0.16), + "time": [datetime(2022, 1, 1)], + }, + ) + data.attrs.update({"valid_min": arr.min(), "valid_max": arr.max(), "fill_value": 0}) + data.rio.write_crs("epsg:4326", inplace=True) + return data + + +app = FastAPI( + title="TiTiler with support of Multidimensional dataset", + openapi_url="/api", + docs_url="/api.html", + version="0.1.0", +) + + +md = TilerFactory( + router_prefix="/md", + # Use rio-tiler XarrayReader which accept xarray.DataArray as input + reader=XarrayReader, + # Use our custom dependency which return a xarray.DataArray + path_dependency=XarrayDataArray, + # Set the reader_dependency to `empty` + reader_dependency=DefaultDependency, +) +app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"]) + +# TileMatrixSets endpoints +app.include_router(TMSFactory().router, tags=["Tiling Schemes"]) + +############################################################################### +# Algorithms endpoints +app.include_router( + AlgorithmFactory().router, + tags=["Algorithms"], +) + +# Colormaps endpoints +app.include_router( + ColorMapFactory().router, + tags=["ColorMaps"], +) + +add_exception_handlers(app, DEFAULT_STATUS_CODES) + +# Set all CORS enabled origins +app.add_middleware( + CORSMiddleware, + allow_origins="*", + allow_credentials=True, + allow_methods=["GET"], + allow_headers=["*"], +) + +app.add_middleware( + CompressionMiddleware, + minimum_size=0, + exclude_mediatype={ + "image/jpeg", + "image/jpg", + "image/png", + "image/jp2", + "image/webp", + }, + compression_level=6, +) + +app.add_middleware( + CacheControlMiddleware, + cachecontrol="public, max-age=3600", + exclude_path={r"/healthz"}, +) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app=app, host="127.0.0.1", port=8080, log_level="info") diff --git a/src/titiler/xarray/notebooks/xarray_dataset_cache.ipynb b/src/titiler/xarray/notebooks/xarray_dataset_cache.ipynb new file mode 100644 index 000000000..a7692741d --- /dev/null +++ b/src/titiler/xarray/notebooks/xarray_dataset_cache.ipynb @@ -0,0 +1,167 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add Caching Layer for Xarray Dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pickle\n", + "from typing import Any, Callable, List, Optional\n", + "\n", + "import attr\n", + "import xarray\n", + "from morecantile import TileMatrixSet\n", + "from rio_tiler.constants import WEB_MERCATOR_TMS\n", + "from rio_tiler.io.xarray import XarrayReader\n", + "\n", + "from titiler.xarray.io import xarray_open_dataset, get_variable\n", + "\n", + "from diskcache import Cache\n", + "\n", + "cache_client = Cache()\n", + "\n", + "\n", + "@attr.s\n", + "class CustomReader(XarrayReader):\n", + " \"\"\"Reader: Open Zarr file and access DataArray.\"\"\"\n", + "\n", + " src_path: str = attr.ib()\n", + " variable: str = attr.ib()\n", + "\n", + " # xarray.Dataset options\n", + " opener: Callable[..., xarray.Dataset] = attr.ib(default=xarray_open_dataset)\n", + "\n", + " group: Optional[Any] = attr.ib(default=None)\n", + " decode_times: bool = attr.ib(default=False)\n", + "\n", + " # xarray.DataArray options\n", + " sel: Optional[List[str]] = attr.ib(default=None)\n", + " method: Optional[str] = attr.ib(default=None)\n", + "\n", + " tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)\n", + "\n", + " ds: xarray.Dataset = attr.ib(init=False)\n", + " input: xarray.DataArray = attr.ib(init=False)\n", + "\n", + " _dims: List = attr.ib(init=False, factory=list)\n", + "\n", + " def __attrs_post_init__(self):\n", + " \"\"\"Set bounds and CRS.\"\"\"\n", + " ds = None\n", + " # Generate cache key and attempt to fetch the dataset from cache\n", + " cache_key = f\"{self.src_path}_group:{self.group}_time:{self.decode_times}\"\n", + " data_bytes = cache_client.get(cache_key)\n", + " if data_bytes:\n", + " print(f\"Found dataset in Cache {cache_key}\")\n", + " ds = pickle.loads(data_bytes)\n", + "\n", + " self.ds = ds or self.opener(\n", + " self.src_path,\n", + " group=self.group,\n", + " decode_times=self.decode_times,\n", + " )\n", + " if not ds:\n", + " # Serialize the dataset to bytes using pickle\n", + " cache_key = f\"{self.src_path}_group:{self.group}_time:{self.decode_times}\"\n", + " data_bytes = pickle.dumps(self.ds)\n", + " print(f\"Adding dataset in Cache: {cache_key}\")\n", + " cache_client.set(cache_key, data_bytes, tag=\"data\", expire=300)\n", + "\n", + " self.input = get_variable(\n", + " self.ds,\n", + " self.variable,\n", + " sel=self.sel,\n", + " method=self.method,\n", + " )\n", + " super().__attrs_post_init__()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found dataset in Cache ../tests/fixtures/dataset_2d.nc_group:None_time:False\n", + " Size: 16MB\n", + "Dimensions: (x: 2000, y: 1000)\n", + "Coordinates:\n", + " * x (x) float64 16kB -170.0 -169.8 -169.7 -169.5 ... 169.5 169.7 169.8\n", + " * y (y) float64 8kB -80.0 -79.84 -79.68 -79.52 ... 79.52 79.68 79.84\n", + "Data variables:\n", + " dataset (y, x) float64 16MB ...\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAa4AAAGiCAYAAAC/NyLhAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAJ55JREFUeJzt3X1wVFWC9/Hf7UCa1ySGkHQyAhMdR2R40UWMeZxhnSFDEtFSydYjyjroUFCyibWY8WXj48jEdSdTzNRq6TryzxS4+8isY9WotczKFgMCpQYURh4G0ZSh2EGHdEDYpHmRkHSf5w+G1oYASfr26b63v5+qptLdt7tPH7rvr8/LPdcxxhgBAOARgXQXAACAwSC4AACeQnABADyF4AIAeArBBQDwFIILAOApBBcAwFMILgCApxBcAABPIbgAAJ6StuB64YUX9PWvf10jRoxQRUWF3nvvvXQVBQDgIWkJrldeeUWNjY1asWKF/vCHP2jGjBmqrq7WoUOH0lEcAICHOOlYZLeiokKzZs3Sv/zLv0iSYrGYJkyYoAcffFD/8A//YLs4AAAPGWb7BU+fPq2dO3eqqakpflsgEFBVVZVaW1v7fUxPT496enri12OxmI4ePapx48bJcZyUlxkA4C5jjI4dO6aysjIFAoPr/LMeXJ9//rmi0ahKSkoSbi8pKdHHH3/c72NaWlrU3Nxso3gAAIs+/fRTXX755YN6jPXgGoqmpiY1NjbGr3d3d2vixIn69NNPlZeXl8aSAQCGIhKJaMKECRo7duygH2s9uIqKipSTk6POzs6E2zs7OxUKhfp9TDAYVDAYPO/2vLw8ggsAPGwowz3WZxXm5uZq5syZ2rhxY/y2WCymjRs3qrKy0nZxAAAek5auwsbGRi1atEjXX3+9brjhBj377LM6ceKE7r///nQUBwDgIWkJrrvuukuHDx/Wk08+qXA4rGuvvVbr168/b8IGAADnSstxXMmKRCLKz89Xd3c3Y1wA4EHJ7MdZqxAA4CkEFwDAUwguAICnEFwAAE8huAAAnkJwAQA8heACAHgKwQUA8BSCCwDgKQQXAMBTCC4AgKcQXAAATyG4AACeQnABADyF4AIAeArBBQDwFIILAOApBBcAwFMILgCApxBcAABPIbgAAJ5CcAEAPIXgAgB4CsEFAPAUggsA4CkEFwDAUwguAICnEFwAAE8huAAAnkJwAQA8heACAHgKwQUA8BSCCwDgKcPSXYBU6+zsUCwaTXcxACArBHJyVFJSmtLX8HVwRaNRnTx4j8qKwukuCgBkhT+HSxUt2qCcnJyUvYavg0uScgJ9Gj68L93FAICskBNIfQ+X74NLMjJpfHUnja8NAH7k++CKGaOYiaXltR05chyiCwDc5PvgSicjo1g6m3sAYFnMpH6n5/vgSndupLejEgD8JyuCK5bl4RFgpA2Aj/g+uEBwA7AnptTPKWDlDACAp2RBcNHaAAA/8X1XoZGx0nQFANgZmsiCFhcAwE+yoMVFZyEA+EkWBJexckAcAMBOQ4GuQgCAp2RBi+vLfwEAqWVjtSDfB1dMhgNwAcASG0MzdBUCADzF9y0ulrkFAH/xfXCxyC4A2MMByAAAnCMLWlx0FQKAn2RBcEmGA5ABwAqmw7sgZoyitLkA/IUjTq7qdb4PLgD4qjMTtpAqNpoJBBeArMPId+oQXC6IiV9XADIPU7qHzvW6+8lPfiLHcRIukydPjt9/6tQp1dfXa9y4cRozZozq6urU2dnpdjEAIKPF9OVpl/x18ejkjG9961v6/e9//+WLDPvyZR566CH97ne/06uvvqr8/Hw1NDRo/vz5euedd1JRFGsVCQCD5cc9k40erpQE17BhwxQKhc67vbu7W7/61a+0du1afe9735MkrV69Wtdcc422bdumG2+80fWyGEkxP346ACADeXaM65NPPlFZWZlGjBihyspKtbS0aOLEidq5c6d6e3tVVVUV33by5MmaOHGiWltbUxdcrj8rAKA/ngyuiooKrVmzRldffbU6OjrU3Nys73znO9qzZ4/C4bByc3NVUFCQ8JiSkhKFw+ELPmdPT496enri1yORyIDLc7bfFQCQep4Mrtra2vjf06dPV0VFhSZNmqTf/OY3Gjly5JCes6WlRc3NzW4VEQDgYSmfDl9QUKBvfvObam9v1/e//32dPn1aXV1dCa2uzs7OfsfEzmpqalJjY2P8eiQS0YQJEwb0+jFDVyEA2GJjTkHKg+v48ePat2+f7r33Xs2cOVPDhw/Xxo0bVVdXJ0lqa2vTgQMHVFlZecHnCAaDCgaDQ3p9ugoBwB5PdhU+/PDDuu222zRp0iQdPHhQK1asUE5Oju6++27l5+dr8eLFamxsVGFhofLy8vTggw+qsrIyJRMzJGYVAoBNngyuzz77THfffbeOHDmi8ePH69vf/ra2bdum8ePHS5KeeeYZBQIB1dXVqaenR9XV1frlL3/pdjEAAD7lGA+e8yMSiSg/P1/d3d3Ky8u74HbRaFTv/78KFY3/s8XSAUD2+vzQBM26tlU5OTkX3W6g+/H++H6tQo7jAgB7PLtyRiY5M6uQc+8AgA029re+Dy5aXPAi5y8XAOfLiuAyhl0AvMdxPDf8DFiZVuj74AK8yEiKGodWFzyHMS4XGDkcgAzP4rMLrzGMcSXvzBgXv1sBwAZPHoCcaWJyCC4AsIRZhS6IGUcxJmcAgBU2JsP5PrgY4wLgdV766U1XoQsY4wLgZQEZfnyfw/fBFZOjKMEFwKMy4Ye3M4i5gswqdIExg6nygaMDEoANmbGnGfg+lK5CFxi5PznDkeSk/0cQAFgxmBPycgCyC2JKTVM7mgEzFQNWGuUAMHA2ZnH7Prj8jBmTADIPweUKv7ZLCC0AmYbJGS5g5QwAsIeVM1zAyhkAYA/B5QIzqCMQAADJYMknF9BVCAD2cByXKwKKKZDuQgBAVqCr0AUxY+e4AgAAswpdYegqBABrOADZBYxxAYA9tLhcEJOdWS4AAILLJQFaXABgCZMzXNCngHpNTrqLAQBZIWphFrfvgytmHKbDA4AldBW6hJUzAMAOZhW6IJaCE0kCAPrHGJcLmA4PAPbQVegCYwguALCH4EoaXYUAYE/Mwiq7vg+uqMlRn5gOD0hnfgs7nDsbKcQYlwuMWGQXOMuR5PB1QAoRXC44MzmD47iAOBpcSKGY4QDkpBnDGBcA2GKjoeD/4JLDAcgAYIlhckbyYnLkEFwAYIWNnmjfB1fUBBRjkV0AsMJY2N/6PriMHIkxLgCwgpUzXBAzoqsQAKwhuJIWNTkSXYWe4cjIcZivDXiVw+rwyYv9Za0AeIXDcUaAhwVocbmD47gAwBaCK2kxjuMCAGuYnOECpsMDgD05LPmUPMNpTQDAGiZnuMBwBmQAsMZG/5bvg6vPBNRnoekKAJAcugqTFzOOoj4NLk4ICCDTMDnDBTHjWDk/jH1GAXpAAWQYwxhX8owcRX05xuUoRoMLQIZhVqELopxIEgCsiXI+ruQZH49xAUCmMZwBOXmcARkA7GFyhguiYjo8ANgSZXJG8mKMcQGANTYWfMiK4GKMC0C2CKT7+E5aXMmLmoD6YukNLsfhYGEAdqR7iTsbXYWD3qNv3bpVt912m8rKyuQ4jl5//fWE+40xevLJJ1VaWqqRI0eqqqpKn3zyScI2R48e1cKFC5WXl6eCggItXrxYx48fT+qNXEhMX07QSNvlL92VXLhw4eL7SyZ2FZ44cUIzZszQD3/4Q82fP/+8+1euXKnnnntOL730ksrLy/XjH/9Y1dXV2rt3r0aMGCFJWrhwoTo6OrRhwwb19vbq/vvv19KlS7V27drk39E5zoZG+mVCGQAgtWzsbwcdXLW1taqtre33PmOMnn32WT3xxBO6/fbbJUn/+q//qpKSEr3++utasGCBPvroI61fv17vv/++rr/+eknS888/r1tuuUW/+MUvVFZWlsTbOV/MBBjjAgBLbOxvXR3j2r9/v8LhsKqqquK35efnq6KiQq2trVqwYIFaW1tVUFAQDy1JqqqqUiAQ0Pbt23XnnXe6WSTFjGOlzxUAkKEtrosJh8OSpJKSkoTbS0pK4veFw2EVFxcnFmLYMBUWFsa3OVdPT496enri1yORyIDLFNOZVhcAIPVsTEPzxKzClpYWNTc3D+mxMc7HBeArHBk5dMKkjOdWhw+FQpKkzs5OlZaWxm/v7OzUtddeG9/m0KFDCY/r6+vT0aNH448/V1NTkxobG+PXI5GIJkyYMKAynZnVN5h3AcDPjBymSqVQRs4qvJjy8nKFQiFt3LgxHlSRSETbt2/XsmXLJEmVlZXq6urSzp07NXPmTEnSpk2bFIvFVFFR0e/zBoNBBYPBIZUpZtJ/XMNgeKekgHfxYzZ1MnKM6/jx42pvb49f379/v3bt2qXCwkJNnDhRy5cv19NPP62rrroqPh2+rKxMd9xxhyTpmmuuUU1NjZYsWaJVq1apt7dXDQ0NWrBggeszCiUpanLUF81x/XlTwXH4NgHwNhtzCgYdXDt27NB3v/vd+PWzXXiLFi3SmjVr9Oijj+rEiRNaunSpurq69O1vf1vr16+PH8MlSS+//LIaGho0Z84cBQIB1dXV6bnnnnPh7ZzPeKjFRW4B8DobLS7HGO81miORiPLz89Xd3a28vLwLbheNRvW/1/0f/c/Y0xZLBwDZq/BYUK/c+rRyci7e0zXQ/Xh/PDGrMBlRY+eAOACAnfFD3weXMYEMWfIJAPwvyhmQkxczdvpcAQC0uFxhFFAsRnABgA0mE2cVek3UcCJJALDFRkPB98GVOac1AQD/89zKGZnIMMYFANYwxuWCmHEY4wIASzJyySevocUFAPbQ4nJBzDhWltkHAHjwtCYZickZAGANXYUuYIwLAOyhxeUCQ1chAFhDcLmAFpc4TTkAe5ickTwTo8XlvRPXAPAqxrhcYIydpisAgOnwrjFZ3lUIALYwxuUG48iDJ3kGAE8iuFxAVyEAWERXoQuMY6UiAQC0uFxhYo5MNN2l8BiHpAcwRLS4XGCYDj5YDl2rsnBKIcCfaHElz9BVOGhUl0QtAENEi8sFRlIs3YWA99DkAobEwuFHWRBctLgAwBpaXC4wIrgAwBbGuFwQk5WmKwDADv8Hl3EY40oVfg8AOBddhclzDNO7U8F85V8AiLPQUPB9cEli/5oCjmSlLxuAx1g4cNb/wUVXIQDYw3R4FzCrEAB8xffBxRgXAFhEV6ELoulfM5bcBJAtbOxvfR9cTgZ0FTpGTB0HkB0ILpdkQpOHcTYAWYAWlxtM+rsKAQDuyYrgAgDY4Vjo4MqK4MqAjkIAgEt8H1x0EwKAPTYaCv4PrphDeAGALYHUv4Tvg4vJGQBgD7MK3ZIJ0+EBIBsQXMlzYmcuAIDUY1ahGzJg5QwAyBq0uJLnMMYFAPYQXMmjqxAAkjOoLGJWoUsILgAYssGMW9loKPg/uOgqBIDkDGYfmpOyUsT5Prgy4bQmAJAtOI7LDYxxAYA9dBUmz5FocQGAj/g+uBRjjAsAbGFyhhuMmFUIAJYwxuUSWlwA4B++Dy4OQP4Saw0DSDlaXMlz6CqMs7H4JYDsRlehGzgA+UvUg53TswLZjOBKHgcgIwGfBcIbKUWLyw0xyYmxtwJwDgI8NZgOnzxHdBUCQH9Ssms0qd/h+j64OJEkgH6xX/DshK1Bnzll69atuu2221RWVibHcfT6668n3H/ffffJcZyES01NTcI2R48e1cKFC5WXl6eCggItXrxYx48fT+qNXIhjTHxKPBcuXLhw+colmoJLLCW78gSDbnGdOHFCM2bM0A9/+EPNnz+/321qamq0evXq+PVgMJhw/8KFC9XR0aENGzaot7dX999/v5YuXaq1a9cOtjiXFhNdhQBgSUZOzqitrVVtbe1FtwkGgwqFQv3e99FHH2n9+vV6//33df3110uSnn/+ed1yyy36xS9+obKyssEW6aKYVQgAFmVicA3E5s2bVVxcrMsuu0zf+9739PTTT2vcuHGSpNbWVhUUFMRDS5KqqqoUCAS0fft23Xnnnec9X09Pj3p6euLXI5HIwAtj7DRdAbex0gm8KCNbXJdSU1Oj+fPnq7y8XPv27dPjjz+u2tpatba2KicnR+FwWMXFxYmFGDZMhYWFCofD/T5nS0uLmpubh1YgDkCGV/G5/RIh7h1eDK4FCxbE/542bZqmT5+uK6+8Ups3b9acOXOG9JxNTU1qbGyMX49EIpowYcLAn4AdADyIfTW8yMbnNuXT4a+44goVFRWpvb1dc+bMUSgU0qFDhxK26evr09GjRy84LhYMBs+b4DFQjHEBPsB32Du82OI612effaYjR46otLRUklRZWamuri7t3LlTM2fOlCRt2rRJsVhMFRUVrr/+mWmfKahJfg4DwHkycjr88ePH1d7eHr++f/9+7dq1S4WFhSosLFRzc7Pq6uoUCoW0b98+Pfroo/rGN76h6upqSdI111yjmpoaLVmyRKtWrVJvb68aGhq0YMEC12cUSkrdGBe/AAHgfJm4csaOHTv03e9+N3797NjTokWL9OKLL2r37t166aWX1NXVpbKyMs2dO1f/+I//mNDV9/LLL6uhoUFz5sxRIBBQXV2dnnvuORfezgUQMgBgRUbOKrz55ptlLpKo//Vf/3XJ5ygsLEzNwcb9OHuEOAAg9TIyuLzGYTo8ANhDcLnEQp8rAEAElxvoKgSyCLN9s4Lvg0syrJ0DAJbYOHGv/4OLMS4ge/BdTz+6CpPnxCSHMS4AsIJZhS5wjCTGuADADoLLDUYOY1wAYIUvFtlNO2NnsBCZzzj8gAFSjhaXC1gdHn/BWCeQek4s9T8QfR9cnNYEACyixeUCY5gODwCWMKvQBbS4AMBffB9cHIAMABbR4nKBMRzHBQCW0FXoAsd4cJFdZm0D8CqCywUenJxhSC4AXmXhsBPfB5fXQkvieCMAHmZhpSLfBxcrZ2CoWGkDGAK6CpPnGMN0eAwJLV9g8Fir0A0cxwUA9tDicgm/nAHADsa4kucYwxgXgKFjrHNQbHSx+z646CoEkBx2IINCi8sFJnWDhXycgSzAF31waHElz4kZKZqainQcscoFAHyFjTPO+z64zkjRLwC6IQHAOv8HF6vDA4A9dBW6wBimwwOAJawO7wa68wDAHoLLDbS4AMAaugpdwAHIyAYOp8NB9siC4BItLvifYYEHZAgLDQX/BxcDXMgW/EBDBmDJJzfQ4gIAX8mC4OJ8XABgDy2upDm0uADAHqbDu4OVMwDAEsa4XGCMFIultwxM9wKQLWhxuSAjxrjSXgAA8I0sCC6lf4wrE3LLES0/AKlHi8sFLLJ7hon/kx6Ow7nLgCzAcVxuIbcyQCZ02YrwBFKN4HKBYXn4jJAx/wUkF+B12RFcdBXirEz4LDDWCF+jxZU0x9jpcwUGLN1zhchNpBKTM9yQIWMrQFx6P5AO3aVIJca4XJAJ0+GBTJIJ3we6S5EE/wcXZ0AGMk8mfCcJz9Sw8H8bSPkrAACyB12FAJAimdDq8yMmZ7iAMS4AsIfgcgHHcQGARXQVJo+FMwDAV7IguGhxAYA1TM5wA8EFANYQXC5gcgYA+EoWBBctLgCwhhaXCwguALCHlTPcQGgBgJ/4vsV1ZoiL8MLgOaxlB2SkQbW4WlpaNGvWLI0dO1bFxcW644471NbWlrDNqVOnVF9fr3HjxmnMmDGqq6tTZ2dnwjYHDhzQvHnzNGrUKBUXF+uRRx5RX19f8u8GcNPZbmYuXLgM7pJig2pxbdmyRfX19Zo1a5b6+vr0+OOPa+7cudq7d69Gjx4tSXrooYf0u9/9Tq+++qry8/PV0NCg+fPn65133pEkRaNRzZs3T6FQSO+++646Ojr0gx/8QMOHD9dPf/pT99+hpYqE//CpAYbAwv7WMUn0ox0+fFjFxcXasmWLZs+ere7ubo0fP15r167V3/zN30iSPv74Y11zzTVqbW3VjTfeqDfffFO33nqrDh48qJKSEknSqlWr9Nhjj+nw4cPKzc295OtGIhHl5+eru7tbeXl5F9wuGo3qnlkPq/vPPUN9iwCAQSicOEL/tu3nysnJueh2A92P9yepMa7u7m5JUmFhoSRp586d6u3tVVVVVXybyZMna+LEifHgam1t1bRp0+KhJUnV1dVatmyZPvzwQ1133XXnvU5PT496er4Mn0gkMvBC0uICAHss7G6HHFyxWEzLly/XTTfdpKlTp0qSwuGwcnNzVVBQkLBtSUmJwuFwfJuvhtbZ+8/e15+WlhY1NzcPtagEF+BlzJHBOYYcXPX19dqzZ4/efvttN8vTr6amJjU2NsavRyIRTZgwYWAPNkaMVgAeZhzCy1MybHLGWQ0NDVq3bp22bt2qyy+/PH57KBTS6dOn1dXVldDq6uzsVCgUim/z3nvvJTzf2VmHZ7c5VzAYVDAYHEpRAXie4benl2TarEJjjB588EG99tpr2rx5s8rLyxPunzlzpoYPH66NGzeqrq5OktTW1qYDBw6osrJSklRZWal/+qd/0qFDh1RcXCxJ2rBhg/Ly8jRlyhQ33tM5hRZdhQBgiY3d7aCCq76+XmvXrtUbb7yhsWPHxsek8vPzNXLkSOXn52vx4sVqbGxUYWGh8vLy9OCDD6qyslI33nijJGnu3LmaMmWK7r33Xq1cuVLhcFhPPPGE6uvrU9Sq4tcaANiTYS2uF198UZJ08803J9y+evVq3XfffZKkZ555RoFAQHV1derp6VF1dbV++ctfxrfNycnRunXrtGzZMlVWVmr06NFatGiRnnrqqeTeyYUYI5lYap4bAKxwJFZyiUvqOK50GdRxXNctV/dnX1gsHQC4zTuhddmkkfq/O57J3OO4PIFJhQA8z0M7sUwb4/ImDkAGAGsybVahJxmjVPWGsno4ANjn+/NxpTL7jZea7wBgBS0uF6SwEg3hBQC2+T+4jKQY0+EBwAoLv+V931UIAPAXggsA4CLGuJIXYzo8ANhiYgRX8jiRZHbg0AQga/g/uCSCy+8ILSCrZEdwwd9oVQOZg5UzXJDClTMAiRVUANuYVQgkid9FwFewyC7gBYbwAs6iq9AFJnZmSjzgZ46YpIKs4f/gArKBif8DpBktLpfwhUYWSPvHnBYf7PB/cDFVGrDESA7zvZB6/g8uAPbwIxF0FbqE7xJgSRq/bPRUZgQbv138H1wcgAxkDQ4Gzw7+Dy4A2YEzkmcGjuMCgEEgt7ICU4AAAJ7i+xaXMTqzegYAIPUsdBVmQYuLvgMA8JMsCC4AgJ/4vquQlTMAZB2fHxbg/+ACgGxjTPoOyGY6PABgSNLU0WTjWDrGuAAAnuLrFpfjOJrzwP/S8f85ke6iAEBWGFs4RoFAattEvg6uQCCgBx5dku5iAABcRFchAMBTCC4AgKcQXAAATyG4AACeQnABADyF4AIAeArBBQDwFIILAOApBBcAwFMILgCApxBcAABPIbgAAJ5CcAEAPIXgAgB4CsEFAPAUggsA4CkEFwDAUwguAICnEFwAAE8huAAAnkJwAQA8heACAHgKwQUA8BSCCwDgKQQXAMBTCC4AgKcQXAAATxlUcLW0tGjWrFkaO3asiouLdccdd6itrS1hm5tvvlmO4yRcHnjggYRtDhw4oHnz5mnUqFEqLi7WI488or6+vuTfDQDA94YNZuMtW7aovr5es2bNUl9fnx5//HHNnTtXe/fu1ejRo+PbLVmyRE899VT8+qhRo+J/R6NRzZs3T6FQSO+++646Ojr0gx/8QMOHD9dPf/pTF94SAMDPHGOMGeqDDx8+rOLiYm3ZskWzZ8+WdKbFde211+rZZ5/t9zFvvvmmbr31Vh08eFAlJSWSpFWrVumxxx7T4cOHlZube8nXjUQiys/PV3d3t/Ly8oZafABAmiSzH09qjKu7u1uSVFhYmHD7yy+/rKKiIk2dOlVNTU06efJk/L7W1lZNmzYtHlqSVF1drUgkog8//LDf1+np6VEkEkm4AACy06C6Cr8qFotp+fLluummmzR16tT47ffcc48mTZqksrIy7d69W4899pja2tr029/+VpIUDocTQktS/Ho4HO73tVpaWtTc3DzUogIAfGTIwVVfX689e/bo7bffTrh96dKl8b+nTZum0tJSzZkzR/v27dOVV145pNdqampSY2Nj/HokEtGECROGVnAAgKcNqauwoaFB69at01tvvaXLL7/8ottWVFRIktrb2yVJoVBInZ2dCducvR4Khfp9jmAwqLy8vIQLACA7DSq4jDFqaGjQa6+9pk2bNqm8vPySj9m1a5ckqbS0VJJUWVmpP/7xjzp06FB8mw0bNigvL09TpkwZTHEAAFloUF2F9fX1Wrt2rd544w2NHTs2PiaVn5+vkSNHat++fVq7dq1uueUWjRs3Trt379ZDDz2k2bNna/r06ZKkuXPnasqUKbr33nu1cuVKhcNhPfHEE6qvr1cwGHT/HQIAfGVQ0+Edx+n39tWrV+u+++7Tp59+qr/927/Vnj17dOLECU2YMEF33nmnnnjiiYTuvT/96U9atmyZNm/erNGjR2vRokX62c9+pmHDBpajTIcHAG9LZj+e1HFc6UJwAYC3JbMfH/KswnQ6m7UczwUA3nR2/z2UtpMng+vYsWOSxJR4APC4Y8eOKT8/f1CP8WRXYSwWU1tbm6ZMmaJPP/2U7sJ+nD3WjfrpH/VzcdTPpVFHF3ep+jHG6NixYyorK1MgMLgjszzZ4goEAvra174mSRzXdQnUz8VRPxdH/VwadXRxF6ufwba0zuJ8XAAATyG4AACe4tngCgaDWrFiBQctXwD1c3HUz8VRP5dGHV1cKuvHk5MzAADZy7MtLgBAdiK4AACeQnABADyF4AIAeIong+uFF17Q17/+dY0YMUIVFRV677330l2ktPjJT34ix3ESLpMnT47ff+rUKdXX12vcuHEaM2aM6urqzjuJp99s3bpVt912m8rKyuQ4jl5//fWE+40xevLJJ1VaWqqRI0eqqqpKn3zyScI2R48e1cKFC5WXl6eCggItXrxYx48ft/guUudS9XPfffed95mqqalJ2Mav9dPS0qJZs2Zp7NixKi4u1h133KG2traEbQbynTpw4IDmzZunUaNGqbi4WI888oj6+vpsvpWUGUgd3Xzzzed9hh544IGEbZKtI88F1yuvvKLGxkatWLFCf/jDHzRjxgxVV1cnnJgym3zrW99SR0dH/PL222/H73vooYf0H//xH3r11Ve1ZcsWHTx4UPPnz09jaVPvxIkTmjFjhl544YV+71+5cqWee+45rVq1Stu3b9fo0aNVXV2tU6dOxbdZuHChPvzwQ23YsEHr1q3T1q1btXTpUltvIaUuVT+SVFNTk/CZ+vWvf51wv1/rZ8uWLaqvr9e2bdu0YcMG9fb2au7cuTpx4kR8m0t9p6LRqObNm6fTp0/r3Xff1UsvvaQ1a9boySefTMdbct1A6kiSlixZkvAZWrlyZfw+V+rIeMwNN9xg6uvr49ej0agpKyszLS0taSxVeqxYscLMmDGj3/u6urrM8OHDzauvvhq/7aOPPjKSTGtrq6USppck89prr8Wvx2IxEwqFzM9//vP4bV1dXSYYDJpf//rXxhhj9u7daySZ999/P77Nm2++aRzHMX/+85+tld2Gc+vHGGMWLVpkbr/99gs+Jpvq59ChQ0aS2bJlizFmYN+p//zP/zSBQMCEw+H4Ni+++KLJy8szPT09dt+ABefWkTHG/PVf/7X5+7//+ws+xo068lSL6/Tp09q5c6eqqqritwUCAVVVVam1tTWNJUufTz75RGVlZbriiiu0cOFCHThwQJK0c+dO9fb2JtTV5MmTNXHixKytq/379yscDifUSX5+vioqKuJ10traqoKCAl1//fXxbaqqqhQIBLR9+3brZU6HzZs3q7i4WFdffbWWLVumI0eOxO/Lpvrp7u6WJBUWFkoa2HeqtbVV06ZNU0lJSXyb6upqRSIRffjhhxZLb8e5dXTWyy+/rKKiIk2dOlVNTU06efJk/D436shTi+x+/vnnikajCW9YkkpKSvTxxx+nqVTpU1FRoTVr1ujqq69WR0eHmpub9Z3vfEd79uxROBxWbm6uCgoKEh5TUlKicDicngKn2dn33d/n5+x94XBYxcXFCfcPGzZMhYWFWVFvNTU1mj9/vsrLy7Vv3z49/vjjqq2tVWtrq3JycrKmfmKxmJYvX66bbrpJU6dOlaQBfafC4XC/n6+z9/lJf3UkSffcc48mTZqksrIy7d69W4899pja2tr029/+VpI7deSp4EKi2tra+N/Tp09XRUWFJk2apN/85jcaOXJkGksGr1qwYEH872nTpmn69Om68sortXnzZs2ZMyeNJbOrvr5ee/bsSRgzRqIL1dFXxzunTZum0tJSzZkzR/v27dOVV17pymt7qquwqKhIOTk5583i6ezsVCgUSlOpMkdBQYG++c1vqr29XaFQSKdPn1ZXV1fCNtlcV2ff98U+P6FQ6LyJPn19fTp69GhW1tsVV1yhoqIitbe3S8qO+mloaNC6dev01ltv6fLLL4/fPpDvVCgU6vfzdfY+v7hQHfWnoqJCkhI+Q8nWkaeCKzc3VzNnztTGjRvjt8ViMW3cuFGVlZVpLFlmOH78uPbt26fS0lLNnDlTw4cPT6irtrY2HThwIGvrqry8XKFQKKFOIpGItm/fHq+TyspKdXV1aefOnfFtNm3apFgsFv8CZpPPPvtMR44cUWlpqSR/148xRg0NDXrttde0adMmlZeXJ9w/kO9UZWWl/vjHPyaE+4YNG5SXl6cpU6bYeSMpdKk66s+uXbskKeEzlHQdDXEySdr8+7//uwkGg2bNmjVm7969ZunSpaagoCBhhkq2+NGPfmQ2b95s9u/fb9555x1TVVVlioqKzKFDh4wxxjzwwANm4sSJZtOmTWbHjh2msrLSVFZWprnUqXXs2DHzwQcfmA8++MBIMv/8z/9sPvjgA/OnP/3JGGPMz372M1NQUGDeeOMNs3v3bnP77beb8vJy88UXX8Sfo6amxlx33XVm+/bt5u233zZXXXWVufvuu9P1llx1sfo5duyYefjhh01ra6vZv3+/+f3vf2/+6q/+ylx11VXm1KlT8efwa/0sW7bM5Ofnm82bN5uOjo745eTJk/FtLvWd6uvrM1OnTjVz5841u3btMuvXrzfjx483TU1N6XhLrrtUHbW3t5unnnrK7Nixw+zfv9+88cYb5oorrjCzZ8+OP4cbdeS54DLGmOeff95MnDjR5ObmmhtuuMFs27Yt3UVKi7vuusuUlpaa3Nxc87Wvfc3cddddpr29PX7/F198Yf7u7/7OXHbZZWbUqFHmzjvvNB0dHWksceq99dZbRtJ5l0WLFhljzkyJ//GPf2xKSkpMMBg0c+bMMW1tbQnPceTIEXP33XebMWPGmLy8PHP//febY8eOpeHduO9i9XPy5Ekzd+5cM378eDN8+HAzadIks2TJkvN+FPq1fvqrF0lm9erV8W0G8p367//+b1NbW2tGjhxpioqKzI9+9CPT29tr+d2kxqXq6MCBA2b27NmmsLDQBINB841vfMM88sgjpru7O+F5kq0jTmsCAPAUT41xAQBAcAEAPIXgAgB4CsEFAPAUggsA4CkEFwDAUwguAICnEFwAAE8huAAAnkJwAQA8heACAHgKwQUA8JT/D3PxsIZqMhGUAAAAAElFTkSuQmCC", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "with CustomReader(\"../tests/fixtures/dataset_2d.nc\", \"dataset\") as src:\n", + " print(src.ds)\n", + " tile = src.tms.tile(src.bounds[0], src.bounds[1], src.minzoom)\n", + " img = src.tile(*tile)\n", + "\n", + "plt.imshow(img.data_as_image())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "py39", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/titiler/xarray/notebooks/xarray_reader.ipynb b/src/titiler/xarray/notebooks/xarray_reader.ipynb new file mode 100644 index 000000000..33da74dd3 --- /dev/null +++ b/src/titiler/xarray/notebooks/xarray_reader.ipynb @@ -0,0 +1,324 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using titiler.xarray Reader" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from titiler.xarray.io import Reader\n", + "\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"bounds\": [\n", + " -170.085,\n", + " -80.08,\n", + " 169.914999999975,\n", + " 79.91999999999659\n", + " ],\n", + " \"crs\": \"http://www.opengis.net/def/crs/EPSG/0/4326\",\n", + " \"band_metadata\": [\n", + " [\n", + " \"b1\",\n", + " {}\n", + " ]\n", + " ],\n", + " \"band_descriptions\": [\n", + " [\n", + " \"b1\",\n", + " \"dataset\"\n", + " ]\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"nodata_type\": \"Nodata\",\n", + " \"colorinterp\": null,\n", + " \"scales\": null,\n", + " \"offsets\": null,\n", + " \"colormap\": null,\n", + " \"name\": \"dataset\",\n", + " \"count\": 1,\n", + " \"width\": 2000,\n", + " \"height\": 1000,\n", + " \"dimensions\": [\n", + " \"y\",\n", + " \"x\"\n", + " ],\n", + " \"attrs\": {\n", + " \"valid_min\": 1.0,\n", + " \"valid_max\": 1000.0,\n", + " \"fill_value\": 0\n", + " }\n", + "}\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# With a simple 2 Dataset\n", + "with Reader(\"../tests/fixtures/dataset_2d.nc\", \"dataset\") as src:\n", + " print(src.info().model_dump_json(indent=4))\n", + "\n", + " tile = src.tms.tile(src.bounds[0], src.bounds[1], src.minzoom)\n", + " img = src.tile(*tile)\n", + "\n", + "plt.imshow(img.data_as_image())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"bounds\": [\n", + " -170.085,\n", + " -80.08,\n", + " 169.914999999975,\n", + " 79.91999999999659\n", + " ],\n", + " \"crs\": \"http://www.opengis.net/def/crs/EPSG/0/4326\",\n", + " \"band_metadata\": [\n", + " [\n", + " \"b1\",\n", + " {}\n", + " ],\n", + " [\n", + " \"b2\",\n", + " {}\n", + " ]\n", + " ],\n", + " \"band_descriptions\": [\n", + " [\n", + " \"b1\",\n", + " \"2022-01-01T00:00:00.000000000\"\n", + " ],\n", + " [\n", + " \"b2\",\n", + " \"2023-01-01T00:00:00.000000000\"\n", + " ]\n", + " ],\n", + " \"dtype\": \"float64\",\n", + " \"nodata_type\": \"Nodata\",\n", + " \"colorinterp\": null,\n", + " \"scales\": null,\n", + " \"offsets\": null,\n", + " \"colormap\": null,\n", + " \"name\": \"dataset\",\n", + " \"count\": 2,\n", + " \"width\": 2000,\n", + " \"height\": 1000,\n", + " \"dimensions\": [\n", + " \"time\",\n", + " \"y\",\n", + " \"x\"\n", + " ],\n", + " \"attrs\": {\n", + " \"valid_min\": 1.0,\n", + " \"valid_max\": 1000.0,\n", + " \"fill_value\": 0\n", + " }\n", + "}\n", + "There are 2 Time Values (represented as Bands)\n", + "[('b1', '2022-01-01T00:00:00.000000000'), ('b2', '2023-01-01T00:00:00.000000000')]\n" + ] + } + ], + "source": [ + "# With a 3D Dataset (Time, X, Y)\n", + "with Reader(\"../tests/fixtures/dataset_3d.nc\", \"dataset\") as src:\n", + " info = src.info()\n", + "\n", + " print(info.model_dump_json(indent=4))\n", + " print(\"There are 2 Time Values (represented as Bands)\")\n", + " print(info.band_descriptions)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# View the first time value\n", + "with Reader(\"../tests/fixtures/dataset_3d.nc\", \"dataset\") as src:\n", + " tile = src.tms.tile(src.bounds[0], src.bounds[1], src.minzoom)\n", + " img = src.tile(*tile, indexes=1)\n", + "\n", + "plt.imshow(img.data_as_image())" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# View the second time value\n", + "with Reader(\"../tests/fixtures/dataset_3d.nc\", \"dataset\") as src:\n", + " tile = src.tms.tile(src.bounds[0], src.bounds[1], src.minzoom)\n", + " img = src.tile(*tile, indexes=2)\n", + "\n", + "plt.imshow(img.data_as_image())" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[('b1', '2023-01-01T00:00:00.000000000')]\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Select time with Xarray Sel method\n", + "with Reader(\n", + " \"../tests/fixtures/dataset_3d.nc\",\n", + " \"dataset\",\n", + " sel=[\"time=2023-01-01\"],\n", + " method=\"nearest\",\n", + ") as src:\n", + " print(src.info().band_descriptions)\n", + " tile = src.tms.tile(src.bounds[0], src.bounds[1], src.minzoom)\n", + " img = src.tile(*tile)\n", + "\n", + "plt.imshow(img.data_as_image())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "py39", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/titiler/xarray/pyproject.toml b/src/titiler/xarray/pyproject.toml new file mode 100644 index 000000000..adcce6cce --- /dev/null +++ b/src/titiler/xarray/pyproject.toml @@ -0,0 +1,96 @@ +[project] +name = "titiler-xarray" +description = "Xarray plugin for TiTiler." +readme = "README.md" +requires-python = ">=3.11" +authors = [ + {name = "Vincent Sarago", email = "vincent@developmentseed.com"}, + {name = "Aimee Barciauskas", email = "aimee@developmentseed.com"}, +] +license = {text = "MIT"} +keywords = [ + "TiTiler", + "Xarray", + "Zarr", + "NetCDF", + "HDF", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Scientific/Engineering :: GIS", +] +dynamic = ["version"] +dependencies = [ + "titiler-core==2.0.0b2", + "xarray", + "rioxarray", + "obstore", + "zarr>=3.1,<4.0", +] + +[project.optional-dependencies] +fs = [ + "h5netcdf", + "h5py", + "fsspec", + "s3fs>=2025.2.0", + "aiohttp", + "gcsfs", + "requests", +] +telemetry = [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-instrumentation-fastapi", + "opentelemetry-instrumentation-logging", + "opentelemetry-exporter-otlp", +] + +[dependency-groups] +test = [ + "pytest", + "pytest-cov", + "pytest-asyncio", + "httpx", + "h5netcdf", + "h5py", + "fsspec", + "s3fs", + "aiohttp", + "requests", +] +upstream = [ + 'xarray @ git+https://github.com/pydata/xarray', + 'universal_pathlib @ git+https://github.com/fsspec/universal_pathlib', + 'numcodecs @ git+https://github.com/zarr-developers/numcodecs', + 'ujson @ git+https://github.com/ultrajson/ultrajson', + 'zarr @ git+https://github.com/zarr-developers/zarr-python', +] + +[project.urls] +Homepage = "https://developmentseed.org/titiler/" +Documentation = "https://developmentseed.org/titiler/" +Issues = "https://github.com/developmentseed/titiler/issues" +Source = "https://github.com/developmentseed/titiler" +Changelog = "https://developmentseed.org/titiler/release-notes/" + +[tool.hatch.version] +path = "titiler/xarray/__init__.py" + +[tool.hatch.build.targets.sdist] +only-include = ["titiler"] + +[tool.hatch.build.targets.wheel] +only-include = ["titiler"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/src/titiler/xarray/tests/conftest.py b/src/titiler/xarray/tests/conftest.py new file mode 100644 index 000000000..dc4af91ae --- /dev/null +++ b/src/titiler/xarray/tests/conftest.py @@ -0,0 +1 @@ +"""titiler.xarray test configuration.""" diff --git a/src/titiler/xarray/tests/fixtures/dataset_2d.nc b/src/titiler/xarray/tests/fixtures/dataset_2d.nc new file mode 100644 index 000000000..2b0b42adc Binary files /dev/null and b/src/titiler/xarray/tests/fixtures/dataset_2d.nc differ diff --git a/src/titiler/xarray/tests/fixtures/dataset_3d.nc b/src/titiler/xarray/tests/fixtures/dataset_3d.nc new file mode 100644 index 000000000..c367e452e Binary files /dev/null and b/src/titiler/xarray/tests/fixtures/dataset_3d.nc differ diff --git a/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zattrs b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zattrs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zattrs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zgroup b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zgroup new file mode 100644 index 000000000..3b7daf227 --- /dev/null +++ b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zmetadata b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zmetadata new file mode 100644 index 000000000..061ce5cb0 --- /dev/null +++ b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zmetadata @@ -0,0 +1,119 @@ +{ + "metadata": { + ".zattrs": {}, + ".zgroup": { + "zarr_format": 2 + }, + "dataset/.zarray": { + "chunks": [ + 1, + 250, + 500 + ], + "compressor": { + "blocksize": 0, + "clevel": 5, + "cname": "lz4", + "id": "blosc", + "shuffle": 1 + }, + "dtype": " xarray.Dataset: + """Open Xarray dataset with fsspec. + + Args: + src_path (str): dataset path. + group (Optional, str): path to the netCDF/Zarr group in the given file to open given as a str. + decode_times (bool): If True, decode times encoded in the standard NetCDF datetime format into datetime objects. Otherwise, leave them encoded as numbers. + + Returns: + xarray.Dataset + + """ + import fsspec # noqa + + parsed = urlparse(src_path) + protocol = parsed.scheme or "file" + + if not special_arg: + raise ValueError("you forgot the special_arg :(") + + xr_open_args: dict[str, Any] = { + "decode_coords": "all", + "decode_times": decode_times, + "engine": "h5netcdf", + "lock": False, + } + + # Argument if we're opening a datatree + if group is not None: + xr_open_args["group"] = group + + fs = fsspec.filesystem(protocol) + ds = xarray.open_dataset(fs.open(src_path), **xr_open_args) + return ds + + with Reader( + src_path=src_path, + opener=custom_netcdf_opener, + opener_options={"special_arg": True}, + variable="dataset", + ) as src: + assert src.info() + + with pytest.raises(ValueError): + with Reader( + src_path=src_path, + opener=custom_netcdf_opener, + opener_options={"special_arg": False}, + variable="dataset", + ) as src: + pass + + +@pytest.mark.parametrize( + "group", + [0, 1, 2], +) +def test_zarr_group(group): + """test reader.""" + src_path = os.path.join(prefix, "pyramid.zarr") + + with Reader(src_path, variable="dataset", group=str(group)) as src: + assert src.info() + assert src.tile(0, 0, 0) + assert src.point(0, 0).data[0] == group * 2 + 1 + + +@pytest.mark.parametrize( + "src_path,options", + [ + ("s3://mur-sst/zarr-v1", {"anon": True}), + ( + "https://nasa-power.s3.amazonaws.com/syn1deg/temporal/power_syn1deg_monthly_temporal_lst.zarr", + {}, + ), + (os.path.join(prefix, "dataset_3d.zarr"), {}), + ], +) +def test_io_fs_open_dataset(src_path, options): + """test fs_open_dataset with cloud hosted files.""" + with fs_open_dataset(src_path, **options) as ds: + assert list(ds.data_vars) + + +@pytest.mark.parametrize( + "src_path,options", + [ + # Let's assume we don't have S3 Credentials + ("s3://mur-sst/zarr-v1", {"skip_signature": True}), + ("s3://mur-sst/zarr-v1", {"skip_signature": True, "region": "us-west-2"}), + # HTTS url are considered public + ("https://mur-sst.s3.us-west-2.amazonaws.com/zarr-v1", {}), + # NOTE: https://github.com/developmentseed/obstore/pull/590 + # ( + # "https://nasa-power.s3.amazonaws.com/syn1deg/temporal/power_syn1deg_monthly_temporal_lst.zarr", + # {}, + # ), + ( + "https://nasa-power.s3.us-west-2.amazonaws.com/syn1deg/temporal/power_syn1deg_monthly_temporal_lst.zarr", + {}, + ), + (os.path.join(prefix, "dataset_3d.zarr"), {}), + ], +) +def test_io_open_zarr(src_path, options): + """test open_zarr with cloud hosted files.""" + with open_zarr(src_path, **options) as ds: + assert list(ds.data_vars) + + +@pytest.mark.parametrize( + "sel,expected", + [ + ( + ["time=2022-01-01", "level=10"], + [ + {"dimension": "time", "values": ["2022-01-01"], "method": None}, + {"dimension": "level", "values": ["10"], "method": None}, + ], + ), + ( + ["time=2022-01-01", "time=2022-01-02"], + [ + { + "dimension": "time", + "values": ["2022-01-01", "2022-01-02"], + "method": None, + }, + ], + ), + ( + ["time=pad::2022-01-01", "time=2022-01-02", "level=nearest::10"], + [ + { + "dimension": "time", + "values": ["2022-01-01", "2022-01-02"], + "method": "pad", + }, + {"dimension": "level", "values": ["10"], "method": "nearest"}, + ], + ), + ([], []), + ], +) +def test_parse_dsl(sel, expected): + """test _parse_dsl function.""" + result = _parse_dsl(sel) + assert result == expected + + +def test_parse_dsl_invalid(): + """Should raise a ValueError when multiple methods are set for a dimension.""" + sel = ["time=pad::2022-01-01", "time=nearest::2022-01-02"] + with pytest.raises(ValueError): + _parse_dsl(sel) diff --git a/src/titiler/xarray/tests/test_validate_extension.py b/src/titiler/xarray/tests/test_validate_extension.py new file mode 100644 index 000000000..2439e8012 --- /dev/null +++ b/src/titiler/xarray/tests/test_validate_extension.py @@ -0,0 +1,105 @@ +"""test titiler.xarray factory.""" + +import os + +import pytest +from fastapi import FastAPI +from starlette.testclient import TestClient + +from titiler.xarray.extensions import ValidateExtension +from titiler.xarray.factory import TilerFactory + +prefix = os.path.join(os.path.dirname(__file__), "fixtures") +dataset_3d_zarr = os.path.join(prefix, "dataset_3d.zarr") +zarr_pyramid = os.path.join(prefix, "pyramid.zarr") +zarr_coord = os.path.join(prefix, "zarr_invalid_coord.zarr") +zarr_coord_name = os.path.join(prefix, "zarr_invalid_coord_name.zarr") +zarr_5d = os.path.join(prefix, "zarr_5d.zarr") + + +@pytest.fixture +def app_zarr(): + """App fixture.""" + md = TilerFactory( + router_prefix="/md", + extensions=[ + ValidateExtension(), + ], + ) + assert len(md.router.routes) == 16 + + app = FastAPI() + app.include_router(md.router, prefix="/md") + with TestClient(app) as client: + yield client + + +def test_validate_extension(app_zarr): + """Test /dataset endpoints.""" + resp = app_zarr.get("/md/validate", params={"url": dataset_3d_zarr}) + assert resp.status_code == 200 + assert resp.headers["content-type"] == "application/json" + assert list(resp.json()) == ["dataset"] + assert resp.json()["dataset"]["compatible_with_titiler"] + + resp = app_zarr.get( + "/md/validate", params={"url": dataset_3d_zarr, "variables": "dataset"} + ) + assert resp.status_code == 200 + assert resp.headers["content-type"] == "application/json" + assert list(resp.json()) == ["dataset"] + assert resp.json()["dataset"]["compatible_with_titiler"] + + resp = app_zarr.get("/md/validate", params={"url": zarr_pyramid, "group": "0"}) + assert resp.status_code == 200 + assert resp.headers["content-type"] == "application/json" + assert list(resp.json()) == ["dataset"] + assert resp.json()["dataset"]["compatible_with_titiler"] + + resp = app_zarr.get( + "/md/validate", + params={"url": zarr_pyramid, "group": "0", "variables": "dataset"}, + ) + assert resp.status_code == 200 + assert resp.headers["content-type"] == "application/json" + assert list(resp.json()) == ["dataset"] + assert resp.json()["dataset"]["compatible_with_titiler"] + + resp = app_zarr.get( + "/md/validate", + params={"url": zarr_coord}, + ) + assert resp.status_code == 200 + assert resp.headers["content-type"] == "application/json" + val = resp.json() + assert list(val) == ["dataset"] + assert not val["dataset"]["compatible_with_titiler"] + assert val["dataset"]["errors"] == [ + "Dataset bounds are not valid, must be in [-180, 180] and [-90, 90]" + ] + + resp = app_zarr.get( + "/md/validate", + params={"url": zarr_coord}, + ) + assert resp.status_code == 200 + assert resp.headers["content-type"] == "application/json" + val = resp.json() + assert list(val) == ["dataset"] + assert not val["dataset"]["compatible_with_titiler"] + assert val["dataset"]["errors"] == [ + "Dataset bounds are not valid, must be in [-180, 180] and [-90, 90]" + ] + + resp = app_zarr.get( + "/md/validate", + params={"url": zarr_5d}, + ) + assert resp.status_code == 200 + assert resp.headers["content-type"] == "application/json" + val = resp.json() + assert list(val) == ["dataset"] + assert val["dataset"]["compatible_with_titiler"] + assert val["dataset"]["warnings"] == [ + "DataArray has too many dimension (5) for titiler.xarray, dimensions reduction (sel) will be required." + ] diff --git a/src/titiler/xarray/titiler/xarray/__init__.py b/src/titiler/xarray/titiler/xarray/__init__.py new file mode 100644 index 000000000..034fa0b97 --- /dev/null +++ b/src/titiler/xarray/titiler/xarray/__init__.py @@ -0,0 +1,3 @@ +"""titiler.xarray""" + +__version__ = "2.0.0b2" diff --git a/src/titiler/xarray/titiler/xarray/dependencies.py b/src/titiler/xarray/titiler/xarray/dependencies.py new file mode 100644 index 000000000..f8443b039 --- /dev/null +++ b/src/titiler/xarray/titiler/xarray/dependencies.py @@ -0,0 +1,163 @@ +"""titiler.xarray dependencies.""" + +from dataclasses import dataclass +from typing import Annotated + +import numpy +from fastapi import Query +from pydantic import Field +from pydantic.types import StringConstraints +from rio_tiler.types import RIOResampling, WarpResampling + +from titiler.core.dependencies import DefaultDependency + + +@dataclass +class XarrayIOParams(DefaultDependency): + """Dataset IO Options.""" + + group: Annotated[ + str | None, + Query( + description="Select a specific zarr group from a zarr hierarchy. Could be associated with a zoom level or dataset." + ), + ] = None + + decode_times: Annotated[ + bool | None, + Query( + title="decode_times", + description="Whether to decode times", + ), + ] = None + + +SelDimStr = Annotated[ + str, + StringConstraints( + pattern=r"^[^=]+=((nearest|pad|ffill|backfill|bfill)::)?[^=::]+$" + ), +] + + +@dataclass +class XarrayDsParams(DefaultDependency): + """Xarray Dataset Options.""" + + variable: Annotated[str, Query(description="Xarray Variable name.")] + + sel: Annotated[ + list[SelDimStr] | None, + Query( + description="Xarray Indexing using dimension names `{dimension}={value}` or `{dimension}={method}::{value}`.", + ), + ] = None + + +@dataclass +class XarrayParams(XarrayIOParams, XarrayDsParams): + """Xarray Reader dependency.""" + + pass + + +@dataclass +class CompatXarrayParams(XarrayIOParams): + """Custom XarrayParams endpoints. + + This Dependency aims to be used in a tiler where both GDAL/Xarray dataset would be supported. + By default `variable` won't be required but when using an Xarray dataset, + it would fail without the variable query-parameter set. + """ + + variable: Annotated[str | None, Query(description="Xarray Variable name.")] = None + + sel: Annotated[ + list[SelDimStr] | None, + Query( + description="Xarray Indexing using dimension names `{dimension}={value}` or `{dimension}={method}::{value}`.", + ), + ] = None + + +@dataclass +class DatasetParams(DefaultDependency): + """Low level WarpedVRT Optional parameters.""" + + nodata: Annotated[ + str | int | float | None, + Query( + title="Nodata value", + description="Overwrite internal Nodata value", + ), + ] = None + reproject_method: Annotated[ + WarpResampling | None, + Query( + alias="reproject", + description="WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`.", + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if self.nodata is not None: + self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata) + + +# Custom PartFeatureParams which add `resampling` +@dataclass +class PartFeatureParams(DefaultDependency): + """Common parameters for bbox and feature.""" + + max_size: Annotated[ + int | None, Field(description="Maximum image size to read onto.") + ] = None + height: Annotated[int | None, Field(description="Force output image height.")] = ( + None + ) + width: Annotated[int | None, Field(description="Force output image width.")] = None + resampling_method: Annotated[ + RIOResampling | None, + Query( + alias="resampling", + description="RasterIO resampling algorithm. Defaults to `nearest`.", + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if self.width or self.height: + self.max_size = None + + +# Custom PreviewParams which add `resampling` +@dataclass +class PreviewParams(DefaultDependency): + """Common Preview parameters.""" + + max_size: Annotated[ + int | None, Field(description="Maximum image size to read onto.") + ] = 1024 + height: Annotated[int | None, Field(description="Force output image height.")] = ( + None + ) + width: Annotated[int | None, Field(description="Force output image width.")] = None + resampling_method: Annotated[ + RIOResampling | None, + Query( + alias="resampling", + description="RasterIO resampling algorithm. Defaults to `nearest`.", + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if self.width or self.height: + self.max_size = None + + # NOTE: By default we don't exclude None when we forward the parameter to the preview() method + # because we need to be able to pass max_size=None + # So we need to set the `resampling_method` to a default = 'nearest' + # https://github.com/developmentseed/titiler/blob/b8cc304382d0cb3b4f16cea9dbb0cfba35517085/src/titiler/core/titiler/core/factory.py#L1300 + self.resampling_method = self.resampling_method or "nearest" diff --git a/src/titiler/xarray/titiler/xarray/extensions.py b/src/titiler/xarray/titiler/xarray/extensions.py new file mode 100644 index 000000000..27dfa8a83 --- /dev/null +++ b/src/titiler/xarray/titiler/xarray/extensions.py @@ -0,0 +1,235 @@ +"""titiler.xarray Extensions.""" + +import sys +import warnings +from collections.abc import Callable +from typing import Annotated + +import xarray +from attrs import define +from fastapi import Depends, Query +from rio_tiler.constants import WGS84_CRS +from starlette.responses import HTMLResponse + +from titiler.core.dependencies import DefaultDependency +from titiler.core.factory import FactoryExtension +from titiler.core.resources.enums import MediaType +from titiler.xarray.dependencies import XarrayIOParams +from titiler.xarray.factory import TilerFactory +from titiler.xarray.io import X_DIM_NAMES, Y_DIM_NAMES, open_zarr + +# Ref: https://docs.pydantic.dev/2.12/errors/usage_errors/#typed-dict-version +if sys.version_info >= (3, 12): + from typing import TypedDict +else: + from typing_extensions import TypedDict + + +@define +class VariablesExtension(FactoryExtension): + """Add /variables endpoint to a Xarray TilerFactory.""" + + # Custom dependency for /variables + io_dependency: type[DefaultDependency] = XarrayIOParams + dataset_opener: Callable[..., xarray.Dataset] = open_zarr + + def __attrs_post_init__(self): + """raise deprecation warning.""" + warnings.warn( + "VariablesExtension extension is deprecated and will be removed in next titiler version", + DeprecationWarning, + stacklevel=1, + ) + + def register(self, factory: TilerFactory): # type: ignore [override] + """Register endpoint to the tiler factory.""" + + @factory.router.get( + "/variables", + response_model=list[str], + responses={200: {"description": "Return Xarray Dataset variables."}}, + ) + def variables( + src_path=Depends(factory.path_dependency), + io_params=Depends(self.io_dependency), + ): + """return available variables.""" + with self.dataset_opener(src_path, **io_params.as_dict()) as ds: + return list(ds.data_vars) # type: ignore + + +@define +class DatasetMetadataExtension(FactoryExtension): + """Add dataset metadata endpoints to a Xarray TilerFactory.""" + + io_dependency: type[DefaultDependency] = XarrayIOParams + dataset_opener: Callable[..., xarray.Dataset] = open_zarr + + def register(self, factory: TilerFactory): # type: ignore [override] # noqa: C901 + """Register endpoint to the tiler factory.""" + + @factory.router.get( + "/dataset/", + responses={ + 200: { + "description": "Returns the HTML representation of the Xarray Dataset.", + "content": { + MediaType.html.value: {}, + }, + }, + }, + response_class=HTMLResponse, + ) + def dataset_metadata_html( + src_path=Depends(factory.path_dependency), + io_params=Depends(self.io_dependency), + ): + """Returns the HTML representation of the Xarray Dataset.""" + with self.dataset_opener(src_path, **io_params.as_dict()) as ds: + return HTMLResponse(ds._repr_html_()) + + @factory.router.get( + "/dataset/dict", + responses={ + 200: {"description": "Returns the full Xarray dataset as a dictionary."} + }, + ) + def dataset_metadata_dict( + src_path=Depends(factory.path_dependency), + io_params=Depends(self.io_dependency), + ): + """Returns the full Xarray dataset as a dictionary.""" + with self.dataset_opener(src_path, **io_params.as_dict()) as ds: + return ds.to_dict(data=False) + + @factory.router.get( + "/dataset/keys", + response_model=list[str], + responses={ + 200: { + "description": "Returns the list of keys/variables in the Dataset." + } + }, + ) + def dataset_variables( + src_path=Depends(factory.path_dependency), + io_params=Depends(self.io_dependency), + ): + """Returns the list of keys/variables in the Dataset.""" + with self.dataset_opener(src_path, **io_params.as_dict()) as ds: + return list(ds.data_vars) + + +class ValidationInfo(TypedDict): + """Variable Validation model.""" + + compatible_with_titiler: bool + errors: list[str] + warnings: list[str] + + +@define +class ValidateExtension(FactoryExtension): + """Add /validate endpoints to a Xarray TilerFactory.""" + + io_dependency: type[DefaultDependency] = XarrayIOParams + dataset_opener: Callable[..., xarray.Dataset] = open_zarr + + def _validate_variable(self, da: xarray.DataArray) -> ValidationInfo: # noqa: C901 + errors: list[str] = [] + warnings: list[str] = [] + + if len(da.dims) not in [2, 3]: + warnings.append( + f"DataArray has too many dimension ({len(da.dims)}) for titiler.xarray, dimensions reduction (sel) will be required.", + ) + + if "y" not in da.dims: + try: + y_dim = next(name for name in Y_DIM_NAMES if name in da.dims) + da = da.rename({y_dim: "y"}) + + except StopIteration: + errors.append( + "Dataset does not have compatible `Y` spatial coordinates" + ) + + if "x" not in da.dims: + try: + x_dim = next(name for name in X_DIM_NAMES if name in da.dims) + da = da.rename({x_dim: "x"}) + except StopIteration: + errors.append( + "Dataset does not have compatible `X` spatial coordinates" + ) + + if {"x", "y"}.issubset(set(da.dims)): + if extra_dims := [d for d in da.dims if d not in ["x", "y"]]: + da = da.transpose(*extra_dims, "y", "x") + else: + da = da.transpose("y", "x") + + bounds = da.rio.bounds() + if not bounds: + errors.append("Dataset does not have rioxarray bounds") + + res = da.rio.resolution() + if not res: + errors.append("Dataset does not have rioxarray resolution") + + if res and bounds: + crs = da.rio.crs or "epsg:4326" + xres, yres = map(abs, res) + + # Adjust the longitude coordinates to the -180 to 180 range + if crs == "epsg:4326" and (da.x > 180 + xres / 2).any(): + da = da.assign_coords(x=(da.x + 180) % 360 - 180) + + # Sort the dataset by the updated longitude coordinates + da = da.sortby(da.x) + + bounds = tuple(da.rio.bounds()) + if crs == WGS84_CRS and ( + bounds[0] + xres / 2 < -180 + or bounds[1] + yres / 2 < -90 + or bounds[2] - xres / 2 > 180 + or bounds[3] - yres / 2 > 90 + ): + errors.append( + "Dataset bounds are not valid, must be in [-180, 180] and [-90, 90]" + ) + + if not da.rio.transform(): + errors.append("Dataset does not have rioxarray transform") + + return { + "compatible_with_titiler": True if not errors else False, + "errors": errors, + "warnings": warnings, + } + + def register(self, factory: TilerFactory): # type: ignore [override] # noqa: C901 + """Register endpoint to the tiler factory.""" + + @factory.router.get( + "/validate", + responses={ + 200: { + "content": { + "application/json": {}, + }, + }, + }, + response_model=dict[str, ValidationInfo], + ) + def validate_dataset( + src_path=Depends(factory.path_dependency), + io_params=Depends(self.io_dependency), + variables: Annotated[ + list[str] | None, Query(description="Xarray Variable name.") + ] = None, + ): + """Returns the HTML representation of the Xarray Dataset.""" + with self.dataset_opener(src_path, **io_params.as_dict()) as dst: + variables = variables or list(dst.data_vars) # type: ignore + return {v: self._validate_variable(dst[v]) for v in variables} diff --git a/src/titiler/xarray/titiler/xarray/factory.py b/src/titiler/xarray/titiler/xarray/factory.py new file mode 100644 index 000000000..68aab715f --- /dev/null +++ b/src/titiler/xarray/titiler/xarray/factory.py @@ -0,0 +1,234 @@ +"""TiTiler.xarray factory.""" + +import logging +import warnings +from collections.abc import Callable +from typing import Annotated, Any + +import rasterio +from attrs import define +from fastapi import Body, Depends, Query +from geojson_pydantic.features import Feature, FeatureCollection +from rio_tiler.constants import WGS84_CRS +from rio_tiler.io import XarrayReader +from rio_tiler.models import Info + +from titiler.core.dependencies import ( + BidxParams, + CoordCRSParams, + CRSParams, + DatasetPathParams, + DefaultDependency, + DstCRSParams, + HistogramParams, + StatisticsParams, +) +from titiler.core.factory import TilerFactory as BaseTilerFactory +from titiler.core.models.responses import InfoGeoJSON, StatisticsGeoJSON +from titiler.core.resources.responses import GeoJSONResponse, JSONResponse +from titiler.core.utils import bounds_to_geometry +from titiler.xarray.dependencies import ( + DatasetParams, + PartFeatureParams, + PreviewParams, + XarrayParams, +) +from titiler.xarray.io import Reader + +logger = logging.getLogger(__name__) + + +@define(kw_only=True) +class TilerFactory(BaseTilerFactory): + """Xarray Tiler Factory.""" + + reader: type[XarrayReader] = Reader + + path_dependency: Callable[..., Any] = DatasetPathParams + + reader_dependency: type[DefaultDependency] = XarrayParams + + # Indexes Dependencies + layer_dependency: type[DefaultDependency] = BidxParams + + # Dataset Options (nodata, reproject) + dataset_dependency: type[DefaultDependency] = DatasetParams + + # Tile/Tilejson/WMTS Dependencies (Not used in titiler.xarray) + tile_dependency: type[DefaultDependency] = DefaultDependency + + # Statistics/Histogram Dependencies + stats_dependency: type[DefaultDependency] = StatisticsParams + histogram_dependency: type[DefaultDependency] = HistogramParams + + img_preview_dependency: type[DefaultDependency] = PreviewParams + img_part_dependency: type[DefaultDependency] = PartFeatureParams + + add_viewer: bool = True + add_part: bool = True + + # /map endpoints disabled by default + add_ogc_maps: bool = False + + # /preview endpoints disabled by default + add_preview: bool = False + + def __attrs_post_init__(self): + """Raise warning if preview is enabled.""" + if self.add_preview: + warnings.warn( + "`preview` endpoints enabled Xarray based TilerFactory. MultiDim dataset might not be suitable for preview.", + UserWarning, + stacklevel=1, + ) + + super().__attrs_post_init__() + + # Custom /info endpoints (adds `show_times` options) + def info(self): + """Register /info endpoint.""" + + @self.router.get( + "/info", + response_model=Info, + response_model_exclude_none=True, + response_class=JSONResponse, + responses={200: {"description": "Return dataset's basic info."}}, + operation_id=f"{self.operation_prefix}getInfo", + ) + def info_endpoint( + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + show_times: Annotated[ + bool | None, + Query(description="Show info about the time dimension"), + ] = None, + env=Depends(self.environment_dependency), + ) -> Info: + """Return dataset's basic info.""" + with rasterio.Env(**env): + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + info = src_dst.info().model_dump() + if show_times and "time" in src_dst.input.dims: + times = [str(x.data) for x in src_dst.input.time] + info["count"] = len(times) + info["times"] = times + + return Info(**info) + + @self.router.get( + "/info.geojson", + response_model=InfoGeoJSON, + response_model_exclude_none=True, + response_class=GeoJSONResponse, + responses={ + 200: { + "content": {"application/geo+json": {}}, + "description": "Return dataset's basic info as a GeoJSON feature.", + } + }, + operation_id=f"{self.operation_prefix}getInfoGeoJSON", + ) + def info_geojson( + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + show_times: Annotated[ + bool | None, + Query(description="Show info about the time dimension"), + ] = None, + crs=Depends(CRSParams), + env=Depends(self.environment_dependency), + ): + """Return dataset's basic info as a GeoJSON feature.""" + with rasterio.Env(**env): + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + geometry = bounds_to_geometry(bounds) + info = src_dst.info().model_dump() + if show_times and "time" in src_dst.input.dims: + times = [str(x.data) for x in src_dst.input.time] + info["count"] = len(times) + info["times"] = times + + return Feature( + type="Feature", + bbox=bounds, + geometry=geometry, + properties=info, + ) + + # custom /statistics endpoints (remove /statistics - GET) + def statistics(self): + """add statistics endpoints.""" + + # POST endpoint + @self.router.post( + "/statistics", + response_model=StatisticsGeoJSON, + response_model_exclude_none=True, + response_class=GeoJSONResponse, + responses={ + 200: { + "content": {"application/geo+json": {}}, + "description": "Return dataset's statistics from feature or featureCollection.", + } + }, + operation_id=f"{self.operation_prefix}postStatisticsForGeoJSON", + ) + def geojson_statistics( + geojson: Annotated[ + FeatureCollection | Feature, + Body(description="GeoJSON Feature or FeatureCollection."), + ], + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + image_params=Depends(self.img_part_dependency), + post_process=Depends(self.process_dependency), + stats_params=Depends(self.stats_dependency), + histogram_params=Depends(self.histogram_dependency), + env=Depends(self.environment_dependency), + ): + """Get Statistics from a geojson feature or featureCollection.""" + fc = geojson + if isinstance(fc, Feature): + fc = FeatureCollection(type="FeatureCollection", features=[geojson]) + + with rasterio.Env(**env): + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + for feature in fc.features: + shape = feature.model_dump(exclude_none=True) + image = src_dst.feature( + shape, + shape_crs=coord_crs or WGS84_CRS, + dst_crs=dst_crs, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), + ) + + # Get the coverage % array + coverage_array = image.get_coverage_array( + shape, + shape_crs=coord_crs or WGS84_CRS, + ) + + if post_process: + image = post_process(image) + + stats = image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), + coverage=coverage_array, + ) + + feature.properties = feature.properties or {} + feature.properties.update({"statistics": stats}) + + return fc.features[0] if isinstance(geojson, Feature) else fc diff --git a/src/titiler/xarray/titiler/xarray/io.py b/src/titiler/xarray/titiler/xarray/io.py new file mode 100644 index 000000000..5c080a3eb --- /dev/null +++ b/src/titiler/xarray/titiler/xarray/io.py @@ -0,0 +1,382 @@ +"""titiler.xarray.io""" + +from __future__ import annotations + +import os +import re +from collections.abc import Callable +from functools import cache +from pathlib import Path +from typing import Any, Literal, TypedDict +from urllib.parse import urlparse + +import attr +import httpx +import obstore +import xarray +import zarr +from morecantile import TileMatrixSet +from rio_tiler.constants import WEB_MERCATOR_TMS +from rio_tiler.io.xarray import Options, XarrayReader +from zarr.storage import ObjectStore + +X_DIM_NAMES = ["lon", "longitude", "LON", "LONGITUDE", "Lon", "Longitude"] +Y_DIM_NAMES = ["lat", "latitude", "LAT", "LATITUDE", "Lat", "Latitude"] + + +def _find_bucket_region(bucket: str, use_https: bool = True) -> str | None: + prefix = "https" if use_https else "http" + response = httpx.get(f"{prefix}://{bucket}.s3.amazonaws.com") + return response.headers.get("x-amz-bucket-region") + + +@cache +def open_zarr( # noqa: C901 + src_path: str, + group: str | None = None, + decode_times: bool = True, + decode_coords: str = "all", + infer_region: bool = True, + **kwargs: Any, +) -> xarray.Dataset: + """Open Xarray dataset with fsspec. + + Args: + src_path (str): dataset path. + group (Optional, str): path to the netCDF/Zarr group in the given file to open given as a str. + decode_times (bool): If True, decode times encoded in the standard NetCDF datetime format into datetime objects. Otherwise, leave them encoded as numbers. + + Returns: + xarray.Dataset + + """ + parsed = urlparse(src_path) + if not parsed.scheme: + src_path = str(Path(src_path).resolve()) + src_path = "file://" + src_path + + # Arguments for xarray.open_dataset + # Default args + xr_open_args: dict[str, Any] = { + "engine": "zarr", + "decode_coords": decode_coords, + "decode_times": decode_times, + } + + # Argument if we're opening a datatree + if group is not None: + xr_open_args["group"] = group + + config = {**kwargs} + # We can't expect the users to pass a REGION so we guess it + if parsed.scheme == "s3" or "amazonaws.com" in parsed.netloc: + if "region" not in config and infer_region: + region_name_env = ( + os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION")) + or None + ) + + # s3:// urls + if parsed.scheme == "s3": + config["region"] = _find_bucket_region(parsed.netloc) or region_name_env + + # https://{bucket}.s3.{region}?.amazonaws.com urls + else: + # We assume that https:// url are public object + config["skip_signature"] = True + + # Get Region from URL or guess if needed + if expr := re.compile( + r"(?P[a-z0-9\.\-_]+)\.s3" + r"(\.dualstack)?" + r"(\.(?P[a-z0-9\-_]+))?" + r"\.amazonaws\.(com|cn)", + re.IGNORECASE, + ).match(parsed.netloc): + bucket = expr.groupdict()["bucket"] + if not expr.groupdict().get("region"): + config["region"] = ( + _find_bucket_region(bucket) or region_name_env + ) + + store = obstore.store.from_url(src_path, config=config) # type: ignore + zarr_store = ObjectStore(store=store, read_only=True) + ds = xarray.open_dataset(zarr_store, **xr_open_args) # type: ignore [arg-type] + + return ds + + +def _arrange_dims(da: xarray.DataArray) -> xarray.DataArray: + """Arrange coordinates and time dimensions. + + An rioxarray.exceptions.InvalidDimensionOrder error is raised if the coordinates are not in the correct order time, y, and x. + See: https://github.com/corteva/rioxarray/discussions/674 + + We conform to using x and y as the spatial dimension names.. + + """ + if "x" not in da.dims and "y" not in da.dims: + try: + y_dim = next(name for name in Y_DIM_NAMES if name in da.dims) + x_dim = next(name for name in X_DIM_NAMES if name in da.dims) + except StopIteration as e: + raise ValueError( + f"Couldn't find X and Y spatial coordinates in {da.dims}" + ) from e + + da = da.rename({y_dim: "y", x_dim: "x"}) + + if extra_dims := [d for d in da.dims if d not in ["x", "y"]]: + da = da.transpose(*extra_dims, "y", "x") + else: + da = da.transpose("y", "x") + + # If min/max values are stored in `valid_range` we add them in `valid_min/valid_max` + vmin, vmax = da.attrs.get("valid_min"), da.attrs.get("valid_max") + if "valid_range" in da.attrs and not (vmin is not None and vmax is not None): + valid_range = da.attrs.get("valid_range") + da.attrs.update({"valid_min": valid_range[0], "valid_max": valid_range[1]}) # type: ignore + + return da + + +class selector(TypedDict): + """STAC Item.""" + + dimension: str + values: list[Any] + method: Literal["nearest", "pad", "ffill", "backfill", "bfill"] | None + + +def _parse_dsl(sel: list[str] | None) -> list[selector]: + """Parse sel DSL into dictionary. + + Args: + sel (list of str, optional): List of Xarray Indexes. + + Returns: + list: list of dimension/values/method. + + """ + sel = sel or [] + + _idx: dict[str, list] = {} + for s in sel: + val: str | slice + dim, val = s.split("=") + + if dim in _idx: + _idx[dim].append(val) + else: + _idx[dim] = [val] + + # Loop through all dimension=values selectors + # - parse method::value if provided + # - check if multiple methods are provided for the same dimension + # - cast values to the dimension dtype + # - apply the selection + selectors: list[selector] = [] + for dimension, values in _idx.items(): + methods, values = zip( # type: ignore + *[v.split("::", 1) if "::" in v else (None, v) for v in values] + ) + method_sets = {m for m in methods if m is not None} + if len(method_sets) > 1: + raise ValueError( + f"Multiple selection methods provided for dimension {dimension}: {methods}" + ) + method = method_sets.pop() if method_sets else None + + selectors.append( + { + "dimension": dimension, + "values": list(values), + "method": method, + } + ) + + return selectors + + +def get_variable( + ds: xarray.Dataset, + variable: str, + sel: list[str] | None = None, +) -> xarray.DataArray: + """Get Xarray variable as DataArray. + + Args: + ds (xarray.Dataset): Xarray Dataset. + variable (str): Variable to extract from the Dataset. + sel (list of str, optional): List of Xarray Indexes. + + Returns: + xarray.DataArray: 2D or 3D DataArray. + + """ + da = ds[variable] + + for selector in _parse_dsl(sel): + dimension = selector["dimension"] + values = selector["values"] + method = selector["method"] + + # TODO: add more casting + # cast string to dtype of the dimension + if da[dimension].dtype != "O": + values = [da[dimension].dtype.type(v) for v in values] + + da = da.sel( + {dimension: values[0] if len(values) < 2 else values}, + method=method, + ) + + da = _arrange_dims(da) + + # Make sure we have a valid CRS + crs = da.rio.crs or "epsg:4326" + da = da.rio.write_crs(crs) + + if crs == "epsg:4326" and (da.x > 180).any(): + # Adjust the longitude coordinates to the -180 to 180 range + da = da.assign_coords(x=(da.x + 180) % 360 - 180) + + # Sort the dataset by the updated longitude coordinates + da = da.sortby(da.x) + + assert len(da.dims) in [2, 3], "titiler.xarray can only work with 2D or 3D dataset" + + return da + + +@attr.s +class Reader(XarrayReader): + """Reader: Open Zarr file and access DataArray.""" + + src_path: str = attr.ib() + variable: str = attr.ib() + + # xarray.Dataset options + opener: Callable[..., xarray.Dataset] = attr.ib(default=open_zarr) + opener_options: dict = attr.ib(factory=dict) + + group: str | None = attr.ib(default=None) + decode_times: bool = attr.ib(default=True) + + # xarray.DataArray options + sel: list[str] | None = attr.ib(default=None) + method: Literal["nearest", "pad", "ffill", "backfill", "bfill"] | None = attr.ib( + default=None + ) + + tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) + + options: Options = attr.ib() + + ds: xarray.Dataset = attr.ib(init=False) + input: xarray.DataArray = attr.ib(init=False) + + _dims: list = attr.ib(init=False, factory=list) + + @options.default + def _options_default(self): + return {} + + def __attrs_post_init__(self): + """Set bounds and CRS.""" + opener_options = { + "group": self.group, + "decode_times": self.decode_times, + **self.opener_options, + } + + self.ds = self.opener(self.src_path, **opener_options) + self.input = get_variable( + self.ds, + self.variable, + sel=self.sel, + ) + super().__attrs_post_init__() + + def close(self): + """Close xarray dataset.""" + self.ds.close() + + def __exit__(self, exc_type, exc_value, traceback): + """Support using with Context Managers.""" + self.close() + + +def fs_open_dataset( # noqa: C901 + src_path: str, + group: str | None = None, + decode_times: bool = True, + decode_coords: str = "all", + **kwargs, +) -> xarray.Dataset: + """Open Xarray dataset with fsspec. + + Args: + src_path (str): dataset path. + group (Optional, str): path to the netCDF/Zarr group in the given file to open given as a str. + decode_times (bool): If True, decode times encoded in the standard NetCDF datetime format into datetime objects. Otherwise, leave them encoded as numbers. + + Returns: + xarray.Dataset + + """ + import fsspec # noqa + + try: + import h5netcdf + except ImportError: # pragma: nocover + h5netcdf = None # type: ignore + + parsed = urlparse(src_path) + protocol = parsed.scheme or "file" + + # Arguments for xarray.open_dataset + # Default args + xr_open_args: dict[str, Any] = { + "decode_coords": decode_coords, + "decode_times": decode_times, + } + + # Argument if we're opening a datatree + if group is not None: + xr_open_args["group"] = group + + # NetCDF arguments + if any(src_path.lower().endswith(ext) for ext in [".nc", ".nc4"]): + assert ( + h5netcdf is not None + ), "'h5netcdf' must be installed to read NetCDF dataset" + + xr_open_args.update( + { + "engine": "h5netcdf", + "lock": False, + } + ) + fs = fsspec.filesystem(protocol, **kwargs) + ds = xarray.open_dataset(fs.open(src_path), **xr_open_args) + + # Fallback to Zarr + else: + store = zarr.storage.FsspecStore.from_url( + src_path, storage_options={"asynchronous": True, **kwargs} + ) + ds = xarray.open_zarr(store, **xr_open_args) + + return ds + + +# Compat +xarray_open_dataset = fs_open_dataset + + +@attr.s +class FsReader(Reader): + """Reader with fs_open_dataset opener""" + + opener: Callable[..., xarray.Dataset] = attr.ib(default=fs_open_dataset) diff --git a/src/titiler/xarray/titiler/xarray/main.py b/src/titiler/xarray/titiler/xarray/main.py new file mode 100644 index 000000000..8984dd951 --- /dev/null +++ b/src/titiler/xarray/titiler/xarray/main.py @@ -0,0 +1,487 @@ +"""titiler.xarray application.""" + +import json +import logging +import os +from logging import config as log_config +from typing import Annotated, Literal + +import jinja2 +import rasterio +import xarray +import zarr +from fastapi import Depends, FastAPI, HTTPException, Query, Security +from fastapi.security.api_key import APIKeyQuery +from pydantic import field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict +from starlette.middleware.cors import CORSMiddleware +from starlette.requests import Request +from starlette.templating import Jinja2Templates +from starlette_cramjam.middleware import CompressionMiddleware + +from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers +from titiler.core.factory import AlgorithmFactory, ColorMapFactory, TMSFactory +from titiler.core.middleware import ( + CacheControlMiddleware, + LoggerMiddleware, + TotalTimeMiddleware, +) +from titiler.core.models.OGC import Conformance, Landing +from titiler.core.resources.enums import MediaType +from titiler.core.utils import accept_media_type, create_html_response, update_openapi +from titiler.xarray import __version__ as titiler_version +from titiler.xarray.extensions import DatasetMetadataExtension, ValidateExtension +from titiler.xarray.factory import TilerFactory + +logging.getLogger("rasterio.session").setLevel(logging.ERROR) +logging.getLogger("rio-tiler").setLevel(logging.ERROR) + + +class ApiSettings(BaseSettings): + """FASTAPI application settings.""" + + name: str = "TiTiler with support of Zarr dataset" + description: str = """A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL/Xarray for Zarr dataset. + +--- + +**Documentation**: https://developmentseed.org/titiler/ + +**Source Code**: https://github.com/developmentseed/titiler + +--- + """ + + cors_origins: str = "*" + cors_allow_methods: str = "GET" + cachecontrol: str = "public, max-age=3600" + root_path: str = "" + debug: bool = False + + template_directory: str | None = None + + telemetry_enabled: bool = False + + # an API key required to access any endpoint, passed via the ?access_token= query parameter + global_access_token: str | None = None + + model_config = SettingsConfigDict( + env_prefix="TITILER_XARRAY_API_", env_file=".env", extra="ignore" + ) + + @field_validator("cors_origins") + def parse_cors_origin(cls, v): + """Parse CORS origins.""" + return [origin.strip() for origin in v.split(",")] + + @field_validator("cors_allow_methods") + def parse_cors_allow_methods(cls, v): + """Parse CORS allowed methods.""" + return [method.strip().upper() for method in v.split(",")] + + +api_settings = ApiSettings() + +templates_location: list[jinja2.PackageLoader | jinja2.FileSystemLoader] = ( + [jinja2.FileSystemLoader(api_settings.template_directory)] + if api_settings.template_directory + else [] +) + +# default template directory +templates_location.extend( + [ + jinja2.FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")), + jinja2.PackageLoader("titiler.core", "templates"), + ] +) + +jinja2_env = jinja2.Environment( + autoescape=jinja2.select_autoescape(["html", "xml"]), + loader=jinja2.ChoiceLoader(templates_location), +) +titiler_templates = Jinja2Templates(env=jinja2_env) + +app_dependencies = [] +if api_settings.global_access_token: + ############################################################################### + # Setup a global API access key, if configured + api_key_query = APIKeyQuery(name="access_token", auto_error=False) + + def validate_access_token(access_token: str = Security(api_key_query)): + """Validates API key access token, set as the `api_settings.global_access_token` value. + Returns True if no access token is required, or if the access token is valid. + Raises an HTTPException (401) if the access token is required but invalid/missing. + """ + if not access_token: + raise HTTPException(status_code=401, detail="Missing `access_token`") + + # if access_token == `token` then OK + if access_token != api_settings.global_access_token: + raise HTTPException(status_code=401, detail="Invalid `access_token`") + + return True + + app_dependencies.append(Depends(validate_access_token)) + + +app = FastAPI( + title=api_settings.name, + openapi_url="/api", + docs_url="/api.html", + description=api_settings.description, + version=titiler_version, + root_path=api_settings.root_path, + dependencies=app_dependencies, +) + +update_openapi(app) + +TITILER_CONFORMS_TO = { + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/landing-page", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/oas30", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/html", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json", +} + + +md = TilerFactory( + extensions=[ + DatasetMetadataExtension(), + ValidateExtension(), + ], + enable_telemetry=api_settings.telemetry_enabled, + templates=titiler_templates, +) +app.include_router(md.router, tags=["Multi Dimensional"]) + +TITILER_CONFORMS_TO.update(md.conforms_to) + +# TileMatrixSets endpoints +tms = TMSFactory(templates=titiler_templates) +app.include_router(tms.router, tags=["Tiling Schemes"]) +TITILER_CONFORMS_TO.update(tms.conforms_to) + +############################################################################### +# Algorithms endpoints +algorithms = AlgorithmFactory(templates=titiler_templates) +app.include_router( + algorithms.router, + tags=["Algorithms"], +) +TITILER_CONFORMS_TO.update(algorithms.conforms_to) + +# Colormaps endpoints +cmaps = ColorMapFactory(templates=titiler_templates) +app.include_router( + cmaps.router, + tags=["ColorMaps"], +) +TITILER_CONFORMS_TO.update(cmaps.conforms_to) + +add_exception_handlers(app, DEFAULT_STATUS_CODES) + +# Set all CORS enabled origins +if api_settings.cors_origins: + app.add_middleware( + CORSMiddleware, + allow_origins=api_settings.cors_origins, + allow_credentials=True, + allow_methods=api_settings.cors_allow_methods, + allow_headers=["*"], + ) + +app.add_middleware( + CompressionMiddleware, + minimum_size=0, + exclude_mediatype={ + "image/jpeg", + "image/jpg", + "image/png", + "image/jp2", + "image/webp", + }, + compression_level=6, +) + +app.add_middleware( + CacheControlMiddleware, + cachecontrol=api_settings.cachecontrol, + exclude_path={r"/healthz"}, +) + + +if api_settings.debug: + app.add_middleware(LoggerMiddleware) + app.add_middleware(TotalTimeMiddleware) + + log_config.dictConfig( + { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "detailed": { + "format": "%(asctime)s - %(levelname)s - %(name)s - %(message)s" + }, + "request": { + "format": ( + "%(asctime)s - %(levelname)s - %(name)s - %(message)s " + + json.dumps( + { + k: f"%({k})s" + for k in [ + "http.method", + "http.referer", + "http.request.header.origin", + "http.route", + "http.target", + "http.request.header.content-length", + "http.request.header.accept-encoding", + "http.request.header.origin", + "titiler.path_params", + "titiler.query_params", + ] + } + ) + ), + }, + }, + "handlers": { + "console_detailed": { + "class": "logging.StreamHandler", + "level": "WARNING", + "formatter": "detailed", + "stream": "ext://sys.stdout", + }, + "console_request": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "request", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "titiler": { + "level": "INFO", + "handlers": ["console_detailed"], + "propagate": False, + }, + "titiler.requests": { + "level": "INFO", + "handlers": ["console_request"], + "propagate": False, + }, + }, + } + ) + + +@app.get( + "/healthz", + description="Health Check.", + summary="Health Check.", + operation_id="healthCheck", + tags=["Health Check"], +) +def application_health_check(): + """Health check.""" + return { + "versions": { + "titiler": titiler_version, + "rasterio": rasterio.__version__, + "gdal": rasterio.__gdal_version__, + "proj": rasterio.__proj_version__, + "geos": rasterio.__geos_version__, + "xarray": xarray.__version__, + "zarr": zarr.__version__, + } + } + + +@app.get( + "/", + response_model=Landing, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "text/html": {}, + "application/json": {}, + } + }, + }, + tags=["OGC Common"], +) +def landing( + request: Request, + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, +): + """TiTiler landing page.""" + data = { + "title": "TiTiler + Xarray", + "description": "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL/Xarray for Zarr dataset.", + "links": [ + { + "title": "Landing page", + "href": str(request.url_for("landing")), + "type": "text/html", + "rel": "self", + }, + { + "title": "The API definition (JSON)", + "href": str(request.url_for("openapi")), + "type": "application/vnd.oai.openapi+json;version=3.0", + "rel": "service-desc", + }, + { + "title": "The API documentation", + "href": str(request.url_for("swagger_ui_html")), + "type": "text/html", + "rel": "service-doc", + }, + { + "title": "Conformance Declaration", + "href": str(request.url_for("conformance")), + "type": "text/html", + "rel": "http://www.opengis.net/def/rel/ogc/1.0/conformance", + }, + { + "title": "List of Available TileMatrixSets", + "href": str(request.url_for("tilematrixsets")), + "type": "application/json", + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes", + }, + { + "title": "List of Available Algorithms", + "href": str(request.url_for("available_algorithms")), + "type": "application/json", + "rel": "data", + }, + { + "title": "List of Available ColorMaps", + "href": str(request.url_for("available_colormaps")), + "type": "application/json", + "rel": "data", + }, + { + "title": "TiTiler Documentation (external link)", + "href": "https://developmentseed.org/titiler/", + "type": "text/html", + "rel": "doc", + }, + { + "title": "TiTiler.Xarray source code (external link)", + "href": "https://github.com/developmentseed/titiler/tree/main/src/titiler/xarray", + "type": "text/html", + "rel": "doc", + }, + ], + } + + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) + + if output_type == MediaType.html: + return create_html_response( + request, + data, + title="TiTiler Xarray", + template_name="landing", + templates=titiler_templates, + ) + + return data + + +@app.get( + "/conformance", + response_model=Conformance, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "text/html": {}, + "application/json": {}, + } + }, + }, + tags=["OGC Common"], +) +def conformance( + request: Request, + f: Annotated[ + Literal["html", "json"] | None, + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, +): + """Conformance classes. + + Called with `GET /conformance`. + + Returns: + Conformance classes which the server conforms to. + + """ + data = {"conformsTo": sorted(TITILER_CONFORMS_TO)} + + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = ( + accept_media_type(request.headers.get("accept", ""), accepted_media) + or MediaType.json + ) + + if output_type == MediaType.html: + return create_html_response( + request, + data, + title="Conformance", + template_name="conformance", + templates=titiler_templates, + ) + + return data + + +if api_settings.telemetry_enabled: + from opentelemetry import trace + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor + from opentelemetry.instrumentation.logging import LoggingInstrumentor + from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION, Resource + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + LoggingInstrumentor().instrument(set_logging_format=True) + FastAPIInstrumentor.instrument_app(app) + + resource = Resource.create( + { + SERVICE_NAME: "titiler", + SERVICE_VERSION: titiler_version, + } + ) + + provider = TracerProvider(resource=resource) + + # uses the OTEL_EXPORTER_OTLP_ENDPOINT env var + processor = BatchSpanProcessor(OTLPSpanExporter()) + provider.add_span_processor(processor) + + trace.set_tracer_provider(provider) diff --git a/src/titiler/xarray/titiler/xarray/py.typed b/src/titiler/xarray/titiler/xarray/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/titiler/xarray/titiler/xarray/templates/conformance.html b/src/titiler/xarray/titiler/xarray/templates/conformance.html new file mode 100644 index 000000000..0471b3683 --- /dev/null +++ b/src/titiler/xarray/titiler/xarray/templates/conformance.html @@ -0,0 +1,32 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

    {{ template.title }}

    + +

    This API implements the conformance classes from standards and community specifications that are listed below.

    + +

    Links

    +
      +{% for url in response.conformsTo %} +
    • {{ url }}
    • +{% endfor %} +
    + +{% include "footer.html" %} diff --git a/src/titiler/xarray/titiler/xarray/templates/header.html b/src/titiler/xarray/titiler/xarray/templates/header.html new file mode 100644 index 000000000..a60001b62 --- /dev/null +++ b/src/titiler/xarray/titiler/xarray/templates/header.html @@ -0,0 +1,44 @@ + + + + {{ template.title }} + + + + + + + + + + + + +
    +
    diff --git a/src/titiler/xarray/titiler/xarray/templates/landing.html b/src/titiler/xarray/titiler/xarray/templates/landing.html new file mode 100644 index 000000000..f9696c6b8 --- /dev/null +++ b/src/titiler/xarray/titiler/xarray/templates/landing.html @@ -0,0 +1,49 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

    {{ response.title }}

    +

    + {{ response.description }} +

    + +
    +   __     __   __     __ __                                                                                
    +  |  \   |  \ |  \   |  \  \                                                                               
    + _| ▓▓_   \▓▓_| ▓▓_   \▓▓ ▓▓ ______   ______         __    __  ______   ______   ______   ______  __    __ 
    +|   ▓▓ \ |  \   ▓▓ \ |  \ ▓▓/      \ /      \       |  \  /  \|      \ /      \ /      \ |      \|  \  |  \
    + \▓▓▓▓▓▓ | ▓▓\▓▓▓▓▓▓ | ▓▓ ▓▓  ▓▓▓▓▓▓\  ▓▓▓▓▓▓\      \\▓▓\/  ▓▓ \▓▓▓▓▓▓\  ▓▓▓▓▓▓\  ▓▓▓▓▓▓\ \▓▓▓▓▓▓\ ▓▓  | ▓▓
    +  | ▓▓ __| ▓▓ | ▓▓ __| ▓▓ ▓▓ ▓▓    ▓▓ ▓▓              >▓▓  ▓▓ /      ▓▓ ▓▓   \▓▓ ▓▓   \▓▓/      ▓▓ ▓▓  | ▓▓
    +  | ▓▓|  \ ▓▓ | ▓▓|  \ ▓▓ ▓▓ ▓▓▓▓▓▓▓▓ ▓▓      ▓▓▓▓▓▓ /  ▓▓▓▓\|  ▓▓▓▓▓▓▓ ▓▓     | ▓▓     |  ▓▓▓▓▓▓▓ ▓▓__/ ▓▓
    +   \▓▓  ▓▓ ▓▓  \▓▓  ▓▓ ▓▓ ▓▓\▓▓     \ ▓▓            |  ▓▓ \▓▓\\▓▓    ▓▓ ▓▓     | ▓▓      \▓▓    ▓▓\▓▓    ▓▓
    +    \▓▓▓▓ \▓▓   \▓▓▓▓ \▓▓\▓▓ \▓▓▓▓▓▓▓\▓▓             \▓▓   \▓▓ \▓▓▓▓▓▓▓\▓▓      \▓▓       \▓▓▓▓▓▓▓_\▓▓▓▓▓▓▓
    +                                                                                                 |  \__| ▓▓
    +                                                                                                  \▓▓    ▓▓
    +                                                                                                   \▓▓▓▓▓▓ 
    +
    +  
    + +

    Links

    + + +{% include "footer.html" %} diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..9a1347029 --- /dev/null +++ b/uv.lock @@ -0,0 +1,6204 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[manifest] +members = [ + "titiler", + "titiler-application", + "titiler-core", + "titiler-extensions", + "titiler-mosaic", + "titiler-xarray", +] + +[[package]] +name = "affine" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/98/d2f0bb06385069e799fc7d2870d9e078cfa0fa396dc8a2b81227d0da08b9/affine-2.4.0.tar.gz", hash = "sha256:a24d818d6a836c131976d22f8c27b8d3ca32d0af64c1d8d29deb7bafa4da1eea", size = 17132, upload-time = "2023-01-19T23:44:30.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/f7/85273299ab57117850cc0a936c64151171fac4da49bc6fba0dad984a7c5f/affine-2.4.0-py3-none-any.whl", hash = "sha256:8a3df80e2b2378aef598a83c1392efd47967afec4242021a0b06b4c7cbc61a92", size = 15662, upload-time = "2023-01-19T23:44:28.833Z" }, +] + +[[package]] +name = "aiobotocore" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aioitertools" }, + { name = "botocore" }, + { name = "jmespath" }, + { name = "multidict" }, + { name = "python-dateutil" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/7d593e50d481b649c99a407c8249f9cf6437840a3adc4ecc9127f9a843d2/aiobotocore-3.2.1.tar.gz", hash = "sha256:59b1c1f59860cb10b2e5096edcc87a88842bee301969bd76a3ca0b1c4c30e6d3", size = 122788, upload-time = "2026-03-04T22:30:43.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/78/79aa8169408996f5a71150abdea2e5e0f364df250c9e54ac385f115c7436/aiobotocore-3.2.1-py3-none-any.whl", hash = "sha256:68b7474af3e7124666b8e191805db5a7255d14e6187e0472481c845b6062e42e", size = 87737, upload-time = "2026-03-04T22:30:41.594Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aioitertools" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "asgiref" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "async-lru" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8a/ca724066c32a53fa75f59e0f21aa822fdaa8a0dffa112d223634e3caabf9/async_lru-2.2.0.tar.gz", hash = "sha256:80abae2a237dbc6c60861d621619af39f0d920aea306de34cb992c879e01370c", size = 14654, upload-time = "2026-02-20T19:11:43.848Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/5c/af990f019b8dd11c5492a6371fe74a5b0276357370030b67254a87329944/async_lru-2.2.0-py3-none-any.whl", hash = "sha256:e2c1cf731eba202b59c5feedaef14ffd9d02ad0037fcda64938699f2c380eafe", size = 7890, upload-time = "2026-02-20T19:11:42.273Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "black" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "boto3" +version = "1.42.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/15/356d38280ce3fce37a8e2b44e2ead81240d933f64411e86415a2ed4c0bd5/boto3-1.42.61.tar.gz", hash = "sha256:117ebfc597c95bfb64c6d37ba77bd1c2a97a1885c1dcac2a8be1a14e2139a76d", size = 112750, upload-time = "2026-03-04T20:30:53.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/d7/a2fa875cb7c5d6b5c5cf6fc181343708c8dc6cafae3e6964ed486ae21bea/boto3-1.42.61-py3-none-any.whl", hash = "sha256:156efcc298a33206be6dfd220815c64aa8b09424017534cabe717636961fc306", size = 140555, upload-time = "2026-03-04T20:30:51.17Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/6a/27836dde004717c496f69f4fe28fa2f3f3762d04859a9292681944a45a36/botocore-1.42.61.tar.gz", hash = "sha256:702d6011ace2b5b652a0dbb45053d4d9f79da2c5b184463042434e1754bdd601", size = 14954743, upload-time = "2026-03-04T20:30:41.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/46/98a01139f318b7a2f0ad1d1e3be2a028d13aeb7e05aaa340a27cdc47fdf0/botocore-1.42.61-py3-none-any.whl", hash = "sha256:476059beb3f462042742950cf195d26bc313461a77189c16e37e205b0a924b26", size = 14627717, upload-time = "2026-03-04T20:30:37.503Z" }, +] + +[[package]] +name = "branca" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/14/9d409124bda3f4ab7af3802aba07181d1fd56aa96cc4b999faea6a27a0d2/branca-0.8.2.tar.gz", hash = "sha256:e5040f4c286e973658c27de9225c1a5a7356dd0702a7c8d84c0f0dfbde388fe7", size = 27890, upload-time = "2025-10-06T10:28:20.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/50/fc9680058e63161f2f63165b84c957a0df1415431104c408e8104a3a18ef/branca-0.8.2-py3-none-any.whl", hash = "sha256:2ebaef3983e3312733c1ae2b793b0a8ba3e1c4edeb7598e10328505280cf2f7c", size = 26193, upload-time = "2025-10-06T10:28:19.255Z" }, +] + +[[package]] +name = "brotlipy" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/91/bc79b88590e4f662bd40a55a2b6beb0f15da4726732efec5aa5a3763d856/brotlipy-0.7.0.tar.gz", hash = "sha256:36def0b859beaf21910157b4c33eb3b06d8ce459c942102f16988cca6ea164df", size = 413338, upload-time = "2017-05-30T08:20:34.637Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/98/446b36a3517eb1b3c70d7a32b0523b8946a90db71a95be01a05cf6dd9206/brotlipy-0.7.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:382971a641125323e90486244d6266ffb0e1f4dd920fbdcf508d2a19acc7c3b3", size = 390662, upload-time = "2021-09-02T17:51:34.63Z" }, + { url = "https://files.pythonhosted.org/packages/3c/58/998e70ed426a86f641d49c7a107eaf17de1f7e22d65b1e0b4e33deb172b9/brotlipy-0.7.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:82f61506d001e626ec3a1ac8a69df11eb3555a4878599befcb672c8178befac8", size = 995723, upload-time = "2021-09-02T17:51:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1b/ca5c77fe4a5b99cef24767b48254ea50ffad813407c74aa06d93089d1014/brotlipy-0.7.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:7ff18e42f51ebc9d9d77a0db33f99ad95f01dd431e4491f0eca519b90e9415a9", size = 1094088, upload-time = "2021-09-02T17:51:41.054Z" }, + { url = "https://files.pythonhosted.org/packages/a1/71/844dc49400ff6c82b169454c03376d3cf68b01543b7e99bd502a9295a5ba/brotlipy-0.7.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:8ef230ca9e168ce2b7dc173a48a0cc3d78bcdf0bd0ea7743472a317041a4768e", size = 995726, upload-time = "2021-09-02T17:51:43.446Z" }, + { url = "https://files.pythonhosted.org/packages/97/2e/372bc34bd5741212ac884b5d88f284ba7369f848bccb9a80b458a9224576/brotlipy-0.7.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:b7cf5bb69e767a59acc3da0d199d4b5d0c9fed7bef3ffa3efa80c6f39095686b", size = 1094094, upload-time = "2021-09-02T17:51:45.659Z" }, + { url = "https://files.pythonhosted.org/packages/7d/19/416936acb7efa79164be7f09a95e26ba03649dcb7799254395ea2f140635/brotlipy-0.7.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:e5c549ae5928dda952463196180445c24d6fad2d73cb13bd118293aced31b771", size = 1043867, upload-time = "2021-09-02T17:51:48.276Z" }, + { url = "https://files.pythonhosted.org/packages/1d/db/94a242376511ce71e9758169dd11ca9cf5e8f675e12dede8fbcea644c089/brotlipy-0.7.0-cp35-abi3-win32.whl", hash = "sha256:79ab3bca8dd12c17e092273484f2ac48b906de2b4828dcdf6a7d520f99646ab3", size = 350080, upload-time = "2021-09-02T17:51:50.096Z" }, + { url = "https://files.pythonhosted.org/packages/03/53/41f17238db696c4c95019174ca5bfd2396ef379c7918ae4f6ff82529ee92/brotlipy-0.7.0-cp35-abi3-win_amd64.whl", hash = "sha256:ac1d66c9774ee62e762750e399a0c95e93b180e96179b645f28b162b55ae8adc", size = 376301, upload-time = "2021-09-02T17:51:51.987Z" }, +] + +[[package]] +name = "cachetools" +version = "7.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/5c/3b882b82e9af737906539a2eafb62f96a229f1fa80255bede0c7b554cbc4/cachetools-7.0.3.tar.gz", hash = "sha256:8c246313b95849964e54a909c03b327a87ab0428b068fac10da7b105ca275ef6", size = 37187, upload-time = "2026-03-05T21:00:57.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4a/573185481c50a8841331f54ddae44e4a3469c46aa0b397731c53a004369a/cachetools-7.0.3-py3-none-any.whl", hash = "sha256:c128ffca156eef344c25fcd08a96a5952803786fa33097f5f2d49edf76f79d53", size = 13907, upload-time = "2026-03-05T21:00:56.486Z" }, +] + +[[package]] +name = "cairocffi" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" }, +] + +[[package]] +name = "cairosvg" +version = "2.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cairocffi" }, + { name = "cssselect2" }, + { name = "defusedxml" }, + { name = "pillow" }, + { name = "tinycss2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/5106168bd43d7cd8b7cc2a2ee465b385f14b63f4c092bb89eee2d48c8e67/cairosvg-2.8.2.tar.gz", hash = "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", size = 8398590, upload-time = "2025-05-15T06:56:32.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/48/816bd4aaae93dbf9e408c58598bc32f4a8c65f4b86ab560864cb3ee60adb/cairosvg-2.8.2-py3-none-any.whl", hash = "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5", size = 45773, upload-time = "2025-05-15T06:56:28.552Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/9e/bcec3b22c64ecec47d39bf5167c2613efd41898c019dccd4183f6aa5d6a7/charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", size = 279531, upload-time = "2026-03-06T06:00:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/58/12/81fd25f7e7078ab5d1eedbb0fac44be4904ae3370a3bf4533c8f2d159acd/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", size = 188006, upload-time = "2026-03-06T06:00:53.8Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6e/f2d30e8c27c1b0736a6520311982cf5286cfc7f6cac77d7bc1325e3a23f2/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", size = 205085, upload-time = "2026-03-06T06:00:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/d0/90/d12cefcb53b5931e2cf792a33718d7126efb116a320eaa0742c7059a95e4/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", size = 200545, upload-time = "2026-03-06T06:00:56.532Z" }, + { url = "https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", size = 193863, upload-time = "2026-03-06T06:00:57.823Z" }, + { url = "https://files.pythonhosted.org/packages/25/4b/f212119c18a6320a9d4a730d1b4057875cdeabf21b3614f76549042ef8a8/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", size = 181827, upload-time = "2026-03-06T06:00:59.323Z" }, + { url = "https://files.pythonhosted.org/packages/74/00/b26158e48b425a202a92965f8069e8a63d9af1481dfa206825d7f74d2a3c/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", size = 191085, upload-time = "2026-03-06T06:01:00.546Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1c1737bf6fd40335fe53d28fe49afd99ee4143cc57a845e99635ce0b9b6d/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", size = 190688, upload-time = "2026-03-06T06:01:02.479Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3d/abb5c22dc2ef493cd56522f811246a63c5427c08f3e3e50ab663de27fcf4/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", size = 183077, upload-time = "2026-03-06T06:01:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/44/33/5298ad4d419a58e25b3508e87f2758d1442ff00c2471f8e0403dab8edad5/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", size = 206706, upload-time = "2026-03-06T06:01:05.773Z" }, + { url = "https://files.pythonhosted.org/packages/7b/17/51e7895ac0f87c3b91d276a449ef09f5532a7529818f59646d7a55089432/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", size = 191665, upload-time = "2026-03-06T06:01:07.473Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/cce9adf1883e98906dbae380d769b4852bb0fa0004bc7d7a2243418d3ea8/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", size = 201950, upload-time = "2026-03-06T06:01:08.973Z" }, + { url = "https://files.pythonhosted.org/packages/08/ca/bce99cd5c397a52919e2769d126723f27a4c037130374c051c00470bcd38/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", size = 195830, upload-time = "2026-03-06T06:01:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/2e3d023a06911f1281f97b8f036edc9872167036ca6f55cc874a0be6c12c/charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", size = 132029, upload-time = "2026-03-06T06:01:11.706Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/a853b73d386521fd44b7f67ded6b17b7b2367067d9106a5c4b44f9a34274/charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", size = 142404, upload-time = "2026-03-06T06:01:12.865Z" }, + { url = "https://files.pythonhosted.org/packages/b4/10/dba36f76b71c38e9d391abe0fd8a5b818790e053c431adecfc98c35cd2a9/charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", size = 132796, upload-time = "2026-03-06T06:01:14.106Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, + { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, + { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, + { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, + { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "cligj" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/0d/837dbd5d8430fd0f01ed72c4cfb2f548180f4c68c635df84ce87956cff32/cligj-0.7.2.tar.gz", hash = "sha256:a4bc13d623356b373c2c27c53dbd9c68cae5d526270bfa71f6c6fa69669c6b27", size = 9803, upload-time = "2021-05-28T21:23:27.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/86/43fa9f15c5b9fb6e82620428827cd3c284aa933431405d1bcf5231ae3d3e/cligj-0.7.2-py3-none-any.whl", hash = "sha256:c1ca117dbce1fe20a5809dc96f01e1c2840f6dcc939b3ddbb1111bf330ba82df", size = 7069, upload-time = "2021-05-28T21:23:26.877Z" }, +] + +[[package]] +name = "cogeo-mosaic" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cachetools" }, + { name = "click" }, + { name = "click-plugins" }, + { name = "cligj" }, + { name = "httpx" }, + { name = "morecantile" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "rasterio", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "rasterio", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "rio-tiler" }, + { name = "shapely" }, + { name = "supermorecado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/48/614f53add12c39c86ac392a365f74c13bcea31d8e8cac4c2ccdb1d9f5cc3/cogeo_mosaic-9.1.0.tar.gz", hash = "sha256:f2102e68d3b1e7ee987ea85137ac198bf2f49d83739e4251a30265f4d5436828", size = 27951, upload-time = "2026-02-02T23:11:36.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/1a/be72e2cb8e1f14611d968da99c3689049637de367ed8f8c01132a2c3861e/cogeo_mosaic-9.1.0-py3-none-any.whl", hash = "sha256:b246182f12ae25d562ebc86987d2736617216e94a6a5d202460220faf7afc0d7", size = 39908, upload-time = "2026-02-02T23:11:38.05Z" }, +] + +[[package]] +name = "color-operations" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/d5/8daa1179809f0d8eab39bd83ce8131e84691eb6ba55f19b7b365a822fea3/color_operations-0.2.0.tar.gz", hash = "sha256:f1bff5cff5992ec7d240f1979320a981f2e9f77d983e9298291e02f3ffaac9bf", size = 18042, upload-time = "2025-03-27T08:42:14.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/18/ed195e388f55ef46b89cee994a5dc7c36a6c76fd3c40ed1960b86dcba4ba/color_operations-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6fb9b74b9dc33832d08afc8f71ec4161531f48e8bf105d0412e9a718904c5369", size = 86416, upload-time = "2025-03-27T09:18:16.848Z" }, + { url = "https://files.pythonhosted.org/packages/32/60/a9955ab7077309241d47f0b88d43b993abd49b753ed69449b8f2ced7c30c/color_operations-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05838eee03df5304e014de76bd3ff19974964fc57dad8ce52cf56a9a62f5d572", size = 50869, upload-time = "2025-03-27T09:18:17.761Z" }, + { url = "https://files.pythonhosted.org/packages/b5/79/3fb8aee10ceb0278bd256eee35dd9fd9607370e4bb75a75a56173fb04394/color_operations-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5222cf35ca089637d3424eb42a0c9bfa25aa91dbf771759f6c8003b09b5134cc", size = 49243, upload-time = "2025-03-27T09:18:18.814Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b5/1783a6834d10960a13c18b6a34f27597052510142ce89125662276184f99/color_operations-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b807de37a40ee1d6fa91d122b0afe1df5f17ee60b9ef1bd38e8c134ffb3070d", size = 189098, upload-time = "2025-03-27T09:18:20.093Z" }, + { url = "https://files.pythonhosted.org/packages/91/d8/c33469d020f135414a5cd936bd31f4d3d2e3db557df94847ed59cf99a422/color_operations-0.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:780685c51e103f378c7bdffbafdd3e24f89b6dcd64079b7d6b3fbab7a23a06bf", size = 195175, upload-time = "2025-03-27T09:18:21.577Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b6/1dbff90606551f6e802fbabb4e422d3261552a7d8b7a5f4e00f5727e7525/color_operations-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:676812cd90ef37e8ca214c376d0a43f223f2717bf37d0b513a4a57c2e1fcfc62", size = 133689, upload-time = "2025-03-27T09:18:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/50/69/e8c09a930c45cbdf6d3cf84e32a6e53a44a200c447bed8ddf94a75a3f372/color_operations-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:98a3348d1dab6c5fdd79a9eeb90cd81bf6f5bf6ca65a24414460d90be76c2c37", size = 86112, upload-time = "2025-03-27T09:18:23.824Z" }, + { url = "https://files.pythonhosted.org/packages/72/eb/d66611577d721318d5a70dcfcd8d26194cfa14e958bd14a02631c1e712f2/color_operations-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:55a10f40ca59505a260e0f8b1ee392a2c049314177d3858ae477e8cc5daff07d", size = 50735, upload-time = "2025-03-27T09:18:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/13dbcc9913967489851f0b7d1c8d27840abe86e02d6e2e133d16db16d0d5/color_operations-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3d791eee208f208da9428f38ec9cad80bae4fa55bcde2af1b6d7e939d4f298d7", size = 49066, upload-time = "2025-03-27T09:18:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/ab/47/b143e2f0ef04cb3e7e4b7236f8a572449ea7340860baf88374ed5ac4f358/color_operations-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4061484091fab17f9cd71620ee10ae5902ae643fddd18dc01f1ba85636d9a0e1", size = 198667, upload-time = "2025-03-27T09:18:27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f4/e754800604d6449d895d7118b346bc2f3c5176cb759934b245aae530138d/color_operations-0.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dfba9c174f3bbd425da388fab22a9670500711d0982e6f82e9999792542d3bf", size = 204259, upload-time = "2025-03-27T09:18:28.096Z" }, + { url = "https://files.pythonhosted.org/packages/56/45/fbd35c3ebb1a2d85339d70262739502f99447aab76220a7126adcb3722f9/color_operations-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c7fea5d7d0e7dd8d469e93e1bdd29c03afb63cebfcb02747104e482be85ea97", size = 133391, upload-time = "2025-03-27T09:18:29.26Z" }, + { url = "https://files.pythonhosted.org/packages/b4/93/fdd2e32eb1dd8929d36aabf8703adb9f438cfc79eb563b23140d0dd42475/color_operations-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fda310b57befc0aa3a02bf3863ff62adfedf7781ea8aab071887c5e82e5ab6c8", size = 84609, upload-time = "2025-03-27T09:18:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c4/abdcc64288c8249f5f312dd7b8ff0ccddc31ddf2d776e13796e3464dcc21/color_operations-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bf3218834d19e4d195885cb0fbf7b1f98db2f4fc6dd43ca5d035655d7ad3b6f7", size = 50084, upload-time = "2025-03-27T09:18:32.294Z" }, + { url = "https://files.pythonhosted.org/packages/bb/75/5ce7c78e44f0660713e0b398620baa87d4ef1d98ec8ae42da2153afaf8d0/color_operations-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cee7d7da762f04110f15615ebd894820db38ab2aa262a940178a3d41350d2a0d", size = 48205, upload-time = "2025-03-27T09:18:33.22Z" }, + { url = "https://files.pythonhosted.org/packages/28/54/eeffafffc815a8bb550d4ac1ae5a7bc86df0891e505b78c37a575ca35cb3/color_operations-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d2eb9dd747c081a801fc3b831bdf28f5115857934b00c4950c9ceecfb90d91f", size = 193013, upload-time = "2025-03-27T09:18:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/7f/87/835e83190dd00e2737162f9d66e5a1afcff2b6fb580b6545760749a2e0ec/color_operations-0.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fcca6e5593f05cf164d1a302c91c012acab2edf5a4d38c6cc0d4bc7b62388e7", size = 198605, upload-time = "2025-03-27T09:18:35.794Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/b1a27ad6490fd316a2e6ba4d05c8dd9f5d867414707d1cd18dcfdb3dbe1f/color_operations-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:317d11b425ab802e1c343d8d1356f538e102d6ca57e435b7386593c69f630ac5", size = 132514, upload-time = "2025-03-27T09:18:37.22Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cramjam" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/12/34bf6e840a79130dfd0da7badfb6f7810b8fcfd60e75b0539372667b41b6/cramjam-2.11.0.tar.gz", hash = "sha256:5c82500ed91605c2d9781380b378397012e25127e89d64f460fea6aeac4389b4", size = 99100, upload-time = "2025-07-27T21:25:07.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/89/8001f6a9b6b6e9fa69bec5319789083475d6f26d52aaea209d3ebf939284/cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:04cfa39118570e70e920a9b75c733299784b6d269733dbc791d9aaed6edd2615", size = 3559272, upload-time = "2025-07-27T21:22:01.988Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f3/001d00070ca92e5fbe6aacc768e455568b0cde46b0eb944561a4ea132300/cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:66a18f68506290349a256375d7aa2f645b9f7993c10fc4cc211db214e4e61d2b", size = 1861743, upload-time = "2025-07-27T21:22:03.754Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/041a3af01bf3f6158f120070f798546d4383b962b63c35cd91dcbf193e17/cramjam-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50e7d65533857736cd56f6509cf2c4866f28ad84dd15b5bdbf2f8a81e77fa28a", size = 1699631, upload-time = "2025-07-27T21:22:05.192Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/5358b238808abebd0c949c42635c3751204ca7cf82b29b984abe9f5e33c8/cramjam-2.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f71989668458fc327ac15396db28d92df22f8024bb12963929798b2729d2df5", size = 2025603, upload-time = "2025-07-27T21:22:06.726Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/19dba7c03a27408d8d11b5a7a4a7908459cfd4e6f375b73264dc66517bf6/cramjam-2.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee77ac543f1e2b22af1e8be3ae589f729491b6090582340aacd77d1d757d9569", size = 1766283, upload-time = "2025-07-27T21:22:08.568Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ad/40e4b3408501d886d082db465c33971655fe82573c535428e52ab905f4d0/cramjam-2.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad52784120e7e4d8a0b5b0517d185b8bf7f74f5e17272857ddc8951a628d9be1", size = 1854407, upload-time = "2025-07-27T21:22:10.518Z" }, + { url = "https://files.pythonhosted.org/packages/36/6e/c1b60ceb6d7ea6ff8b0bf197520aefe23f878bf2bfb0de65f2b0c2f82cd1/cramjam-2.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b86f8e6d9c1b3f9a75b2af870c93ceee0f1b827cd2507387540e053b35d7459", size = 2035793, upload-time = "2025-07-27T21:22:12.504Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ad/32a8d5f4b1e3717787945ec6d71bd1c6e6bccba4b7e903fc0d9d4e4b08c3/cramjam-2.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:320d61938950d95da2371b46c406ec433e7955fae9f396c8e1bf148ffc187d11", size = 2067499, upload-time = "2025-07-27T21:22:14.067Z" }, + { url = "https://files.pythonhosted.org/packages/ff/cd/3b5a662736ea62ff7fa4c4a10a85e050bfdaad375cc53dc80427e8afe41c/cramjam-2.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41eafc8c1653a35a5c7e75ad48138f9f60085cc05cd99d592e5298552d944e9f", size = 1981853, upload-time = "2025-07-27T21:22:15.908Z" }, + { url = "https://files.pythonhosted.org/packages/26/8e/1dbcfaaa7a702ee82ee683ec3a81656934dd7e04a7bc4ee854033686f98a/cramjam-2.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03a7316c6bf763dfa34279335b27702321da44c455a64de58112968c0818ec4a", size = 2034514, upload-time = "2025-07-27T21:22:17.352Z" }, + { url = "https://files.pythonhosted.org/packages/50/62/f11709bfdce74af79a88b410dcb76dedc97612166e759136931bf63cfd7b/cramjam-2.11.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:244c2ed8bd7ccbb294a2abe7ca6498db7e89d7eb5e744691dc511a7dc82e65ca", size = 2155343, upload-time = "2025-07-27T21:22:18.854Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/3b98b61841a5376d9a9b8468ae58753a8e6cf22be9534a0fa5af4d8621cc/cramjam-2.11.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:405f8790bad36ce0b4bbdb964ad51507bfc7942c78447f25cb828b870a1d86a0", size = 2169367, upload-time = "2025-07-27T21:22:20.389Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/bd5db5c49dbebc8b002f1c4983101b28d2e7fc9419753db1c31ec22b03ef/cramjam-2.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6b1b751a5411032b08fb3ac556160229ca01c6bbe4757bb3a9a40b951ebaac23", size = 2159334, upload-time = "2025-07-27T21:22:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/34/32/203c57acdb6eea727e7078b2219984e64ed4ad043c996ed56321301ba167/cramjam-2.11.0-cp311-cp311-win32.whl", hash = "sha256:5251585608778b9ac8effed544933df7ad85b4ba21ee9738b551f17798b215ac", size = 1605313, upload-time = "2025-07-27T21:22:24.126Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bd/102d6deb87a8524ac11cddcd31a7612b8f20bf9b473c3c645045e3b957c7/cramjam-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:dca88bc8b68ce6d35dafd8c4d5d59a238a56c43fa02b74c2ce5f9dfb0d1ccb46", size = 1710991, upload-time = "2025-07-27T21:22:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0d/7c84c913a5fae85b773a9dcf8874390f9d68ba0fcc6630efa7ff1541b950/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dba5c14b8b4f73ea1e65720f5a3fe4280c1d27761238378be8274135c60bbc6e", size = 3553368, upload-time = "2025-07-27T21:22:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cc/4f6d185d8a744776f53035e72831ff8eefc2354f46ab836f4bd3c4f6c138/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11eb40722b3fcf3e6890fba46c711bf60f8dc26360a24876c85e52d76c33b25b", size = 1860014, upload-time = "2025-07-27T21:22:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a8/626c76263085c6d5ded0e71823b411e9522bfc93ba6cc59855a5869296e7/cramjam-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aeb26e2898994b6e8319f19a4d37c481512acdcc6d30e1b5ecc9d8ec57e835cb", size = 1693512, upload-time = "2025-07-27T21:22:30.999Z" }, + { url = "https://files.pythonhosted.org/packages/e9/52/0851a16a62447532e30ba95a80e638926fdea869a34b4b5b9d0a020083ba/cramjam-2.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f8d82081ed7d8fe52c982bd1f06e4c7631a73fe1fb6d4b3b3f2404f87dc40fe", size = 2025285, upload-time = "2025-07-27T21:22:32.954Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/122e444f59dbc216451d8e3d8282c9665dc79eaf822f5f1470066be1b695/cramjam-2.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:092a3ec26e0a679305018380e4f652eae1b6dfe3fc3b154ee76aa6b92221a17c", size = 1761327, upload-time = "2025-07-27T21:22:34.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bc/3a0189aef1af2b29632c039c19a7a1b752bc21a4053582a5464183a0ad3d/cramjam-2.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:529d6d667c65fd105d10bd83d1cd3f9869f8fd6c66efac9415c1812281196a92", size = 1854075, upload-time = "2025-07-27T21:22:36.157Z" }, + { url = "https://files.pythonhosted.org/packages/2e/80/8a6343b13778ce52d94bb8d5365a30c3aa951276b1857201fe79d7e2ad25/cramjam-2.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:555eb9c90c450e0f76e27d9ff064e64a8b8c6478ab1a5594c91b7bc5c82fd9f0", size = 2032710, upload-time = "2025-07-27T21:22:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/cd1778a207c29eda10791e3dfa018b588001928086e179fc71254793c625/cramjam-2.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5edf4c9e32493035b514cf2ba0c969d81ccb31de63bd05490cc8bfe3b431674e", size = 2068353, upload-time = "2025-07-27T21:22:39.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f0/5c2a5cd5711032f3b191ca50cb786c17689b4a9255f9f768866e6c9f04d9/cramjam-2.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2fe41f48c4d58d923803383b0737f048918b5a0d10390de9628bb6272b107", size = 1978104, upload-time = "2025-07-27T21:22:41.106Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8b/b363a5fb2c3347504fe9a64f8d0f1e276844f0e532aa7162c061cd1ffee4/cramjam-2.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9ca14cf1cabdb0b77d606db1bb9e9ca593b1dbd421fcaf251ec9a5431ec449f3", size = 2030779, upload-time = "2025-07-27T21:22:42.969Z" }, + { url = "https://files.pythonhosted.org/packages/78/7b/d83dad46adb6c988a74361f81ad9c5c22642be53ad88616a19baedd06243/cramjam-2.11.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:309e95bf898829476bccf4fd2c358ec00e7ff73a12f95a3cdeeba4bb1d3683d5", size = 2155297, upload-time = "2025-07-27T21:22:44.6Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/60d9be4cb33d8740a4aa94c7513f2ef3c4eba4fd13536f086facbafade71/cramjam-2.11.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:86dca35d2f15ef22922411496c220f3c9e315d5512f316fe417461971cc1648d", size = 2169255, upload-time = "2025-07-27T21:22:46.534Z" }, + { url = "https://files.pythonhosted.org/packages/11/b0/4a595f01a243aec8ad272b160b161c44351190c35d98d7787919d962e9e5/cramjam-2.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:193c6488bd2f514cbc0bef5c18fad61a5f9c8d059dd56edf773b3b37f0e85496", size = 2155651, upload-time = "2025-07-27T21:22:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/38/47/7776659aaa677046b77f527106e53ddd47373416d8fcdb1e1a881ec5dc06/cramjam-2.11.0-cp312-cp312-win32.whl", hash = "sha256:514e2c008a8b4fa823122ca3ecab896eac41d9aa0f5fc881bd6264486c204e32", size = 1603568, upload-time = "2025-07-27T21:22:50.084Z" }, + { url = "https://files.pythonhosted.org/packages/75/b1/d53002729cfd94c5844ddfaf1233c86d29f2dbfc1b764a6562c41c044199/cramjam-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:53fed080476d5f6ad7505883ec5d1ec28ba36c2273db3b3e92d7224fe5e463db", size = 1709287, upload-time = "2025-07-27T21:22:51.534Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/406c5dc0f8e82385519d8c299c40fd6a56d97eca3fcd6f5da8dad48de75b/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2c289729cc1c04e88bafa48b51082fb462b0a57dbc96494eab2be9b14dca62af", size = 3553330, upload-time = "2025-07-27T21:22:53.124Z" }, + { url = "https://files.pythonhosted.org/packages/00/ad/4186884083d6e4125b285903e17841827ab0d6d0cffc86216d27ed91e91d/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:045201ee17147e36cf43d8ae2fa4b4836944ac672df5874579b81cf6d40f1a1f", size = 1859756, upload-time = "2025-07-27T21:22:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/54/01/91b485cf76a7efef638151e8a7d35784dae2c4ff221b1aec2c083e4b106d/cramjam-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:619cd195d74c9e1d2a3ad78d63451d35379c84bd851aec552811e30842e1c67a", size = 1693609, upload-time = "2025-07-27T21:22:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/d0c80d279b2976870fc7d10f15dcb90a3c10c06566c6964b37c152694974/cramjam-2.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6eb3ae5ab72edb2ed68bdc0f5710f0a6cad7fd778a610ec2c31ee15e32d3921e", size = 2024912, upload-time = "2025-07-27T21:22:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/d6/70/88f2a5cb904281ed5d3c111b8f7d5366639817a5470f059bcd26833fc870/cramjam-2.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7da3f4b19e3078f9635f132d31b0a8196accb2576e3213ddd7a77f93317c20", size = 1760715, upload-time = "2025-07-27T21:22:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/cf5b02081132537d28964fb385fcef9ed9f8a017dd7d8c59d317e53ba50d/cramjam-2.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57286b289cd557ac76c24479d8ecfb6c3d5b854cce54ccc7671f9a2f5e2a2708", size = 1853782, upload-time = "2025-07-27T21:23:01.07Z" }, + { url = "https://files.pythonhosted.org/packages/57/27/63525087ed40a53d1867021b9c4858b80cc86274ffe7225deed067d88d92/cramjam-2.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28952fbbf8b32c0cb7fa4be9bcccfca734bf0d0989f4b509dc7f2f70ba79ae06", size = 2032354, upload-time = "2025-07-27T21:23:03.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ef/dbba082c6ebfb6410da4dd39a64e654d7194fcfd4567f85991a83fa4ec32/cramjam-2.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78ed2e4099812a438b545dfbca1928ec825e743cd253bc820372d6ef8c3adff4", size = 2068007, upload-time = "2025-07-27T21:23:04.526Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/d902b9358a46a086938feae83b2251720e030f06e46006f4c1fc0ac9da20/cramjam-2.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9aecd5c3845d415bd6c9957c93de8d93097e269137c2ecb0e5a5256374bdc8", size = 1977485, upload-time = "2025-07-27T21:23:06.058Z" }, + { url = "https://files.pythonhosted.org/packages/e8/03/982f54553244b0afcbdb2ad2065d460f0ab05a72a96896a969a1ca136a1e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:362fcf4d6f5e1242a4540812455f5a594949190f6fbc04f2ffbfd7ae0266d788", size = 2030447, upload-time = "2025-07-27T21:23:07.679Z" }, + { url = "https://files.pythonhosted.org/packages/74/5f/748e54cdb665ec098ec519e23caacc65fc5ae58718183b071e33fc1c45b4/cramjam-2.11.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:13240b3dea41b1174456cb9426843b085dc1a2bdcecd9ee2d8f65ac5703374b0", size = 2154949, upload-time = "2025-07-27T21:23:09.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/81/c4e6cb06ed69db0dc81f9a8b1dc74995ebd4351e7a1877143f7031ff2700/cramjam-2.11.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:c54eed83726269594b9086d827decc7d2015696e31b99bf9b69b12d9063584fe", size = 2168925, upload-time = "2025-07-27T21:23:10.976Z" }, + { url = "https://files.pythonhosted.org/packages/13/5b/966365523ce8290a08e163e3b489626c5adacdff2b3da9da1b0823dfb14e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f8195006fdd0fc0a85b19df3d64a3ef8a240e483ae1dfc7ac6a4316019eb5df2", size = 2154950, upload-time = "2025-07-27T21:23:12.514Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7d/7f8eb5c534b72b32c6eb79d74585bfee44a9a5647a14040bb65c31c2572d/cramjam-2.11.0-cp313-cp313-win32.whl", hash = "sha256:ccf30e3fe6d770a803dcdf3bb863fa44ba5dc2664d4610ba2746a3c73599f2e4", size = 1603199, upload-time = "2025-07-27T21:23:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/37/05/47b5e0bf7c41a3b1cdd3b7c2147f880c93226a6bef1f5d85183040cbdece/cramjam-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee36348a204f0a68b03400f4736224e9f61d1c6a1582d7f875c1ca56f0254268", size = 1708924, upload-time = "2025-07-27T21:23:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/de/07/a1051cdbbe6d723df16d756b97f09da7c1adb69e29695c58f0392bc12515/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7ba5e38c9fbd06f086f4a5a64a1a5b7b417cd3f8fc07a20e5c03651f72f36100", size = 3554141, upload-time = "2025-07-27T21:23:17.938Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/58487d2e16ef3d04f51a7c7f0e69823e806744b4c21101e89da4873074bc/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8adeee57b41fe08e4520698a4b0bd3cc76dbd81f99424b806d70a5256a391d3", size = 1860353, upload-time = "2025-07-27T21:23:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/67/b4/67f6254d166ffbcc9d5fa1b56876eaa920c32ebc8e9d3d525b27296b693b/cramjam-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b96a74fa03a636c8a7d76f700d50e9a8bc17a516d6a72d28711225d641e30968", size = 1693832, upload-time = "2025-07-27T21:23:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/55/a3/4e0b31c0d454ae70c04684ed7c13d3c67b4c31790c278c1e788cb804fa4a/cramjam-2.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c3811a56fa32e00b377ef79121c0193311fd7501f0fb378f254c7f083cc1fbe0", size = 2027080, upload-time = "2025-07-27T21:23:23.303Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c7/5e8eed361d1d3b8be14f38a54852c5370cc0ceb2c2d543b8ba590c34f080/cramjam-2.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d927e87461f8a0d448e4ab5eb2bca9f31ca5d8ea86d70c6f470bb5bc666d7e", size = 1761543, upload-time = "2025-07-27T21:23:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/09/0c/06b7f8b0ce9fde89470505116a01fc0b6cb92d406c4fb1e46f168b5d3fa5/cramjam-2.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f1f5c450121430fd89cb5767e0a9728ecc65997768fd4027d069cb0368af62f9", size = 1854636, upload-time = "2025-07-27T21:23:26.987Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c6/6ebc02c9d5acdf4e5f2b1ec6e1252bd5feee25762246798ae823b3347457/cramjam-2.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:724aa7490be50235d97f07e2ca10067927c5d7f336b786ddbc868470e822aa25", size = 2032715, upload-time = "2025-07-27T21:23:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/a122971c23f5ca4b53e4322c647ac7554626c95978f92d19419315dddd05/cramjam-2.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54c4637122e7cfd7aac5c1d3d4c02364f446d6923ea34cf9d0e8816d6e7a4936", size = 2069039, upload-time = "2025-07-27T21:23:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f6121b90b86b9093c066889274d26a1de3f29969d45c2ed1ecbe2033cb78/cramjam-2.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17eb39b1696179fb471eea2de958fa21f40a2cd8bf6b40d428312d5541e19dc4", size = 1979566, upload-time = "2025-07-27T21:23:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/f95bc57fd7f4166ce6da816cfa917fb7df4bb80e669eb459d85586498414/cramjam-2.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:36aa5a798aa34e11813a80425a30d8e052d8de4a28f27bfc0368cfc454d1b403", size = 2030905, upload-time = "2025-07-27T21:23:33.696Z" }, + { url = "https://files.pythonhosted.org/packages/fc/52/e429de4e8bc86ee65e090dae0f87f45abd271742c63fb2d03c522ffde28a/cramjam-2.11.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:449fca52774dc0199545fbf11f5128933e5a6833946707885cf7be8018017839", size = 2155592, upload-time = "2025-07-27T21:23:35.375Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6c/65a7a0207787ad39ad804af4da7f06a60149de19481d73d270b540657234/cramjam-2.11.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:d87d37b3d476f4f7623c56a232045d25bd9b988314702ea01bd9b4a94948a778", size = 2170839, upload-time = "2025-07-27T21:23:37.197Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/5c5db505ba692bc844246b066e23901d5905a32baf2f33719c620e65887f/cramjam-2.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:26cb45c47d71982d76282e303931c6dd4baee1753e5d48f9a89b3a63e690b3a3", size = 2157236, upload-time = "2025-07-27T21:23:38.854Z" }, + { url = "https://files.pythonhosted.org/packages/b0/22/88e6693e60afe98901e5bbe91b8dea193e3aa7f42e2770f9c3339f5c1065/cramjam-2.11.0-cp314-cp314-win32.whl", hash = "sha256:4efe919d443c2fd112fe25fe636a52f9628250c9a50d9bddb0488d8a6c09acc6", size = 1604136, upload-time = "2025-07-27T21:23:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f8/01618801cd59ccedcc99f0f96d20be67d8cfc3497da9ccaaad6b481781dd/cramjam-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ccec3524ea41b9abd5600e3e27001fd774199dbb4f7b9cb248fcee37d4bda84c", size = 1710272, upload-time = "2025-07-27T21:23:42.236Z" }, + { url = "https://files.pythonhosted.org/packages/40/81/6cdb3ed222d13ae86bda77aafe8d50566e81a1169d49ed195b6263610704/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:966ac9358b23d21ecd895c418c048e806fd254e46d09b1ff0cdad2eba195ea3e", size = 3559671, upload-time = "2025-07-27T21:23:44.504Z" }, + { url = "https://files.pythonhosted.org/packages/cb/43/52b7e54fe5ba1ef0270d9fdc43dabd7971f70ea2d7179be918c997820247/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:387f09d647a0d38dcb4539f8a14281f8eb6bb1d3e023471eb18a5974b2121c86", size = 1867876, upload-time = "2025-07-27T21:23:46.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/28/30d5b8d10acd30db3193bc562a313bff722888eaa45cfe32aa09389f2b24/cramjam-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:665b0d8fbbb1a7f300265b43926457ec78385200133e41fef19d85790fc1e800", size = 1695562, upload-time = "2025-07-27T21:23:48.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/86/ec806f986e01b896a650655024ea52a13e25c3ac8a3a382f493089483cdc/cramjam-2.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ca905387c7a371531b9622d93471be4d745ef715f2890c3702479cd4fc85aa51", size = 2025056, upload-time = "2025-07-27T21:23:50.404Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/c2c17586b90848d29d63181f7d14b8bd3a7d00975ad46e3edf2af8af7e1f/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1aa56aef2c8af55a21ed39040a94a12b53fb23beea290f94d19a76027e2ffb", size = 1764084, upload-time = "2025-07-27T21:23:52.265Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a9/68bc334fadb434a61df10071dc8606702aa4f5b6cdb2df62474fc21d2845/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5db59c1cdfaa2ab85cc988e602d6919495f735ca8a5fd7603608eb1e23c26d5", size = 1854859, upload-time = "2025-07-27T21:23:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4e/b48e67835b5811ec5e9cb2e2bcba9c3fd76dab3e732569fe801b542c6ca9/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f893014f00fe5e89a660a032e813bf9f6d91de74cd1490cdb13b2b59d0c9a3", size = 2035970, upload-time = "2025-07-27T21:23:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/c4/70/d2ac33d572b4d90f7f0f2c8a1d60fb48f06b128fdc2c05f9b49891bb0279/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c26a1eb487947010f5de24943bd7c422dad955b2b0f8650762539778c380ca89", size = 2069320, upload-time = "2025-07-27T21:23:57.494Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4c/85cec77af4a74308ba5fca8e296c4e2f80ec465c537afc7ab1e0ca2f9a00/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d5c8bfb438d94e7b892d1426da5fc4b4a5370cc360df9b8d9d77c33b896c37e", size = 1982668, upload-time = "2025-07-27T21:23:59.126Z" }, + { url = "https://files.pythonhosted.org/packages/55/45/938546d1629e008cc3138df7c424ef892719b1796ff408a2ab8550032e5e/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:cb1fb8c9337ab0da25a01c05d69a0463209c347f16512ac43be5986f3d1ebaf4", size = 2034028, upload-time = "2025-07-27T21:24:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/b5a53e20505555f1640e66dcf70394bcf51a1a3a072aa18ea35135a0f9ed/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:1f6449f6de52dde3e2f1038284910c8765a397a25e2d05083870f3f5e7fc682c", size = 2155513, upload-time = "2025-07-27T21:24:02.92Z" }, + { url = "https://files.pythonhosted.org/packages/84/12/8d3f6ceefae81bbe45a347fdfa2219d9f3ac75ebc304f92cd5fcb4fbddc5/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:382dec4f996be48ed9c6958d4e30c2b89435d7c2c4dbf32480b3b8886293dd65", size = 2170035, upload-time = "2025-07-27T21:24:04.558Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/3be6f0a1398f976070672be64f61895f8839857618a2d8cc0d3ab529d3dc/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:d388bd5723732c3afe1dd1d181e4213cc4e1be210b080572e7d5749f6e955656", size = 2160229, upload-time = "2025-07-27T21:24:06.729Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/66cfc3635511b20014bbb3f2ecf0095efb3049e9e96a4a9e478e4f3d7b78/cramjam-2.11.0-cp314-cp314t-win32.whl", hash = "sha256:0a70ff17f8e1d13f322df616505550f0f4c39eda62290acb56f069d4857037c8", size = 1610267, upload-time = "2025-07-27T21:24:08.428Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c6/c71e82e041c95ffe6a92ac707785500aa2a515a4339c2c7dd67e3c449249/cramjam-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:028400d699442d40dbda02f74158c73d05cb76587a12490d0bfedd958fd49188", size = 1713108, upload-time = "2025-07-27T21:24:10.147Z" }, + { url = "https://files.pythonhosted.org/packages/81/da/b3301962ccd6fce9fefa1ecd8ea479edaeaa38fadb1f34d5391d2587216a/cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:52d5db3369f95b27b9f3c14d067acb0b183333613363ed34268c9e04560f997f", size = 3573546, upload-time = "2025-07-27T21:24:52.944Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c2/410ddb8ad4b9dfb129284666293cb6559479645da560f7077dc19d6bee9e/cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4820516366d455b549a44d0e2210ee7c4575882dda677564ce79092588321d54", size = 1873654, upload-time = "2025-07-27T21:24:54.958Z" }, + { url = "https://files.pythonhosted.org/packages/d5/99/f68a443c64f7ce7aff5bed369b0aa5b2fac668fa3dfd441837e316e97a1f/cramjam-2.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d9e5db525dc0a950a825202f84ee68d89a072479e07da98795a3469df942d301", size = 1702846, upload-time = "2025-07-27T21:24:57.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/02/0ff358ab773def1ee3383587906c453d289953171e9c92db84fdd01bf172/cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62ab4971199b2270005359cdc379bc5736071dc7c9a228581c5122d9ffaac50c", size = 1773683, upload-time = "2025-07-27T21:24:59.28Z" }, + { url = "https://files.pythonhosted.org/packages/e9/31/3298e15f87c9cf2aabdbdd90b153d8644cf989cb42a45d68a1b71e1f7aaf/cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24758375cc5414d3035ca967ebb800e8f24604ececcba3c67d6f0218201ebf2d", size = 1994136, upload-time = "2025-07-27T21:25:01.565Z" }, + { url = "https://files.pythonhosted.org/packages/c7/90/20d1747255f1ee69a412e319da51ea594c18cca195e7a4d4c713f045eff5/cramjam-2.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6c2eea545fef1065c7dd4eda991666fd9c783fbc1d226592ccca8d8891c02f23", size = 1714982, upload-time = "2025-07-27T21:25:05.79Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + +[[package]] +name = "cssselect2" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tinycss2" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "donfig" +version = "0.8.1.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/71/80cc718ff6d7abfbabacb1f57aaa42e9c1552bfdd01e64ddd704e4a03638/donfig-0.8.1.post1.tar.gz", hash = "sha256:3bef3413a4c1c601b585e8d297256d0c1470ea012afa6e8461dc28bfb7c23f52", size = 19506, upload-time = "2024-05-23T14:14:31.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl", hash = "sha256:2a3175ce74a06109ff9307d90a230f81215cbac9a751f4d1c6194644b8204f9d", size = 21592, upload-time = "2024-05-23T14:13:55.283Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, +] + +[[package]] +name = "folium" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "branca" }, + { name = "jinja2" }, + { name = "numpy" }, + { name = "requests" }, + { name = "xyzservices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/76/84a1b1b00ce71f9c0c44af7d80f310c02e2e583591fe7d4cb03baecd0d3f/folium-0.20.0.tar.gz", hash = "sha256:a0d78b9d5a36ba7589ca9aedbd433e84e9fcab79cd6ac213adbcff922e454cb9", size = 109932, upload-time = "2025-06-16T20:22:51.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/a8/5f764f333204db0390362a4356d03a43626997f26818a0e9396f1b3bd8c9/folium-0.20.0-py2.py3-none-any.whl", hash = "sha256:f0bc2a92acde20bca56367aa5c1c376c433f450608d058daebab2fc9bf8198bf", size = 113394, upload-time = "2025-06-16T20:22:50.318Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[[package]] +name = "gcsfs" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "decorator" }, + { name = "fsspec" }, + { name = "google-auth" }, + { name = "google-auth-oauthlib" }, + { name = "google-cloud-storage" }, + { name = "google-cloud-storage-control" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/91/e7a2f237d51436a4fc947f30f039d2c277bb4f4ce02f86628ba0a094a3ce/gcsfs-2026.2.0.tar.gz", hash = "sha256:d58a885d9e9c6227742b86da419c7a458e1f33c1de016e826ea2909f6338ed84", size = 163376, upload-time = "2026-02-06T18:35:52.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/6b/c2f68ac51229fc94f094c7f802648fc1de3d19af36434def5e64c0caa32b/gcsfs-2026.2.0-py3-none-any.whl", hash = "sha256:407feaa2af0de81ebce44ea7e6f68598a3753e5e42257b61d6a9f8c0d6d4754e", size = 57557, upload-time = "2026-02-06T18:35:51.09Z" }, +] + +[[package]] +name = "geojson-pydantic" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/52/961c8f7c51067f5d853a732cd4abc09b4d15c742384406dda8348b98071e/geojson_pydantic-2.1.0.tar.gz", hash = "sha256:78a52b2a7cd9c113bac4898a81ce00c146c7927dd2804f1c7e9fd05c2515073f", size = 9398, upload-time = "2025-10-08T13:31:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/18/8a9dca353e605b344408114f6b045b11d14082d19f4668b073259d3ed1a9/geojson_pydantic-2.1.0-py3-none-any.whl", hash = "sha256:f9091bed334ab9fbb1bef113674edc1212a3737f374a0b13b1aa493f57964c1d", size = 8819, upload-time = "2025-10-08T13:31:11.646Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/98/586ec94553b569080caef635f98a3723db36a38eac0e3d7eb3ea9d2e4b9a/google_api_core-2.30.0.tar.gz", hash = "sha256:02edfa9fab31e17fc0befb5f161b3bf93c9096d99aed584625f38065c511ad9b", size = 176959, upload-time = "2026-02-18T20:28:11.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/27/09c33d67f7e0dcf06d7ac17d196594e66989299374bfb0d4331d1038e76b/google_api_core-2.30.0-py3-none-any.whl", hash = "sha256:80be49ee937ff9aba0fd79a6eddfde35fe658b9953ab9b79c57dd7061afa8df5", size = 173288, upload-time = "2026-02-18T20:28:10.367Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/b4/1b19567e4c567b796f5c593d89895f3cfae5a38e04f27c6af87618fd0942/google_auth_oauthlib-1.3.0.tar.gz", hash = "sha256:cd39e807ac7229d6b8b9c1e297321d36fcc8a9e4857dff4301870985df51a528", size = 21777, upload-time = "2026-02-27T14:13:01.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/56/909fd5632226d3fba31d7aeffd4754410735d49362f5809956fe3e9af344/google_auth_oauthlib-1.3.0-py3-none-any.whl", hash = "sha256:386b3fb85cf4a5b819c6ad23e3128d975216b4cac76324de1d90b128aaf38f29", size = 19308, upload-time = "2026-02-27T14:12:47.865Z" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027, upload-time = "2025-10-29T23:17:39.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469, upload-time = "2025-10-29T23:17:38.548Z" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/b1/4f0798e88285b50dfc60ed3a7de071def538b358db2da468c2e0deecbb40/google_cloud_storage-3.9.0.tar.gz", hash = "sha256:f2d8ca7db2f652be757e92573b2196e10fbc09649b5c016f8b422ad593c641cc", size = 17298544, upload-time = "2026-02-02T13:36:34.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/0b/816a6ae3c9fd096937d2e5f9670558908811d57d59ddf69dd4b83b326fd1/google_cloud_storage-3.9.0-py3-none-any.whl", hash = "sha256:2dce75a9e8b3387078cbbdad44757d410ecdb916101f8ba308abf202b6968066", size = 321324, upload-time = "2026-02-02T13:36:32.271Z" }, +] + +[[package]] +name = "google-cloud-storage-control" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/c0/12dfbf7c5e86e34da4af971bb043f11cdc9be8d204eb06ac8a1f9b1d5c74/google_cloud_storage_control-1.10.0.tar.gz", hash = "sha256:2bcbfa4ca6530d25a5baa8dbe80caf0eeabe4c6804798f4f107279719c316bdb", size = 116845, upload-time = "2026-02-12T14:50:07.096Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/04/96a674d4ee90eed4e99c0f4faec21c9bbe1a470d37a4757508e90e31f5b9/google_cloud_storage_control-1.10.0-py3-none-any.whl", hash = "sha256:81d9dc6b50106836733adca868501f879f0d7a1c41503d887a1a1b9b9ddbf508", size = 89257, upload-time = "2026-02-12T14:50:01.966Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, + { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, +] + +[[package]] +name = "google-resumable-media" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/d7/520b62a35b23038ff005e334dba3ffc75fcf583bee26723f1fd8fd4b6919/google_resumable_media-2.8.0.tar.gz", hash = "sha256:f1157ed8b46994d60a1bc432544db62352043113684d4e030ee02e77ebe9a1ae", size = 2163265, upload-time = "2025-11-17T15:38:06.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl", hash = "sha256:dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582", size = 81340, upload-time = "2025-11-17T15:38:05.594Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + +[[package]] +name = "griffe-inherited-docstrings" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/da/fd002dc5f215cd896bfccaebe8b4aa1cdeed8ea1d9d60633685bd61ff933/griffe_inherited_docstrings-1.1.3.tar.gz", hash = "sha256:cd1f937ec9336a790e5425e7f9b92f5a5ab17f292ba86917f1c681c0704cb64e", size = 26738, upload-time = "2026-02-21T09:38:44.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/20/4bc15f242181daad1c104e0a7d33be49e712461ea89e548152be0365b9ea/griffe_inherited_docstrings-1.1.3-py3-none-any.whl", hash = "sha256:aa7f6e624515c50d9325a5cfdf4b2acac547f1889aca89092d5da7278f739695", size = 6710, upload-time = "2026-02-20T11:06:38.75Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, +] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/1e/1011451679a983f2f5c6771a1682542ecb027776762ad031fd0d7129164b/grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389", size = 23745, upload-time = "2025-10-15T21:14:53.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/bd/330a1bbdb1afe0b96311249e699b6dc9cfc17916394fd4503ac5aca2514b/grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6", size = 32690, upload-time = "2025-10-15T21:14:51.72Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/cd/89ce482a931b543b92cdd9b2888805518c4620e0094409acb8c81dd4610a/grpcio_status-1.78.0.tar.gz", hash = "sha256:a34cfd28101bfea84b5aa0f936b4b423019e9213882907166af6b3bddc59e189", size = 13808, upload-time = "2026-02-06T10:01:48.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/8a/1241ec22c41028bddd4a052ae9369267b4475265ad0ce7140974548dc3fa/grpcio_status-1.78.0-py3-none-any.whl", hash = "sha256:b492b693d4bf27b47a6c32590701724f1d3b9444b36491878fb71f6208857f34", size = 14523, upload-time = "2026-02-06T10:01:32.584Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h5netcdf" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/03/92d6cc02c0055158167255980461155d6e17f1c4143c03f8bcc18d3e3f3a/h5netcdf-1.8.1.tar.gz", hash = "sha256:9b396a4cc346050fc1a4df8523bc1853681ec3544e0449027ae397cb953c7a16", size = 78679, upload-time = "2026-01-23T07:35:31.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/8b/88f16936a8e8070a83d36239555227ecd91728f9ef222c5382cda07e0fd6/h5netcdf-1.8.1-py3-none-any.whl", hash = "sha256:a76ed7cfc9b8a8908ea7057c4e57e27307acff1049b7f5ed52db6c2247636879", size = 62915, upload-time = "2026-01-23T07:35:30.195Z" }, +] + +[[package]] +name = "h5py" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243", size = 3434135, upload-time = "2025-10-16T10:33:47.954Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509", size = 2870958, upload-time = "2025-10-16T10:33:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e3/c255cafc9b85e6ea04e2ad1bba1416baa1d7f57fc98a214be1144087690c/h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b", size = 4504770, upload-time = "2025-10-16T10:33:54.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf", size = 4700329, upload-time = "2025-10-16T10:33:57.616Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e4/932a3a8516e4e475b90969bf250b1924dbe3612a02b897e426613aed68f4/h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c", size = 4152456, upload-time = "2025-10-16T10:34:00.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/f74d589883b13737021b2049ac796328f188dbb60c2ed35b101f5b95a3fc/h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831", size = 4617295, upload-time = "2025-10-16T10:34:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878", size = 2882129, upload-time = "2025-10-16T10:34:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/cfcc70b8a42222ba3ad4478bcef1791181ea908e2adbd7d53c66395edad5/h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427", size = 2477121, upload-time = "2025-10-16T10:34:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" }, + { url = "https://files.pythonhosted.org/packages/3a/30/d1c94066343a98bb2cea40120873193a4fed68c4ad7f8935c11caf74c681/h5py-3.15.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25c8843fec43b2cc368aa15afa1cdf83fc5e17b1c4e10cd3771ef6c39b72e5ce", size = 5109965, upload-time = "2025-10-16T10:34:21.853Z" }, + { url = "https://files.pythonhosted.org/packages/81/3d/d28172116eafc3bc9f5991b3cb3fd2c8a95f5984f50880adfdf991de9087/h5py-3.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a308fd8681a864c04423c0324527237a0484e2611e3441f8089fd00ed56a8171", size = 4561870, upload-time = "2025-10-16T10:34:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/393a7226024238b0f51965a7156004eaae1fcf84aa4bfecf7e582676271b/h5py-3.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4a016df3f4a8a14d573b496e4d1964deb380e26031fc85fb40e417e9131888a", size = 5037161, upload-time = "2025-10-16T10:34:30.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/51/329e7436bf87ca6b0fe06dd0a3795c34bebe4ed8d6c44450a20565d57832/h5py-3.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:59b25cf02411bf12e14f803fef0b80886444c7fe21a5ad17c6a28d3f08098a1e", size = 2874165, upload-time = "2025-10-16T10:34:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/2d02b10a66747c54446e932171dd89b8b4126c0111b440e6bc05a7c852ec/h5py-3.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:61d5a58a9851e01ee61c932bbbb1c98fe20aba0a5674776600fb9a361c0aa652", size = 2458214, upload-time = "2025-10-16T10:34:35.733Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8440fd8bee9500c235ecb7aa1917a0389a2adb80c209fa1cc485bd70e0d94a5", size = 3376511, upload-time = "2025-10-16T10:34:38.596Z" }, + { url = "https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab2219dbc6fcdb6932f76b548e2b16f34a1f52b7666e998157a4dfc02e2c4123", size = 2826143, upload-time = "2025-10-16T10:34:41.342Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/fc6375d07ea3962df7afad7d863fe4bde18bb88530678c20d4c90c18de1d/h5py-3.15.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8cb02c3a96255149ed3ac811eeea25b655d959c6dd5ce702c9a95ff11859eb5", size = 4908316, upload-time = "2025-10-16T10:34:44.619Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8", size = 5103710, upload-time = "2025-10-16T10:34:48.639Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f6/11f1e2432d57d71322c02a97a5567829a75f223a8c821764a0e71a65cde8/h5py-3.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59b0d63b318bf3cc06687def2b45afd75926bbc006f7b8cd2b1a231299fc8599", size = 4556042, upload-time = "2025-10-16T10:34:51.841Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/3eda3ef16bfe7a7dbc3d8d6836bbaa7986feb5ff091395e140dc13927bcc/h5py-3.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e02fe77a03f652500d8bff288cbf3675f742fc0411f5a628fa37116507dc7cc0", size = 5030639, upload-time = "2025-10-16T10:34:55.257Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:dea78b092fd80a083563ed79a3171258d4a4d307492e7cf8b2313d464c82ba52", size = 2864363, upload-time = "2025-10-16T10:34:58.099Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c9/35021cc9cd2b2915a7da3026e3d77a05bed1144a414ff840953b33937fb9/h5py-3.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:c256254a8a81e2bddc0d376e23e2a6d2dc8a1e8a2261835ed8c1281a0744cd97", size = 2449570, upload-time = "2025-10-16T10:35:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/926eba1514e4d2e47d0e9eb16c784e717d8b066398ccfca9b283917b1bfb/h5py-3.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f4fb0567eb8517c3ecd6b3c02c4f4e9da220c8932604960fd04e24ee1254763", size = 3380368, upload-time = "2025-10-16T10:35:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/65/4b/d715ed454d3baa5f6ae1d30b7eca4c7a1c1084f6a2edead9e801a1541d62/h5py-3.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:954e480433e82d3872503104f9b285d369048c3a788b2b1a00e53d1c47c98dd2", size = 2833793, upload-time = "2025-10-16T10:35:05.623Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/ef386c28e4579314610a8bffebbee3b69295b0237bc967340b7c653c6c10/h5py-3.15.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd125c131889ebbef0849f4a0e29cf363b48aba42f228d08b4079913b576bb3a", size = 4903199, upload-time = "2025-10-16T10:35:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/33/5d/65c619e195e0b5e54ea5a95c1bb600c8ff8715e0d09676e4cce56d89f492/h5py-3.15.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28a20e1a4082a479b3d7db2169f3a5034af010b90842e75ebbf2e9e49eb4183e", size = 5097224, upload-time = "2025-10-16T10:35:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/30/30/5273218400bf2da01609e1292f562c94b461fcb73c7a9e27fdadd43abc0a/h5py-3.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8df5267f545b4946df8ca0d93d23382191018e4cda2deda4c2cedf9a010e13", size = 4551207, upload-time = "2025-10-16T10:35:16.24Z" }, + { url = "https://files.pythonhosted.org/packages/d3/39/a7ef948ddf4d1c556b0b2b9559534777bccc318543b3f5a1efdf6b556c9c/h5py-3.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99d374a21f7321a4c6ab327c4ab23bd925ad69821aeb53a1e75dd809d19f67fa", size = 5025426, upload-time = "2025-10-16T10:35:19.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d8/7368679b8df6925b8415f9dcc9ab1dab01ddc384d2b2c24aac9191bd9ceb/h5py-3.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:9c73d1d7cdb97d5b17ae385153472ce118bed607e43be11e9a9deefaa54e0734", size = 2865704, upload-time = "2025-10-16T10:35:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/4a806f85d62c20157e62e58e03b27513dc9c55499768530acc4f4c5ce4be/h5py-3.15.1-cp314-cp314-win_arm64.whl", hash = "sha256:a6d8c5a05a76aca9a494b4c53ce8a9c29023b7f64f625c6ce1841e92a362ccdf", size = 2465544, upload-time = "2025-10-16T10:35:25.695Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "identify" +version = "2.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "ipykernel" +version = "6.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/1d/d5ba6edbfe6fae4c3105bca3a9c889563cc752c7f2de45e333164c7f4846/ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6", size = 167493, upload-time = "2025-10-20T11:42:39.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" }, +] + +[[package]] +name = "ipython" +version = "9.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version < '3.12'" }, + { name = "jedi", marker = "python_full_version < '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.12'" }, + { name = "pexpect", marker = "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "stack-data", marker = "python_full_version < '3.12'" }, + { name = "traitlets", marker = "python_full_version < '3.12'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, +] + +[[package]] +name = "ipython" +version = "9.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, + { name = "jedi", marker = "python_full_version >= '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, + { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "stack-data", marker = "python_full_version >= '3.12'" }, + { name = "traitlets", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "ipywidgets" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "json5" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "jupyter-console" }, + { name = "jupyterlab" }, + { name = "nbconvert" }, + { name = "notebook" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959, upload-time = "2024-08-30T07:15:48.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-console" +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "pyzmq" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363, upload-time = "2023-03-06T14:13:31.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510, upload-time = "2023-03-06T14:13:28.229Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/2d/953a5612a34a3c799a62566a548e711d103f631672fd49650e0f2de80870/jupyterlab-4.5.5.tar.gz", hash = "sha256:eac620698c59eb810e1729909be418d9373d18137cac66637141abba613b3fda", size = 23968441, upload-time = "2026-02-23T18:57:34.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/52/372d3494766d690dfdd286871bf5f7fb9a6c61f7566ccaa7153a163dd1df/jupyterlab-4.5.5-py3-none-any.whl", hash = "sha256:a35694a40a8e7f2e82f387472af24e61b22adcce87b5a8ab97a5d9c486202a6d", size = 12446824, upload-time = "2026-02-23T18:57:30.398Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, +] + +[[package]] +name = "jupytext" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/a5/80c02f307c8ce863cb33e27daf049315e9d96979e14eead700923b5ec9cc/jupytext-1.19.1.tar.gz", hash = "sha256:82587c07e299173c70ed5e8ec7e75183edf1be289ed518bab49ad0d4e3d5f433", size = 4307829, upload-time = "2026-01-25T21:35:13.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl", hash = "sha256:d8975035155d034bdfde5c0c37891425314b7ea8d3a6c4b5d18c294348714cd9", size = 170478, upload-time = "2026-01-25T21:35:11.17Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-jupyter" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "jupytext" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "nbconvert" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/23/6ffb8d2fd2117aa860a04c6fe2510b21bc3c3c085907ffdd851caba53152/mkdocs_jupyter-0.25.1.tar.gz", hash = "sha256:0e9272ff4947e0ec683c92423a4bfb42a26477c103ab1a6ab8277e2dcc8f7afe", size = 1626747, upload-time = "2024-10-15T14:56:32.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.1-py3-none-any.whl", hash = "sha256:3f679a857609885d322880e72533ef5255561bbfdb13cfee2a1e92ef4d4ad8d8", size = 1456197, upload-time = "2024-10-15T14:56:29.854Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/ce/a1cd02ac7448763f0bb56aaf5f23fa2527944ac6df335080c38c2f253165/mkdocs_material-9.7.4.tar.gz", hash = "sha256:711b0ee63aca9a8c7124d4c73e83a25aa996e27e814767c3a3967df1b9e56f32", size = 4097804, upload-time = "2026-03-03T19:57:36.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/94/e3535a9ed078b238df3df75a44694ca0ff5772fd538df4939c658a58c59d/mkdocs_material-9.7.4-py3-none-any.whl", hash = "sha256:6549ad95e4d130ed5099759dfa76ea34c593eefdb9c18c97273605518e99cfbf", size = 9305224, upload-time = "2026-03-03T19:57:34.063Z" }, +] + +[package.optional-dependencies] +imaging = [ + { name = "cairosvg" }, + { name = "pillow" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, +] + +[[package]] +name = "morecantile" +version = "7.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "pydantic" }, + { name = "pyproj" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/44/fa4f0685481b51e5ce33089ca84e821453593d34f306738515a937934751/morecantile-7.0.3.tar.gz", hash = "sha256:b44419c83f310b411b1f547df2d07c115dc6d194ab7c8a4c318a154490e938c1", size = 43104, upload-time = "2026-02-04T23:03:26.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/1a/f9edb1c167bd8ad214dc87e9ae17fb208d08f7a05e724f74ce29e375c8de/morecantile-7.0.3-py3-none-any.whl", hash = "sha256:747c6b8f3a8029ddaadb04d96c834f10d2796d1898ad893bed47c896a058ddc7", size = 50885, upload-time = "2026-02-04T23:03:27.463Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "notebook" +version = "7.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, + { name = "jupyterlab" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/08/9d446fbb49f95de316ea6d7f25d0a4bc95117dd574e35f405895ac706f29/notebook-7.5.4.tar.gz", hash = "sha256:b928b2ba22cb63aa83df2e0e76fe3697950a0c1c4a41b84ebccf1972b1bb5771", size = 14167892, upload-time = "2026-02-24T14:13:56.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/01/05e5387b53e0f549212d5eff58845886f3827617b5c9409c966ddc07cb6d/notebook-7.5.4-py3-none-any.whl", hash = "sha256:860e31782b3d3a25ca0819ff039f5cf77845d1bf30c78ef9528b88b25e0a9850", size = 14578014, upload-time = "2026-02-24T14:13:52.274Z" }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, +] + +[[package]] +name = "numcodecs" +version = "0.16.6.dev2" +source = { git = "https://github.com/zarr-developers/numcodecs#e0ddee6b6d01bfd35a91085ec0a20dae6bf50e13" } +dependencies = [ + { name = "numpy" }, + { name = "typing-extensions" }, +] + +[[package]] +name = "numexpr" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/2f/fdba158c9dbe5caca9c3eca3eaffffb251f2fb8674bf8e2d0aed5f38d319/numexpr-2.14.1.tar.gz", hash = "sha256:4be00b1086c7b7a5c32e31558122b7b80243fe098579b170967da83f3152b48b", size = 119400, upload-time = "2025-10-13T16:17:27.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/67999bdd1ed1f938d38f3fedd4969632f2f197b090e50505f7cc1fa82510/numexpr-2.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d03fcb4644a12f70a14d74006f72662824da5b6128bf1bcd10cc3ed80e64c34", size = 163195, upload-time = "2025-10-13T16:16:31.212Z" }, + { url = "https://files.pythonhosted.org/packages/25/95/d64f680ea1fc56d165457287e0851d6708800f9fcea346fc1b9957942ee6/numexpr-2.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2773ee1133f77009a1fc2f34fe236f3d9823779f5f75450e183137d49f00499f", size = 152088, upload-time = "2025-10-13T16:16:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7f/3bae417cb13ae08afd86d08bb0301c32440fe0cae4e6262b530e0819aeda/numexpr-2.14.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebe4980f9494b9f94d10d2e526edc29e72516698d3bf95670ba79415492212a4", size = 451126, upload-time = "2025-10-13T16:13:22.248Z" }, + { url = "https://files.pythonhosted.org/packages/4c/1a/edbe839109518364ac0bd9e918cf874c755bb2c128040e920f198c494263/numexpr-2.14.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a381e5e919a745c9503bcefffc1c7f98c972c04ec58fc8e999ed1a929e01ba6", size = 442012, upload-time = "2025-10-13T16:14:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/b1/be4ce99bff769a5003baddac103f34681997b31d4640d5a75c0e8ed59c78/numexpr-2.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d08856cfc1b440eb1caaa60515235369654321995dd68eb9377577392020f6cb", size = 1415975, upload-time = "2025-10-13T16:13:26.088Z" }, + { url = "https://files.pythonhosted.org/packages/e7/33/b33b8fdc032a05d9ebb44a51bfcd4b92c178a2572cd3e6c1b03d8a4b45b2/numexpr-2.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03130afa04edf83a7b590d207444f05a00363c9b9ea5d81c0f53b1ea13fad55a", size = 1464683, upload-time = "2025-10-13T16:14:58.87Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b2/ddcf0ac6cf0a1d605e5aecd4281507fd79a9628a67896795ab2e975de5df/numexpr-2.14.1-cp311-cp311-win32.whl", hash = "sha256:db78fa0c9fcbaded3ae7453faf060bd7a18b0dc10299d7fcd02d9362be1213ed", size = 166838, upload-time = "2025-10-13T16:17:06.765Z" }, + { url = "https://files.pythonhosted.org/packages/64/72/4ca9bd97b2eb6dce9f5e70a3b6acec1a93e1fb9b079cb4cba2cdfbbf295d/numexpr-2.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:e9b2f957798c67a2428be96b04bce85439bed05efe78eb78e4c2ca43737578e7", size = 160069, upload-time = "2025-10-13T16:17:08.752Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/c473fc04a371f5e2f8c5749e04505c13e7a8ede27c09e9f099b2ad6f43d6/numexpr-2.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ebae0ab18c799b0e6b8c5a8d11e1fa3848eb4011271d99848b297468a39430", size = 162790, upload-time = "2025-10-13T16:16:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/45/93/b6760dd1904c2a498e5f43d1bb436f59383c3ddea3815f1461dfaa259373/numexpr-2.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47041f2f7b9e69498fb311af672ba914a60e6e6d804011caacb17d66f639e659", size = 152196, upload-time = "2025-10-13T16:16:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/72/94/cc921e35593b820521e464cbbeaf8212bbdb07f16dc79fe283168df38195/numexpr-2.14.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d686dfb2c1382d9e6e0ee0b7647f943c1886dba3adbf606c625479f35f1956c1", size = 452468, upload-time = "2025-10-13T16:13:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/d9/43/560e9ba23c02c904b5934496486d061bcb14cd3ebba2e3cf0e2dccb6c22b/numexpr-2.14.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee6d4fbbbc368e6cdd0772734d6249128d957b3b8ad47a100789009f4de7083", size = 443631, upload-time = "2025-10-13T16:15:02.473Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6c/78f83b6219f61c2c22d71ab6e6c2d4e5d7381334c6c29b77204e59edb039/numexpr-2.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a2839efa25f3c8d4133252ea7342d8f81226c7c4dda81f97a57e090b9d87a48", size = 1417670, upload-time = "2025-10-13T16:13:33.464Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bb/1ccc9dcaf46281568ce769888bf16294c40e98a5158e4b16c241de31d0d3/numexpr-2.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9f9137f1351b310436662b5dc6f4082a245efa8950c3b0d9008028df92fefb9b", size = 1466212, upload-time = "2025-10-13T16:15:12.828Z" }, + { url = "https://files.pythonhosted.org/packages/31/9f/203d82b9e39dadd91d64bca55b3c8ca432e981b822468dcef41a4418626b/numexpr-2.14.1-cp312-cp312-win32.whl", hash = "sha256:36f8d5c1bd1355df93b43d766790f9046cccfc1e32b7c6163f75bcde682cda07", size = 166996, upload-time = "2025-10-13T16:17:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/ffe750b5452eb66de788c34e7d21ec6d886abb4d7c43ad1dc88ceb3d998f/numexpr-2.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:fdd886f4b7dbaf167633ee396478f0d0aa58ea2f9e7ccc3c6431019623e8d68f", size = 160187, upload-time = "2025-10-13T16:17:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/73/b4/9f6d637fd79df42be1be29ee7ba1f050fab63b7182cb922a0e08adc12320/numexpr-2.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09078ba73cffe94745abfbcc2d81ab8b4b4e9d7bfbbde6cac2ee5dbf38eee222", size = 162794, upload-time = "2025-10-13T16:16:38.291Z" }, + { url = "https://files.pythonhosted.org/packages/35/ae/d58558d8043de0c49f385ea2fa789e3cfe4d436c96be80200c5292f45f15/numexpr-2.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dce0b5a0447baa7b44bc218ec2d7dcd175b8eee6083605293349c0c1d9b82fb6", size = 152203, upload-time = "2025-10-13T16:16:39.907Z" }, + { url = "https://files.pythonhosted.org/packages/13/65/72b065f9c75baf8f474fd5d2b768350935989d4917db1c6c75b866d4067c/numexpr-2.14.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06855053de7a3a8425429bd996e8ae3c50b57637ad3e757e0fa0602a7874be30", size = 455860, upload-time = "2025-10-13T16:13:35.811Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f9/c9457652dfe28e2eb898372da2fe786c6db81af9540c0f853ee04a0699cc/numexpr-2.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f9366d23a2e991fd5a8b5e61a17558f028ba86158a4552f8f239b005cdf83c", size = 446574, upload-time = "2025-10-13T16:15:17.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/8d3879c4d67d3db5560cf2de65ce1778b80b75f6fa415eb5c3e7bd37ba27/numexpr-2.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c5f1b1605695778896534dfc6e130d54a65cd52be7ed2cd0cfee3981fd676bf5", size = 1417306, upload-time = "2025-10-13T16:13:42.813Z" }, + { url = "https://files.pythonhosted.org/packages/ea/05/6bddac9f18598ba94281e27a6943093f7d0976544b0cb5d92272c64719bd/numexpr-2.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4ba71db47ea99c659d88ee6233fa77b6dc83392f1d324e0c90ddf617ae3f421", size = 1466145, upload-time = "2025-10-13T16:15:27.464Z" }, + { url = "https://files.pythonhosted.org/packages/24/5d/cbeb67aca0c5a76ead13df7e8bd8dd5e0d49145f90da697ba1d9f07005b0/numexpr-2.14.1-cp313-cp313-win32.whl", hash = "sha256:638dce8320f4a1483d5ca4fda69f60a70ed7e66be6e68bc23fb9f1a6b78a9e3b", size = 166996, upload-time = "2025-10-13T16:17:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/cc/23/9281bceaeb282cead95f0aa5f7f222ffc895670ea689cc1398355f6e3001/numexpr-2.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fdcd4735121658a313f878fd31136d1bfc6a5b913219e7274e9fca9f8dac3bb", size = 160189, upload-time = "2025-10-13T16:17:15.417Z" }, + { url = "https://files.pythonhosted.org/packages/f3/76/7aac965fd93a56803cbe502aee2adcad667253ae34b0badf6c5af7908b6c/numexpr-2.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:557887ad7f5d3c2a40fd7310e50597045a68e66b20a77b3f44d7bc7608523b4b", size = 163524, upload-time = "2025-10-13T16:16:42.213Z" }, + { url = "https://files.pythonhosted.org/packages/58/65/79d592d5e63fbfab3b59a60c386853d9186a44a3fa3c87ba26bdc25b6195/numexpr-2.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:af111c8fe6fc55d15e4c7cab11920fc50740d913636d486545b080192cd0ad73", size = 152919, upload-time = "2025-10-13T16:16:44.229Z" }, + { url = "https://files.pythonhosted.org/packages/84/78/3c8335f713d4aeb99fa758d7c62f0be1482d4947ce5b508e2052bb7aeee9/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33265294376e7e2ae4d264d75b798a915d2acf37b9dd2b9405e8b04f84d05cfc", size = 465972, upload-time = "2025-10-13T16:13:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/35/81/9ee5f69b811e8f18746c12d6f71848617684edd3161927f95eee7a305631/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83647d846d3eeeb9a9255311236135286728b398d0d41d35dedb532dca807fe9", size = 456953, upload-time = "2025-10-13T16:15:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/9b8bc6e294d85cbb54a634e47b833e9f3276a8bdf7ce92aa808718a0212d/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6e575fd3ad41ddf3355d0c7ef6bd0168619dc1779a98fe46693cad5e95d25e6e", size = 1426199, upload-time = "2025-10-13T16:13:48.231Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ce/0d4fcd31ab49319740d934fba1734d7dad13aa485532ca754e555ca16c8b/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:67ea4771029ce818573b1998f5ca416bd255156feea017841b86176a938f7d19", size = 1474214, upload-time = "2025-10-13T16:15:38.893Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/b2a93cbdb3ba4e009728ad1b9ef1550e2655ea2c86958ebaf03b9615f275/numexpr-2.14.1-cp313-cp313t-win32.whl", hash = "sha256:15015d47d3d1487072d58c0e7682ef2eb608321e14099c39d52e2dd689483611", size = 167676, upload-time = "2025-10-13T16:17:17.351Z" }, + { url = "https://files.pythonhosted.org/packages/86/99/ee3accc589ed032eea68e12172515ed96a5568534c213ad109e1f4411df1/numexpr-2.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:94c711f6d8f17dfb4606842b403699603aa591ab9f6bf23038b488ea9cfb0f09", size = 161096, upload-time = "2025-10-13T16:17:19.174Z" }, + { url = "https://files.pythonhosted.org/packages/ac/36/9db78dfbfdfa1f8bf0872993f1a334cdd8fca5a5b6567e47dcb128bcb7c2/numexpr-2.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ede79f7ff06629f599081de644546ce7324f1581c09b0ac174da88a470d39c21", size = 162848, upload-time = "2025-10-13T16:16:46.216Z" }, + { url = "https://files.pythonhosted.org/packages/13/c1/a5c78ae637402c5550e2e0ba175275d2515d432ec28af0cdc23c9b476e65/numexpr-2.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2eac7a5a2f70b3768c67056445d1ceb4ecd9b853c8eda9563823b551aeaa5082", size = 152270, upload-time = "2025-10-13T16:16:47.92Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ed/aabd8678077848dd9a751c5558c2057839f5a09e2a176d8dfcd0850ee00e/numexpr-2.14.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aedf38d4c0c19d3cecfe0334c3f4099fb496f54c146223d30fa930084bc8574", size = 455918, upload-time = "2025-10-13T16:13:50.338Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/3db65117f02cdefb0e5e4c440daf1c30beb45051b7f47aded25b7f4f2f34/numexpr-2.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439ec4d57b853792ebe5456e3160312281c3a7071ecac5532ded3278ede614de", size = 446512, upload-time = "2025-10-13T16:15:42.313Z" }, + { url = "https://files.pythonhosted.org/packages/9a/fb/7ceb9ee55b5f67e4a3e4d73d5af4c7e37e3c9f37f54bee90361b64b17e3f/numexpr-2.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e23b87f744e04e302d82ac5e2189ae20a533566aec76a46885376e20b0645bf8", size = 1417845, upload-time = "2025-10-13T16:13:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9b5764d0eafbbb2889288f80de773791358acf6fad1a55767538d8b79599/numexpr-2.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:44f84e0e5af219dbb62a081606156420815890e041b87252fbcea5df55214c4c", size = 1466211, upload-time = "2025-10-13T16:15:48.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/21/204db708eccd71aa8bc55bcad55bc0fc6c5a4e01ad78e14ee5714a749386/numexpr-2.14.1-cp314-cp314-win32.whl", hash = "sha256:1f1a5e817c534539351aa75d26088e9e1e0ef1b3a6ab484047618a652ccc4fc3", size = 168835, upload-time = "2025-10-13T16:17:20.82Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3e/d83e9401a1c3449a124f7d4b3fb44084798e0d30f7c11e60712d9b94cf11/numexpr-2.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:587c41509bc373dfb1fe6086ba55a73147297247bedb6d588cda69169fc412f2", size = 162608, upload-time = "2025-10-13T16:17:22.228Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d6/ec947806bb57836d6379a8c8a253c2aeaa602b12fef2336bfd2462bb4ed5/numexpr-2.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec368819502b64f190c3f71be14a304780b5935c42aae5bf22c27cc2cbba70b5", size = 163525, upload-time = "2025-10-13T16:16:50.133Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/048f30dcf661a3d52963a88c29b52b6d5ce996d38e9313a56a922451c1e0/numexpr-2.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e87f6d203ac57239de32261c941e9748f9309cbc0da6295eabd0c438b920d3a", size = 152917, upload-time = "2025-10-13T16:16:52.055Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/956a13e628d722d649fbf2fded615134a308c082e122a48bad0e90a99ce9/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd72d8c2a165fe45ea7650b16eb8cc1792a94a722022006bb97c86fe51fd2091", size = 466242, upload-time = "2025-10-13T16:13:55.795Z" }, + { url = "https://files.pythonhosted.org/packages/d6/dd/abe848678d82486940892f2cacf39e82eec790e8930d4d713d3f9191063b/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70d80fcb418a54ca208e9a38e58ddc425c07f66485176b261d9a67c7f2864f73", size = 457149, upload-time = "2025-10-13T16:15:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/fd/bb/797b583b5fb9da5700a5708ca6eb4f889c94d81abb28de4d642c0f4b3258/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:edea2f20c2040df8b54ee8ca8ebda63de9545b2112872466118e9df4d0ae99f3", size = 1426493, upload-time = "2025-10-13T16:13:59.244Z" }, + { url = "https://files.pythonhosted.org/packages/77/c4/0519ab028fdc35e3e7ee700def7f2b4631b175cd9e1202bd7966c1695c33/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:790447be6879a6c51b9545f79612d24c9ea0a41d537a84e15e6a8ddef0b6268e", size = 1474413, upload-time = "2025-10-13T16:15:59.211Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/33044878c8f4a75213cfe9c11d4c02058bb710a7a063fe14f362e8de1077/numexpr-2.14.1-cp314-cp314t-win32.whl", hash = "sha256:538961096c2300ea44240209181e31fae82759d26b51713b589332b9f2a4117e", size = 169502, upload-time = "2025-10-13T16:17:23.829Z" }, + { url = "https://files.pythonhosted.org/packages/41/a2/5a1a2c72528b429337f49911b18c302ecd36eeab00f409147e1aa4ae4519/numexpr-2.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a40b350cd45b4446076fa11843fa32bbe07024747aeddf6d467290bf9011b392", size = 163589, upload-time = "2025-10-13T16:17:25.696Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "obstore" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/54/0466176837608ddb698e0267bf362a386da6bec5a64af5291c17e40afa4e/obstore-0.9.1.tar.gz", hash = "sha256:ae213796697a1c2a9b079f9704388ef900b3780e5892516bb7e50e66aaacf239", size = 123264, upload-time = "2026-02-26T20:03:07.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/87/7667f20adfa9615910ce952a36fe60730b1b36e9e256082c8a5d49cd3064/obstore-0.9.1-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1f93405704fc846812eb05030bc15103aa787cb0e53a4f9a53c26e03f5b9d19a", size = 3757106, upload-time = "2026-02-26T20:02:25.487Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/30e23767679c38658b3eb3bb49bf07c536ff2b298de9e69dc54a496ff83c/obstore-0.9.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:82a7a99f13ab3acc80a4fe1e1798114ce3bc5cce35ab6d38ac31851f613d8d57", size = 3531491, upload-time = "2026-02-26T20:02:27.066Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/ec03f3699dc8b9af39c82739aed835cdf10671ad4d47bca2b91450d2addc/obstore-0.9.1-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8800de2607a4da4e708cd2589eb534ab663eb2b2a239f7eb55a9a35a0eba12a3", size = 3577861, upload-time = "2026-02-26T20:02:29.604Z" }, + { url = "https://files.pythonhosted.org/packages/97/58/e8225c3edc265c9958f637b0a9fa60fb6c7ab099fdf06fe5962beaf762ff/obstore-0.9.1-cp311-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d183ecd7e8805e1c0411dde949c9eae4d5f7abcfb332765e718ea16d971b255", size = 3805577, upload-time = "2026-02-26T20:02:31.324Z" }, + { url = "https://files.pythonhosted.org/packages/63/d8/32d588cb52a413183c7c754792f19922dd2d283708ae817d803ae60fb8de/obstore-0.9.1-cp311-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c3e45b507ff3ee7d064cdda4c60dfad1132c5db324a02056172c9ccfe138030", size = 4097771, upload-time = "2026-02-26T20:02:33.042Z" }, + { url = "https://files.pythonhosted.org/packages/ae/45/f48e50f669e851cf6526d34368de2ec79a4fd8083f98c2575d0092575fc8/obstore-0.9.1-cp311-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26c1ba8ffbb975d15a64ff1f0975d88a6a62305e0a591132cf81e7421be695cc", size = 3992728, upload-time = "2026-02-26T20:02:34.524Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0d/0cb65bdda8a96421a0e12fb0ff56d0ada38de15aadaff83afed24913c5ac/obstore-0.9.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6730b28f576b11be189b162bf430dabc2907465b5dbb644cce08bfa03568127f", size = 3896678, upload-time = "2026-02-26T20:02:36.174Z" }, + { url = "https://files.pythonhosted.org/packages/88/64/caa0ca79a8e3fec2f0ee0071d5d2c7d2dadc38e53fd6d3381ecb579e8334/obstore-0.9.1-cp311-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:eb05a1673d85280092a3e9c7ae1531ef3f9d73cfd265ecd710cfaf31927d5944", size = 3709980, upload-time = "2026-02-26T20:02:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/d8/3d/7482d11fecc65952f816cbdd89eb1d04b7d95526df56b952cdc45ab51250/obstore-0.9.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5ad89a85bb8e3790513dc4779a81c8a20cd78d87135aaf8f4146d7b23d4c1da1", size = 3899254, upload-time = "2026-02-26T20:02:38.87Z" }, + { url = "https://files.pythonhosted.org/packages/11/f7/26acea4fc14a99258f21ca67e4aa4d75b21da906b9374fb0f4a14ca2c820/obstore-0.9.1-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:58d5a42b6e5bf070e3718722a26bbedc9e0433c1c30bb9bf24e37100f90aba5a", size = 3817793, upload-time = "2026-02-26T20:02:40.436Z" }, + { url = "https://files.pythonhosted.org/packages/65/51/b72199f35816deea93f2c8bb1c6ccceaf360ef4ff1411cbc61f8ef937b05/obstore-0.9.1-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:070bd295787f52a33ef26cba35d8545f7449a1dadfcdf44cbe6fb795e2831e01", size = 3926980, upload-time = "2026-02-26T20:02:43.92Z" }, + { url = "https://files.pythonhosted.org/packages/34/d2/40a49f7a60d9145dbf377127007374017e32a5880a803027ad79d22218ec/obstore-0.9.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38fad190d1f8cf3e7e686b0c2960996ae694af4fc1fcdbfd4dc38a1cfbf6b931", size = 4116994, upload-time = "2026-02-26T20:02:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ac/e358b65bf9135c096425668c342f4e4c0504297e4bde01aeae863af51578/obstore-0.9.1-cp311-abi3-win_amd64.whl", hash = "sha256:9eca64f978e95065b61d0c1e81d150b73958f1a2b12a5f24acce711a01779dbc", size = 4187457, upload-time = "2026-02-26T20:02:47.221Z" }, + { url = "https://files.pythonhosted.org/packages/3f/08/643e3cf0b34f8394a131e12c15771126e1723120cd2665e697936e37140e/obstore-0.9.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:44fdf90c1467a8ad2a409d1d089bf0833855639af03526b75afb557114718f95", size = 3755119, upload-time = "2026-02-26T20:02:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/4be57315adc1e0ae027fc8e17993dbf31ef331f5cf31c2acd4a199985bc9/obstore-0.9.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:73687c0d57118258a906a49f672c86ceeebefdbf83b6cac64e7bad7617125df3", size = 3529655, upload-time = "2026-02-26T20:02:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/54/6b/54731c356e98a0827e86633bd05293961488f61802b5c0065b7253d7f1aa/obstore-0.9.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e910551ca20a080ff8c3286c078b8da8efaf8a2c4292149ea7c05d2db7b8fac1", size = 3577773, upload-time = "2026-02-26T20:02:52.247Z" }, + { url = "https://files.pythonhosted.org/packages/58/b8/2b66ac817d34d7ce537b46f9e0a436b6eec91aab07d91e496128f4079377/obstore-0.9.1-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4daa8a11d76fd3877161f2102ff00ccc89b2953dc082ecd55260be41a9c71ac3", size = 3806364, upload-time = "2026-02-26T20:02:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/7d/98/0e6c106277dbbbe601e378032441773207c1995d70af1efd543e729eb458/obstore-0.9.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e830bac44b131fd9bd6193c3e0f148905eb53baa481c44dd364c996e79c07712", size = 4093115, upload-time = "2026-02-26T20:02:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/0fff664303d7bef66a01c90c48776a245a00a06ea2c74fce5c2032133cf8/obstore-0.9.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e13e505da9410aafe99921e7a4fdbb2c57edebfd10c52d2fd9ecd9a7699f1d7", size = 3995897, upload-time = "2026-02-26T20:02:56.405Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/1854bb0c1d8c07996584f5ec28a2d25579188698f3de2a252a37c7184f38/obstore-0.9.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78c0045d888bd3ceb19eef49d1fd84cc38a596d40736bccb024069ea150cc081", size = 3895356, upload-time = "2026-02-26T20:02:57.738Z" }, + { url = "https://files.pythonhosted.org/packages/41/48/87d6c9c727ed558069d0bdba8cbdb9473cf82ad6546e64e554e2622bbb0f/obstore-0.9.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:370698816e07b0098f5aead3b2fc1bf21b5da77117fdc85d49e65de3ef8d1168", size = 3712446, upload-time = "2026-02-26T20:03:00.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/87/f9576fc09366465b8dab16fc076c7fbb6dc3aa064090c20560dc86f4e2ba/obstore-0.9.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:67db5153765e4447304622413da8504472b4ab3a5c21c6f967c2c79338b39914", size = 3902032, upload-time = "2026-02-26T20:03:02.04Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/46a84342b6aa17edc4053c771c64dee5f780efa6cd5f7b7e56fe795d5cdb/obstore-0.9.1-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:41d4d56f28a1490deed2b51bdd5af17b08ddaf366bdf726273a5a128068b0b3c", size = 3815743, upload-time = "2026-02-26T20:03:03.277Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8b/e5876892388ff6ad67610ae94e96cb17c9dbae0b3b3c247c2618701325f1/obstore-0.9.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:10916c79342b82fe8364f4d2bd96e9831362a07762ba9fa0d5f057a06bdf8db0", size = 3928147, upload-time = "2026-02-26T20:03:04.747Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/112bfe74180dd8950616f986ba9bcad6444a6bd3d3c1f863d2028635c99f/obstore-0.9.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:85fcf79453dd526943295e3897fcf4809233627ee89151ecc67cd178e61c545d", size = 4116521, upload-time = "2026-02-26T20:03:06.153Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/37/b6708e0eff5c5fb9aba2e0ea09f7f3bcbfd12a592d2a780241b5f6014df7/opentelemetry_exporter_otlp-1.40.0.tar.gz", hash = "sha256:7caa0870b95e2fcb59d64e16e2b639ecffb07771b6cd0000b5d12e5e4fef765a", size = 6152, upload-time = "2026-03-04T14:17:23.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/fc/aea77c28d9f3ffef2fdafdc3f4a235aee4091d262ddabd25882f47ce5c5f/opentelemetry_exporter_otlp-1.40.0-py3-none-any.whl", hash = "sha256:48c87e539ec9afb30dc443775a1334cc5487de2f72a770a4c00b1610bf6c697d", size = 7023, upload-time = "2026-03-04T14:17:03.612Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/37/6bf8e66bfcee5d3c6515b79cb2ee9ad05fe573c20f7ceb288d0e7eeec28c/opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7", size = 32606, upload-time = "2026-03-04T14:20:16.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/3e/f6f10f178b6316de67f0dfdbbb699a24fbe8917cf1743c1595fb9dcdd461/opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63", size = 33448, upload-time = "2026-03-04T14:19:02.447Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/3e/143cf5c034e58037307e6a24f06e0dd64b2c49ae60a965fc580027581931/opentelemetry_instrumentation_asgi-0.61b0.tar.gz", hash = "sha256:9d08e127244361dc33976d39dd4ca8f128b5aa5a7ae425208400a80a095019b5", size = 26691, upload-time = "2026-03-04T14:20:21.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/78/154470cf9d741a7487fbb5067357b87386475bbb77948a6707cae982e158/opentelemetry_instrumentation_asgi-0.61b0-py3-none-any.whl", hash = "sha256:e4b3ce6b66074e525e717efff20745434e5efd5d9df6557710856fba356da7a4", size = 16980, upload-time = "2026-03-04T14:19:10.894Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/35/aa727bb6e6ef930dcdc96a617b83748fece57b43c47d83ba8d83fbeca657/opentelemetry_instrumentation_fastapi-0.61b0.tar.gz", hash = "sha256:3a24f35b07c557ae1bbc483bf8412221f25d79a405f8b047de8b670722e2fa9f", size = 24800, upload-time = "2026-03-04T14:20:32.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/05/acfeb2cccd434242a0a7d0ea29afaf077e04b42b35b485d89aee4e0d9340/opentelemetry_instrumentation_fastapi-0.61b0-py3-none-any.whl", hash = "sha256:a1a844d846540d687d377516b2ff698b51d87c781b59f47c214359c4a241047c", size = 13485, upload-time = "2026-03-04T14:19:30.351Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/69473f925acfe2d4edf5c23bcced36906ac3627aa7c5722a8e3f60825f3b/opentelemetry_instrumentation_logging-0.61b0.tar.gz", hash = "sha256:feaa30b700acd2a37cc81db5f562ab0c3a5b6cc2453595e98b72c01dcf649584", size = 17906, upload-time = "2026-03-04T14:20:37.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/0e/2137db5239cc5e564495549a4d11488a7af9b48fc76520a0eea20e69ddae/opentelemetry_instrumentation_logging-0.61b0-py3-none-any.whl", hash = "sha256:6d87e5ded6a0128d775d41511f8380910a1b610671081d16efb05ac3711c0074", size = 17076, upload-time = "2026-03-04T14:19:36.765Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/3c/f0196223efc5c4ca19f8fad3d5462b171ac6333013335ce540c01af419e9/opentelemetry_util_http-0.61b0.tar.gz", hash = "sha256:1039cb891334ad2731affdf034d8fb8b48c239af9b6dd295e5fabd07f1c95572", size = 11361, upload-time = "2026-03-04T14:20:57.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/e5/c08aaaf2f64288d2b6ef65741d2de5454e64af3e050f34285fb1907492fe/opentelemetry_util_http-0.61b0-py3-none-any.whl", hash = "sha256:8e715e848233e9527ea47e275659ea60a57a75edf5206a3b937e236a6da5fc33", size = 9281, upload-time = "2026-03-04T14:20:08.364Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "owslib" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/03/5df0b995a3a6eefc24cc2320e3fe830d95e76ee6b0b1c6c9d0f1d4040bbe/owslib-0.35.0.tar.gz", hash = "sha256:0182f377bb30d25b78284bbaf82a12dece97902ed844cee88791ff38665b9b00", size = 194282, upload-time = "2025-10-28T15:13:42.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/12/74c145b9e273b2b479f1ea578c7778d96d0c34f2d112fd0ec96c905bb792/owslib-0.35.0-py3-none-any.whl", hash = "sha256:01648ea9b2b86502f456ad68a8dd07131d336416c5637f34adb358aafc0ad380", size = 240487, upload-time = "2025-10-28T15:13:41.399Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/07/c7087e003ceee9b9a82539b40414ec557aa795b584a1a346e89180853d79/pandas-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de09668c1bf3b925c07e5762291602f0d789eca1b3a781f99c1c78f6cac0e7ea", size = 10323380, upload-time = "2026-02-17T22:18:16.133Z" }, + { url = "https://files.pythonhosted.org/packages/c1/27/90683c7122febeefe84a56f2cde86a9f05f68d53885cebcc473298dfc33e/pandas-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24ba315ba3d6e5806063ac6eb717504e499ce30bd8c236d8693a5fd3f084c796", size = 9923455, upload-time = "2026-02-17T22:18:19.13Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f1/ed17d927f9950643bc7631aa4c99ff0cc83a37864470bc419345b656a41f/pandas-3.0.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:406ce835c55bac912f2a0dcfaf27c06d73c6b04a5dde45f1fd3169ce31337389", size = 10753464, upload-time = "2026-02-17T22:18:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7c/870c7e7daec2a6c7ff2ac9e33b23317230d4e4e954b35112759ea4a924a7/pandas-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:830994d7e1f31dd7e790045235605ab61cff6c94defc774547e8b7fdfbff3dc7", size = 11255234, upload-time = "2026-02-17T22:18:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/3653fe59af68606282b989c23d1a543ceba6e8099cbcc5f1d506a7bae2aa/pandas-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a64ce8b0f2de1d2efd2ae40b0abe7f8ae6b29fbfb3812098ed5a6f8e235ad9bf", size = 11767299, upload-time = "2026-02-17T22:18:26.824Z" }, + { url = "https://files.pythonhosted.org/packages/9b/31/1daf3c0c94a849c7a8dab8a69697b36d313b229918002ba3e409265c7888/pandas-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9832c2c69da24b602c32e0c7b1b508a03949c18ba08d4d9f1c1033426685b447", size = 12333292, upload-time = "2026-02-17T22:18:28.996Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/af63f83cd6ca603a00fe8530c10a60f0879265b8be00b5930e8e78c5b30b/pandas-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:84f0904a69e7365f79a0c77d3cdfccbfb05bf87847e3a51a41e1426b0edb9c79", size = 9892176, upload-time = "2026-02-17T22:18:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/79/ab/9c776b14ac4b7b4140788eca18468ea39894bc7340a408f1d1e379856a6b/pandas-3.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:4a68773d5a778afb31d12e34f7dd4612ab90de8c6fb1d8ffe5d4a03b955082a1", size = 9151328, upload-time = "2026-02-17T22:18:35.721Z" }, + { url = "https://files.pythonhosted.org/packages/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d", size = 10344357, upload-time = "2026-02-17T22:18:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955", size = 9884543, upload-time = "2026-02-17T22:18:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/327802e0b6d693182403c144edacbc27eb82907b57062f23ef5a4c4a5ea7/pandas-3.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b", size = 10396030, upload-time = "2026-02-17T22:18:43.822Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fe/89d77e424365280b79d99b3e1e7d606f5165af2f2ecfaf0c6d24c799d607/pandas-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4", size = 10876435, upload-time = "2026-02-17T22:18:45.954Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a6/2a75320849dd154a793f69c951db759aedb8d1dd3939eeacda9bdcfa1629/pandas-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:356e5c055ed9b0da1580d465657bc7d00635af4fd47f30afb23025352ba764d1", size = 11405133, upload-time = "2026-02-17T22:18:48.533Z" }, + { url = "https://files.pythonhosted.org/packages/58/53/1d68fafb2e02d7881df66aa53be4cd748d25cbe311f3b3c85c93ea5d30ca/pandas-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d810036895f9ad6345b8f2a338dd6998a74e8483847403582cab67745bff821", size = 11932065, upload-time = "2026-02-17T22:18:50.837Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/67cc404b3a966b6df27b38370ddd96b3b023030b572283d035181854aac5/pandas-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43", size = 9741627, upload-time = "2026-02-17T22:18:53.905Z" }, + { url = "https://files.pythonhosted.org/packages/86/4f/caf9952948fb00d23795f09b893d11f1cacb384e666854d87249530f7cbe/pandas-3.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f463ebfd8de7f326d38037c7363c6dacb857c5881ab8961fb387804d6daf2f7", size = 9052483, upload-time = "2026-02-17T22:18:57.31Z" }, + { url = "https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262", size = 10317509, upload-time = "2026-02-17T22:18:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56", size = 9860561, upload-time = "2026-02-17T22:19:02.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/f01ff54664b6d70fed71475543d108a9b7c888e923ad210795bef04ffb7d/pandas-3.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e", size = 10365506, upload-time = "2026-02-17T22:19:05.017Z" }, + { url = "https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791", size = 10873196, upload-time = "2026-02-17T22:19:07.204Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/9301c83d0b47c23ac5deab91c6b39fd98d5b5db4d93b25df8d381451828f/pandas-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a", size = 11370859, upload-time = "2026-02-17T22:19:09.436Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/0c1fc5bd2d29c7db2ab372330063ad555fb83e08422829c785f5ec2176ca/pandas-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8", size = 11924584, upload-time = "2026-02-17T22:19:11.562Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25", size = 9742769, upload-time = "2026-02-17T22:19:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cb/810a22a6af9a4e97c8ab1c946b47f3489c5bca5adc483ce0ffc84c9cc768/pandas-3.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59", size = 9043855, upload-time = "2026-02-17T22:19:16.09Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/423c89086cca1f039cf1253c3ff5b90f157b5b3757314aa635f6bf3e30aa/pandas-3.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06", size = 10752673, upload-time = "2026-02-17T22:19:18.304Z" }, + { url = "https://files.pythonhosted.org/packages/22/23/b5a08ec1f40020397f0faba72f1e2c11f7596a6169c7b3e800abff0e433f/pandas-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f", size = 10404967, upload-time = "2026-02-17T22:19:20.726Z" }, + { url = "https://files.pythonhosted.org/packages/5c/81/94841f1bb4afdc2b52a99daa895ac2c61600bb72e26525ecc9543d453ebc/pandas-3.0.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324", size = 10320575, upload-time = "2026-02-17T22:19:24.919Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/2ae37d66a5342a83adadfd0cb0b4bf9c3c7925424dd5f40d15d6cfaa35ee/pandas-3.0.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9", size = 10710921, upload-time = "2026-02-17T22:19:27.181Z" }, + { url = "https://files.pythonhosted.org/packages/a2/61/772b2e2757855e232b7ccf7cb8079a5711becb3a97f291c953def15a833f/pandas-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76", size = 11334191, upload-time = "2026-02-17T22:19:29.411Z" }, + { url = "https://files.pythonhosted.org/packages/1b/08/b16c6df3ef555d8495d1d265a7963b65be166785d28f06a350913a4fac78/pandas-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098", size = 11782256, upload-time = "2026-02-17T22:19:32.34Z" }, + { url = "https://files.pythonhosted.org/packages/55/80/178af0594890dee17e239fca96d3d8670ba0f5ff59b7d0439850924a9c09/pandas-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35", size = 10485047, upload-time = "2026-02-17T22:19:34.605Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" }, + { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" }, + { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" }, + { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" }, + { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" }, + { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" }, + { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" }, + { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + +[[package]] +name = "pathlib-abc" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/cb/448649d7f25d228bf0be3a04590ab7afa77f15e056f8fa976ed05ec9a78f/pathlib_abc-0.5.2.tar.gz", hash = "sha256:fcd56f147234645e2c59c7ae22808b34c364bb231f685ddd9f96885aed78a94c", size = 33342, upload-time = "2025-10-10T18:37:20.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/29/c028a0731e202035f0e2e0bfbf1a3e46ad6c628cbb17f6f1cc9eea5d9ff1/pathlib_abc-0.5.2-py3-none-any.whl", hash = "sha256:4c9d94cf1b23af417ce7c0417b43333b06a106c01000b286c99de230d95eefbb", size = 19070, upload-time = "2025-10-10T18:37:19.437Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/02/8832cde80e7380c600fbf55090b6ab7b62bd6825dbedde6d6657c15a1f8e/proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", size = 56929, upload-time = "2026-02-02T17:34:49.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc", size = 50480, upload-time = "2026-02-02T17:34:47.339Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyproj" +version = "3.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz", hash = "sha256:39a0cf1ecc7e282d1d30f36594ebd55c9fae1fda8a2622cee5d100430628f88c", size = 226279, upload-time = "2025-08-14T12:05:42.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/bd/f205552cd1713b08f93b09e39a3ec99edef0b3ebbbca67b486fdf1abe2de/pyproj-3.7.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:2514d61f24c4e0bb9913e2c51487ecdaeca5f8748d8313c933693416ca41d4d5", size = 6227022, upload-time = "2025-08-14T12:03:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/75/4c/9a937e659b8b418ab573c6d340d27e68716928953273e0837e7922fcac34/pyproj-3.7.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8693ca3892d82e70de077701ee76dd13d7bca4ae1c9d1e739d72004df015923a", size = 4625810, upload-time = "2025-08-14T12:03:53.808Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/a9f41e814dc4d1dc54e95b2ccaf0b3ebe3eb18b1740df05fe334724c3d89/pyproj-3.7.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5e26484d80fea56273ed1555abaea161e9661d81a6c07815d54b8e883d4ceb25", size = 9638694, upload-time = "2025-08-14T12:03:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ab/9bdb4a6216b712a1f9aab1c0fcbee5d3726f34a366f29c3e8c08a78d6b70/pyproj-3.7.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:281cb92847814e8018010c48b4069ff858a30236638631c1a91dd7bfa68f8a8a", size = 9493977, upload-time = "2025-08-14T12:03:57.937Z" }, + { url = "https://files.pythonhosted.org/packages/c9/db/2db75b1b6190f1137b1c4e8ef6a22e1c338e46320f6329bfac819143e063/pyproj-3.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9c8577f0b7bb09118ec2e57e3babdc977127dd66326d6c5d755c76b063e6d9dc", size = 10841151, upload-time = "2025-08-14T12:04:00.271Z" }, + { url = "https://files.pythonhosted.org/packages/89/f7/989643394ba23a286e9b7b3f09981496172f9e0d4512457ffea7dc47ffc7/pyproj-3.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a23f59904fac3a5e7364b3aa44d288234af267ca041adb2c2b14a903cd5d3ac5", size = 10751585, upload-time = "2025-08-14T12:04:02.228Z" }, + { url = "https://files.pythonhosted.org/packages/53/6d/ad928fe975a6c14a093c92e6a319ca18f479f3336bb353a740bdba335681/pyproj-3.7.2-cp311-cp311-win32.whl", hash = "sha256:f2af4ed34b2cf3e031a2d85b067a3ecbd38df073c567e04b52fa7a0202afde8a", size = 5908533, upload-time = "2025-08-14T12:04:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/79/e0/b95584605cec9ed50b7ebaf7975d1c4ddeec5a86b7a20554ed8b60042bd7/pyproj-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:0b7cb633565129677b2a183c4d807c727d1c736fcb0568a12299383056e67433", size = 6320742, upload-time = "2025-08-14T12:04:06.357Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/536e8f93bca808175c2d0a5ac9fdf69b960d8ab6b14f25030dccb07464d7/pyproj-3.7.2-cp311-cp311-win_arm64.whl", hash = "sha256:38b08d85e3a38e455625b80e9eb9f78027c8e2649a21dec4df1f9c3525460c71", size = 6245772, upload-time = "2025-08-14T12:04:08.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ab/9893ea9fb066be70ed9074ae543914a618c131ed8dff2da1e08b3a4df4db/pyproj-3.7.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:0a9bb26a6356fb5b033433a6d1b4542158fb71e3c51de49b4c318a1dff3aeaab", size = 6219832, upload-time = "2025-08-14T12:04:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/53/78/4c64199146eed7184eb0e85bedec60a4aa8853b6ffe1ab1f3a8b962e70a0/pyproj-3.7.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:567caa03021178861fad27fabde87500ec6d2ee173dd32f3e2d9871e40eebd68", size = 4620650, upload-time = "2025-08-14T12:04:11.978Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ac/14a78d17943898a93ef4f8c6a9d4169911c994e3161e54a7cedeba9d8dde/pyproj-3.7.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c203101d1dc3c038a56cff0447acc515dd29d6e14811406ac539c21eed422b2a", size = 9667087, upload-time = "2025-08-14T12:04:13.964Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/212882c450bba74fc8d7d35cbd57e4af84792f0a56194819d98106b075af/pyproj-3.7.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1edc34266c0c23ced85f95a1ee8b47c9035eae6aca5b6b340327250e8e281630", size = 9552797, upload-time = "2025-08-14T12:04:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/c0f25c87b5d2a8686341c53c1792a222a480d6c9caf60311fec12c99ec26/pyproj-3.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa9f26c21bc0e2dc3d224cb1eb4020cf23e76af179a7c66fea49b828611e4260", size = 10837036, upload-time = "2025-08-14T12:04:18.733Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/5cbd6772addde2090c91113332623a86e8c7d583eccb2ad02ea634c4a89f/pyproj-3.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9428b318530625cb389b9ddc9c51251e172808a4af79b82809376daaeabe5e9", size = 10775952, upload-time = "2025-08-14T12:04:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/a1/dc250e3cf83eb4b3b9a2cf86fdb5e25288bd40037ae449695550f9e96b2f/pyproj-3.7.2-cp312-cp312-win32.whl", hash = "sha256:b3d99ed57d319da042f175f4554fc7038aa4bcecc4ac89e217e350346b742c9d", size = 5898872, upload-time = "2025-08-14T12:04:22.485Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a6/6fe724b72b70f2b00152d77282e14964d60ab092ec225e67c196c9b463e5/pyproj-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:11614a054cd86a2ed968a657d00987a86eeb91fdcbd9ad3310478685dc14a128", size = 6312176, upload-time = "2025-08-14T12:04:24.736Z" }, + { url = "https://files.pythonhosted.org/packages/5d/68/915cc32c02a91e76d02c8f55d5a138d6ef9e47a0d96d259df98f4842e558/pyproj-3.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:509a146d1398bafe4f53273398c3bb0b4732535065fa995270e52a9d3676bca3", size = 6233452, upload-time = "2025-08-14T12:04:27.287Z" }, + { url = "https://files.pythonhosted.org/packages/be/14/faf1b90d267cea68d7e70662e7f88cefdb1bc890bd596c74b959e0517a72/pyproj-3.7.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:19466e529b1b15eeefdf8ff26b06fa745856c044f2f77bf0edbae94078c1dfa1", size = 6214580, upload-time = "2025-08-14T12:04:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/da9a45b184d375f62667f62eba0ca68569b0bd980a0bb7ffcc1d50440520/pyproj-3.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c79b9b84c4a626c5dc324c0d666be0bfcebd99f7538d66e8898c2444221b3da7", size = 4615388, upload-time = "2025-08-14T12:04:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e7/d2b459a4a64bca328b712c1b544e109df88e5c800f7c143cfbc404d39bfb/pyproj-3.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ceecf374cacca317bc09e165db38ac548ee3cad07c3609442bd70311c59c21aa", size = 9628455, upload-time = "2025-08-14T12:04:32.435Z" }, + { url = "https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681", size = 9514269, upload-time = "2025-08-14T12:04:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/07a9b89ae7467872f9a476883a5bad9e4f4d1219d31060f0f2b282276cbe/pyproj-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f000841e98ea99acbb7b8ca168d67773b0191de95187228a16110245c5d954d5", size = 10808437, upload-time = "2025-08-14T12:04:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/fda1daeabbd39dec5b07f67233d09f31facb762587b498e6fc4572be9837/pyproj-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8115faf2597f281a42ab608ceac346b4eb1383d3b45ab474fd37341c4bf82a67", size = 10745540, upload-time = "2025-08-14T12:04:38.568Z" }, + { url = "https://files.pythonhosted.org/packages/0d/90/c793182cbba65a39a11db2ac6b479fe76c59e6509ae75e5744c344a0da9d/pyproj-3.7.2-cp313-cp313-win32.whl", hash = "sha256:f18c0579dd6be00b970cb1a6719197fceecc407515bab37da0066f0184aafdf3", size = 5896506, upload-time = "2025-08-14T12:04:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/be/0f/747974129cf0d800906f81cd25efd098c96509026e454d4b66868779ab04/pyproj-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb41c29d5f60854b1075853fe80c58950b398d4ebb404eb532536ac8d2834ed7", size = 6310195, upload-time = "2025-08-14T12:04:42.974Z" }, + { url = "https://files.pythonhosted.org/packages/82/64/fc7598a53172c4931ec6edf5228280663063150625d3f6423b4c20f9daff/pyproj-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:2b617d573be4118c11cd96b8891a0b7f65778fa7733ed8ecdb297a447d439100", size = 6230748, upload-time = "2025-08-14T12:04:44.491Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f0/611dd5cddb0d277f94b7af12981f56e1441bf8d22695065d4f0df5218498/pyproj-3.7.2-cp313-cp313t-macosx_13_0_x86_64.whl", hash = "sha256:d27b48f0e81beeaa2b4d60c516c3a1cfbb0c7ff6ef71256d8e9c07792f735279", size = 6241729, upload-time = "2025-08-14T12:04:46.274Z" }, + { url = "https://files.pythonhosted.org/packages/15/93/40bd4a6c523ff9965e480870611aed7eda5aa2c6128c6537345a2b77b542/pyproj-3.7.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:55a3610d75023c7b1c6e583e48ef8f62918e85a2ae81300569d9f104d6684bb6", size = 4652497, upload-time = "2025-08-14T12:04:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/7150ead53c117880b35e0d37960d3138fe640a235feb9605cb9386f50bb0/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8d7349182fa622696787cc9e195508d2a41a64765da9b8a6bee846702b9e6220", size = 9942610, upload-time = "2025-08-14T12:04:49.652Z" }, + { url = "https://files.pythonhosted.org/packages/d8/17/7a4a7eafecf2b46ab64e5c08176c20ceb5844b503eaa551bf12ccac77322/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:d230b186eb876ed4f29a7c5ee310144c3a0e44e89e55f65fb3607e13f6db337c", size = 9692390, upload-time = "2025-08-14T12:04:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/c3/55/ae18f040f6410f0ea547a21ada7ef3e26e6c82befa125b303b02759c0e9d/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:237499c7862c578d0369e2b8ac56eec550e391a025ff70e2af8417139dabb41c", size = 11047596, upload-time = "2025-08-14T12:04:53.748Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/d3fff4d2909473f26ae799f9dda04caa322c417a51ff3b25763f7d03b233/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8c225f5978abd506fd9a78eaaf794435e823c9156091cabaab5374efb29d7f69", size = 10896975, upload-time = "2025-08-14T12:04:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bc/8fc7d3963d87057b7b51ebe68c1e7c51c23129eee5072ba6b86558544a46/pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2", size = 5953057, upload-time = "2025-08-14T12:04:58.466Z" }, + { url = "https://files.pythonhosted.org/packages/cc/27/ea9809966cc47d2d51e6d5ae631ea895f7c7c7b9b3c29718f900a8f7d197/pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3", size = 6375414, upload-time = "2025-08-14T12:04:59.861Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/1ef0129fba9a555c658e22af68989f35e7ba7b9136f25758809efec0cd6e/pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd", size = 6262501, upload-time = "2025-08-14T12:05:01.39Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02", size = 6214541, upload-time = "2025-08-14T12:05:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/03/68/68ada9c8aea96ded09a66cfd9bf87aa6db8c2edebe93f5bf9b66b0143fbc/pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08", size = 4617456, upload-time = "2025-08-14T12:05:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b", size = 9615590, upload-time = "2025-08-14T12:05:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/05/1e/ada6fb15a1d75b5bd9b554355a69a798c55a7dcc93b8d41596265c1772e3/pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281", size = 9474960, upload-time = "2025-08-14T12:05:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/51/07/9d48ad0a8db36e16f842f2c8a694c1d9d7dcf9137264846bef77585a71f3/pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516", size = 10799478, upload-time = "2025-08-14T12:05:14.102Z" }, + { url = "https://files.pythonhosted.org/packages/85/cf/2f812b529079f72f51ff2d6456b7fef06c01735e5cfd62d54ffb2b548028/pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e", size = 10710030, upload-time = "2025-08-14T12:05:16.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/9b/4626a19e1f03eba4c0e77b91a6cf0f73aa9cb5d51a22ee385c22812bcc2c/pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25", size = 5991181, upload-time = "2025-08-14T12:05:19.492Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112", size = 6434721, upload-time = "2025-08-14T12:05:21.022Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/6c910ea2e1c74ef673c5d48c482564b8a7824a44c4e35cca2e765b68cfcc/pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6", size = 6363821, upload-time = "2025-08-14T12:05:22.627Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/5532f6f7491812ba782a2177fe9de73fd8e2912b59f46a1d056b84b9b8f2/pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37", size = 6241773, upload-time = "2025-08-14T12:05:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/0938c3f2bbbef1789132d1726d9b0e662f10cfc22522743937f421ad664e/pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b", size = 4652537, upload-time = "2025-08-14T12:05:26.391Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/488b1ed47d25972f33874f91f09ca8f2227902f05f63a2b80dc73e7b1c97/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357", size = 9940864, upload-time = "2025-08-14T12:05:27.985Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/7f4c895d0cb98e47b6a85a6d79eaca03eb266129eed2f845125c09cf31ff/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81", size = 9688868, upload-time = "2025-08-14T12:05:30.425Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/c7e306b8bb0f071d9825b753ee4920f066c40fbfcce9372c4f3cfb2fc4ed/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888", size = 11045910, upload-time = "2025-08-14T12:05:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/42/fb/538a4d2df695980e2dde5c04d965fbdd1fe8c20a3194dc4aaa3952a4d1be/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59", size = 10895724, upload-time = "2025-08-14T12:05:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8b/a3f0618b03957de9db5489a04558a8826f43906628bb0b766033aa3b5548/pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa", size = 6056848, upload-time = "2025-08-14T12:05:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/bc/56/413240dd5149dd3291eda55aa55a659da4431244a2fd1319d0ae89407cfb/pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c", size = 6517676, upload-time = "2025-08-14T12:05:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" }, +] + +[[package]] +name = "pystac" +version = "1.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/e6/efbc20dbc94ad7ed18fe11a4208103a509384ffcccd9bdc27953b725e686/pystac-1.14.3.tar.gz", hash = "sha256:24f92d6f301371859aa0abc1bbe7b1523a603e1184a6d139ecb323967c2c9bb3", size = 164205, upload-time = "2026-01-09T12:38:42.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/b4/a9430e72bfc3c458e1fcf8363890994e483052ab052ed93912be4e5b32c8/pystac-1.14.3-py3-none-any.whl", hash = "sha256:2f60005f521d541fb801428307098f223c14697b3faf4d2f0209afb6a43f39e5", size = 208506, upload-time = "2026-01-09T12:38:40.721Z" }, +] + +[package.optional-dependencies] +validation = [ + { name = "jsonschema" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/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, upload-time = "2024-03-01T18:36:20.211Z" } +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, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pywinpty" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/54/37c7370ba91f579235049dc26cd2c5e657d2a943e01820844ffc81f32176/pywinpty-3.0.3.tar.gz", hash = "sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412", size = 31309, upload-time = "2026-02-04T21:51:09.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/c3/3e75075c7f71735f22b66fab0481f2c98e3a4d58cba55cb50ba29114bcf6/pywinpty-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:dff25a9a6435f527d7c65608a7e62783fc12076e7d44487a4911ee91be5a8ac8", size = 2114430, upload-time = "2026-02-04T21:54:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1e/8a54166a8c5e4f5cb516514bdf4090be4d51a71e8d9f6d98c0aa00fe45d4/pywinpty-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc1e230e5b193eef4431cba3f39996a288f9958f9c9f092c8a961d930ee8f68", size = 236191, upload-time = "2026-02-04T21:50:36.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d4/aeb5e1784d2c5bff6e189138a9ca91a090117459cea0c30378e1f2db3d54/pywinpty-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c9081df0e49ffa86d15db4a6ba61530630e48707f987df42c9d3313537e81fc0", size = 2113098, upload-time = "2026-02-04T21:54:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/b9/53/7278223c493ccfe4883239cf06c823c56460a8010e0fc778eef67858dc14/pywinpty-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:15e79d870e18b678fb8a5a6105fd38496b55697c66e6fc0378236026bc4d59e9", size = 234901, upload-time = "2026-02-04T21:53:31.35Z" }, + { url = "https://files.pythonhosted.org/packages/e5/cb/58d6ed3fd429c96a90ef01ac9a617af10a6d41469219c25e7dc162abbb71/pywinpty-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9c91dbb026050c77bdcef964e63a4f10f01a639113c4d3658332614544c467ab", size = 2112686, upload-time = "2026-02-04T21:52:03.035Z" }, + { url = "https://files.pythonhosted.org/packages/fd/50/724ed5c38c504d4e58a88a072776a1e880d970789deaeb2b9f7bd9a5141a/pywinpty-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:fe1f7911805127c94cf51f89ab14096c6f91ffdcacf993d2da6082b2142a2523", size = 234591, upload-time = "2026-02-04T21:52:29.821Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ad/90a110538696b12b39fd8758a06d70ded899308198ad2305ac68e361126e/pywinpty-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:3f07a6cf1c1d470d284e614733c3d0f726d2c85e78508ea10a403140c3c0c18a", size = 2112360, upload-time = "2026-02-04T21:55:33.397Z" }, + { url = "https://files.pythonhosted.org/packages/44/0f/7ffa221757a220402bc79fda44044c3f2cc57338d878ab7d622add6f4581/pywinpty-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:15c7c0b6f8e9d87aabbaff76468dabf6e6121332c40fc1d83548d02a9d6a3759", size = 233107, upload-time = "2026-02-04T21:51:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/28/88/2ff917caff61e55f38bcdb27de06ee30597881b2cae44fbba7627be015c4/pywinpty-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:d4b6b7b0fe0cdcd02e956bd57cfe9f4e5a06514eecf3b5ae174da4f951b58be9", size = 2113282, upload-time = "2026-02-04T21:52:08.188Z" }, + { url = "https://files.pythonhosted.org/packages/63/32/40a775343ace542cc43ece3f1d1fce454021521ecac41c4c4573081c2336/pywinpty-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:34789d685fc0d547ce0c8a65e5a70e56f77d732fa6e03c8f74fefb8cbb252019", size = 234207, upload-time = "2026-02-04T21:51:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/8d/54/5d5e52f4cb75028104ca6faf36c10f9692389b1986d34471663b4ebebd6d/pywinpty-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0c37e224a47a971d1a6e08649a1714dac4f63c11920780977829ed5c8cadead1", size = 2112910, upload-time = "2026-02-04T21:52:30.976Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/dcd184824e21d4620b06c7db9fbb15c3ad0a0f1fa2e6de79969fb82647ec/pywinpty-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c4e9c3dff7d86ba81937438d5819f19f385a39d8f592d4e8af67148ceb4f6ab5", size = 233425, upload-time = "2026-02-04T21:51:56.754Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "rasterio" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "affine", marker = "python_full_version < '3.12'" }, + { name = "attrs", marker = "python_full_version < '3.12'" }, + { name = "certifi", marker = "python_full_version < '3.12'" }, + { name = "click", marker = "python_full_version < '3.12'" }, + { name = "click-plugins", marker = "python_full_version < '3.12'" }, + { name = "cligj", marker = "python_full_version < '3.12'" }, + { name = "numpy", marker = "python_full_version < '3.12'" }, + { name = "pyparsing", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/fa/fce8dc9f09e5bc6520b6fc1b4ecfa510af9ca06eb42ad7bdff9c9b8989d0/rasterio-1.4.4.tar.gz", hash = "sha256:c95424e2c7f009b8f7df1095d645c52895cd332c0c2e1b4c2e073ea28b930320", size = 445004, upload-time = "2025-12-12T18:01:08.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/0d/d3859e49ab94464de2623fec82c6798d8d7c8bea2473cd2696fc5e09f717/rasterio-1.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:b8eea428b5f0c78a963f6003a19b60777df83a0aba8c28231d65431e32ac160e", size = 21144125, upload-time = "2025-12-12T17:58:59.511Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3c/97ba4b146309cdc0e36f289b02ac69465b026a21afc828e4e4e1dc39466a/rasterio-1.4.4-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:1cc0ea5aa0d22f5f349aa221674481de689b7b3a99607ce6bb58a29e5be54d17", size = 25746406, upload-time = "2025-12-12T17:59:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/ce/33/75f81bd837ac2336b24456fdb249597a4b9af2a212b7151f64d09022be36/rasterio-1.4.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7eb25b23666b29dadfc49a59206cead62c99190584b61771bba0e95f7da06801", size = 34587242, upload-time = "2025-12-12T17:59:05.848Z" }, + { url = "https://files.pythonhosted.org/packages/f9/77/3869a426f6e752dde13f3868cdf16253ca0214f92107db79c1583c9aa07b/rasterio-1.4.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e24b7b8c2df801dde2a1dffb44c58902bd76b5cab740dc11de4ff9963992a71a", size = 35881871, upload-time = "2025-12-12T17:59:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/66/d0/3818859ddbd3750d0ef5a6580a3272e81764286d943c689dd41e49b8b786/rasterio-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:0718630f607be2f5742d8e4b34b434746fd788a192d77eefc9bb924399fea802", size = 25716477, upload-time = "2025-12-12T17:59:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/02/039eb4970c93aaef4c9eb1ee159abad18e6e7f932c2eed575c95f78d94f6/rasterio-1.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:0308ff4762ae9eb40a991f12d758626b59af4376b13675480391dd7295d17bbf", size = 24075993, upload-time = "2025-12-12T17:59:16.407Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fc/63d89ddfcb4643730553683ee322566b9b15fe56d026e4c21c4f4f5d9d26/rasterio-1.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3c4f0cbd188f893011f2a0a6dc2852b3892799b3a0d79eddf92f2b115ec7ed7", size = 21120715, upload-time = "2025-12-12T17:59:19.35Z" }, + { url = "https://files.pythonhosted.org/packages/43/70/2c003f76a23dbb078fdee35c8e2ec490d2ad8982f4dc956ba08b56027b87/rasterio-1.4.4-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:6fce26090b9f509eab337228420145947c491a13628965410f25bc3e6e05cf75", size = 25732944, upload-time = "2025-12-12T17:59:22.533Z" }, + { url = "https://files.pythonhosted.org/packages/f6/cc/4a8e92362c0ff496dd1007c3dcba66e9ededf1a45eca8ad1db302b071c49/rasterio-1.4.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c1c722da390dc264aeccdc0dc200ca37923875d910ca4cd5bec0fec351bb818e", size = 34295209, upload-time = "2025-12-12T17:59:26.035Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/717d2dec47fbefad33ca0d27bd5f0d543b1d1bc9fcab5ef82a13adaaf38d/rasterio-1.4.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98b6dfb8282b2a54b9d75c3dc8d2520a69bbc66916c7d43de8e0bbf6e0240ca1", size = 35661866, upload-time = "2025-12-12T17:59:29.928Z" }, + { url = "https://files.pythonhosted.org/packages/ed/60/ae3351fba2726ec0976974ce2eb030c159edd3363b8771e832b8db571c24/rasterio-1.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:9513f4c7a6d93b45098f8dff2421fa9516604e3bfbf35aa144484a88d36a321f", size = 25682853, upload-time = "2025-12-12T17:59:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/38/ee/35387296bbacfc5cbbb4273228b1b959793d3ce38b0402a07f11a248420b/rasterio-1.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:60b49a482e0f12f12ce9d2cc3090add02f89f3d422e85f2cffaa9207adb83c04", size = 24043249, upload-time = "2025-12-12T17:59:39.915Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fe/e3e37041c49956f4f4cbe473c3fe290aaba96ed20e9c07da304e0cad2015/rasterio-1.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:df26c96aa81ffbd0b33189680859211eadf9950123c21579f84de73bb0f91d81", size = 21107336, upload-time = "2025-12-12T17:59:43.585Z" }, + { url = "https://files.pythonhosted.org/packages/f3/02/c217fdcc8e80a4b7d1b1bc4529d78f98452816e9add53ff8742049a77ae7/rasterio-1.4.4-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:b3af0ecc922a80f3755516629f7948e37bade9077b5f5c12a3869a5e7f01619b", size = 25719929, upload-time = "2025-12-12T17:59:47.64Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d0/7f177f37bc9595d809dabb0073abd0c42358469f6b10875192b46331c652/rasterio-1.4.4-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7ce3b0f9a22e95a27790087908753973644d7c3877d495ec9bd6e04a25233ca4", size = 34198845, upload-time = "2025-12-12T17:59:52.405Z" }, + { url = "https://files.pythonhosted.org/packages/7b/84/66c0d9cca2a09074ec2ce6fffa87709ca51b0d197ae742d835e841bac660/rasterio-1.4.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c072450caa96428b1218b030500bb908fd6f09bc013a88969ff81a124b6a112a", size = 35576074, upload-time = "2025-12-12T17:59:56.392Z" }, + { url = "https://files.pythonhosted.org/packages/32/68/f7df5478458ace2fa50be43e9fab1a39957a0e71afaa3e6147ec289e0fc8/rasterio-1.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ee92ef10c0ba89f45f9c2b40fca9f971f357385f04ee9b716fb09cbd9ce20c", size = 25680573, upload-time = "2025-12-12T18:00:00.45Z" }, + { url = "https://files.pythonhosted.org/packages/34/e5/1bdaccb658430dfd391ad4a63d206546f36639d7e4130bf31f125c6525b4/rasterio-1.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:65c10afe64b5e488185aaff0b659e08eda22c89285b54a3e433b80e6c6621770", size = 24040367, upload-time = "2025-12-12T18:00:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/32/76/54643a7d1d650fd7f1acea9093c298603e4c01bba6f90be2254310b48507/rasterio-1.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:18c2c1130e789dc2771d0aa5ec4b56d5b8a0097c648ccb94882d5ff3ab55c928", size = 21247203, upload-time = "2025-12-12T18:00:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/ef/434b4849ccd6a3e03a0b1ac37c963c1771564945745613d15c5d96ce768d/rasterio-1.4.4-cp313-cp313t-macosx_15_0_x86_64.whl", hash = "sha256:2d1654b7ffa6f3dde42c5fd27159ae45148c11e352de26f12fe7313a3236aeed", size = 25822050, upload-time = "2025-12-12T18:00:11.081Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fa/fe9a478aa0cde246da58baeb0df3248c7ca174e4d9c9b27e81b504e40a76/rasterio-1.4.4-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c4022cbddb659856e120603b12233cec8913ae760fff220657ce888c3c6b9f9d", size = 34833783, upload-time = "2025-12-12T18:00:14.525Z" }, + { url = "https://files.pythonhosted.org/packages/04/cd/ed4716590dbcd4b8ae633417d758564e510bee4d6aaac5050a0f6d5179c5/rasterio-1.4.4-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:96b88880551a07b7a3b50439483cefbd9af91a09e19ff2b736815994e5671314", size = 35738114, upload-time = "2025-12-12T18:00:17.96Z" }, + { url = "https://files.pythonhosted.org/packages/7e/29/da7050d11ba1d041e0333ac14768e6e9ca1aa2b9fa8416f317d2650ed276/rasterio-1.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:def75d486d0ab8f306f918a913c425ed57159495518c54efe8e18d5164d37d90", size = 25896835, upload-time = "2025-12-12T18:00:21.411Z" }, + { url = "https://files.pythonhosted.org/packages/88/80/304dbe5434c4aa8dfaf90480c16d770161796a6a61fa88e72e8a402153df/rasterio-1.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:770b7e86f6c565e6f9cf30f6fa4479a5a2bab4e10ff44fe7acfd518ca4a71d1b", size = 24128074, upload-time = "2025-12-12T18:00:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/03/01/d5a3dc51cd5fef62b76ecc77d33c1ca20de305fed7e16c71bcdf4858e466/rasterio-1.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:019693f14a83ae9225cb57c16e466901d0e6284962dcf13a9f4bb1175b979011", size = 21120237, upload-time = "2025-12-12T18:00:27.723Z" }, + { url = "https://files.pythonhosted.org/packages/50/da/db18362602b17327c0e00c9e9c0847c1c4ac657c1a289169ca06a26faccb/rasterio-1.4.4-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:87d7c3e97e3b40c9041d1602e2dcb4fc2d88abe6c645fccb4939dec297a91cf8", size = 25720506, upload-time = "2025-12-12T18:00:30.592Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8f/a15d66c9c05bffb176c9707ef1f2bfcf9c0b835272937c80ac7207a20b5c/rasterio-1.4.4-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a2401e4c43a31c7382154d4042b60a63b9bca5886802983c5c9362cdc5b09548", size = 34153931, upload-time = "2025-12-12T18:00:33.852Z" }, + { url = "https://files.pythonhosted.org/packages/05/2d/cd778286b910db7a3f0bc1743ca362173f1fbb7365137e4982ca857b6d26/rasterio-1.4.4-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6c4287d8934d953f7870b8e2a1df1096fbf47eba39ad0f777a31ea500f4e5010", size = 35421139, upload-time = "2025-12-12T18:00:37.482Z" }, + { url = "https://files.pythonhosted.org/packages/70/97/13a2e33aede8d7a42178c696a6a93868d1f9560f73de05033a1675f0806a/rasterio-1.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:c3ba1871549221140661227dd4fa1f9a472ded4a6d2f2c2e367b0648bb15b99d", size = 26419132, upload-time = "2025-12-12T18:00:40.871Z" }, + { url = "https://files.pythonhosted.org/packages/27/d8/2dcfcb362d6a2fd07c14cfb803a345a7926d4d9fb6243e196df105671e97/rasterio-1.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:7c9d7dc824cb8d222808be153643cd4e65ea3e1f66019ada1ccd630221edfe30", size = 24800998, upload-time = "2025-12-12T18:00:45.332Z" }, + { url = "https://files.pythonhosted.org/packages/13/f8/16e9b648e7f16cadb41df7c0116dbab26b4a2ba02c85cbe3f744065bdf56/rasterio-1.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:98e17bded830a59992d9f8f8d9f227ce1c4be0694930afcc4360358f5cb1a5db", size = 21247046, upload-time = "2025-12-12T18:00:49.429Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ea/f3dc3a25d7591821d488f5c5eb89f6abcd1f5c8e2ef4bd2792f965cbc9c8/rasterio-1.4.4-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:56134ca203f952855e60774b06672033cf65057eb9810fcc5c1a75f1921053a3", size = 25821677, upload-time = "2025-12-12T18:00:52.458Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d3/1e038350218e852f904c8dc4ab751aa023a2e82e68998767b7b42e33832c/rasterio-1.4.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:52edde65515b33fe4314c8a44a9ee2fc00b550deed6d56e1a8d085d42bbca3e6", size = 34829572, upload-time = "2025-12-12T18:00:56.294Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ce/28abf7a5f5d9cb014c2e14cc396bebe953b3deefbf604d49f4322e73fa35/rasterio-1.4.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d61d3f2c171c64050bd75e54a5d964ff7f165b3f5d2b92c9ee09b9716aa1b8bf", size = 35735171, upload-time = "2025-12-12T18:00:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/54/91/1ce35cfda2d56dacd6395faf20a5290268bd9009c53393ac42b5f9bb2c4c/rasterio-1.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:40137fe512c0d6e96c0167a0ae4e56d82c488f244163c45494b7392e51c844de", size = 26700712, upload-time = "2025-12-12T18:01:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/3b/33/4d13f48a8f01d782ffc1eece20821586518f3f515dca7cf152bca9fd22d4/rasterio-1.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:29ec3a794454b5bb255c9c0374cc380030a8a1e295c81eee7feb036802d2a9e3", size = 24875933, upload-time = "2025-12-12T18:01:06.134Z" }, +] + +[[package]] +name = "rasterio" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "affine", marker = "python_full_version >= '3.12'" }, + { name = "attrs", marker = "python_full_version >= '3.12'" }, + { name = "certifi", marker = "python_full_version >= '3.12'" }, + { name = "click", marker = "python_full_version >= '3.12'" }, + { name = "cligj", marker = "python_full_version >= '3.12'" }, + { name = "numpy", marker = "python_full_version >= '3.12'" }, + { name = "pyparsing", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/88/edb4b66b6cb2c13f123af5a3896bf70c0cbe73ab3cd4243cb4eb0212a0f6/rasterio-1.5.0.tar.gz", hash = "sha256:1e0ea56b02eea4989b36edf8e58a5a3ef40e1b7edcb04def2603accd5ab3ee7b", size = 452184, upload-time = "2026-01-05T16:06:47.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/de/ba1cd11d7d1182bfb26e758bf07016d04e5442f4f5fea35b0d7279b72399/rasterio-1.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:420656074897a460f5ef46f657b3061d2e004f9d99e613914b0671643e69d92c", size = 22787192, upload-time = "2026-01-05T16:05:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/e6/42/efaeb6dc531dbcd02fec01c791a853bb5a139a5126ecec579ac0f735eeb9/rasterio-1.5.0-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:c5c3597a783857e760550e8f26365d928b0377ac5ffc3e12ba447ac65ca5406d", size = 24412221, upload-time = "2026-01-05T16:05:22.526Z" }, + { url = "https://files.pythonhosted.org/packages/a2/14/89645988424c40cbcb8334f94305ffe094dd28d85c643341d9690704c9f0/rasterio-1.5.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e14d07a09833b6df6024ce7a57aee1e1977b3aec682e30b1e58ce773462f2382", size = 36128020, upload-time = "2026-01-05T16:05:25.556Z" }, + { url = "https://files.pythonhosted.org/packages/85/23/5a52319a98451ff910f42e5f7f4804bfb39f9327933a89daab685d1ce2dd/rasterio-1.5.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:26dbcffcf0d01fc121cbb92186bc1cb78e16efe62b17be45ad7494446b325cf8", size = 37634010, upload-time = "2026-01-05T16:05:28.673Z" }, + { url = "https://files.pythonhosted.org/packages/57/d6/fe8826f813c98b046d8d4c3bc83053c89c71f367f89257d211fe5dd0b0ba/rasterio-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac8d04eee66ca8060763ead607800e5611d857dd005905d920365e24a16ba20a", size = 30142328, upload-time = "2026-01-05T16:05:31.357Z" }, + { url = "https://files.pythonhosted.org/packages/af/62/6397379271d5628ed65ef781bf2d3a8f56094a86e6d8479c6ca506a1b960/rasterio-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:31f1edc45c781ebd087e60cc00a4fc37028dd3fe25cff4098e4139fc9d0565be", size = 28500710, upload-time = "2026-01-05T16:05:33.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/87/42865a77cebf2e524d27b6afc71db48984799ecd1dbe6a213d4713f42f5f/rasterio-1.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e7b25b0a19975ccd511e507e6de45b0a2d8fb6802abe49bb726cf48588e34833", size = 22776107, upload-time = "2026-01-05T16:05:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/6a/53/e81683fbbfdf04e019e68b042d9cff8524b0571aa80e4f4d81c373c31a49/rasterio-1.5.0-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:1162c18eaece9f6d2aa1c2ff6b373b99651d93f113f24120a991eaebf28aa4f4", size = 24401477, upload-time = "2026-01-05T16:05:39.702Z" }, + { url = "https://files.pythonhosted.org/packages/bc/3c/6aa6e0690b18eea02a61739cb362a47c5df66138f0a02cc69e1181b964e5/rasterio-1.5.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:8eb87fd6f843eea109f3df9bef83f741b053b716b0465932276e2c0577dfb929", size = 36018214, upload-time = "2026-01-05T16:05:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/48/4a/1af9aa9810fb30668568f2c4dd3eec2412c8e9762b69201d971c509b295e/rasterio-1.5.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:08a7580cbb9b3bd320bdf827e10c9b2424d0df066d8eef6f2feb37e154ce0c17", size = 37544972, upload-time = "2026-01-05T16:05:45.815Z" }, + { url = "https://files.pythonhosted.org/packages/01/62/bfe3408743c9837919ff232474a09ece9eaa88d4ee8c040711fa3dff6dad/rasterio-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:d7d6729c0739b5ec48c33686668a30e27f5bdb361093f180ee7818ff19665547", size = 30140141, upload-time = "2026-01-05T16:05:48.751Z" }, + { url = "https://files.pythonhosted.org/packages/63/ca/e90e19a6d065a718cc3d468a12b9f015289ad17017656dea8c76f7318d1f/rasterio-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:8af7c368c22f0a99d1259ccc5a5cd96c432c2bde6f132c1ac78508cd7445a745", size = 28498556, upload-time = "2026-01-05T16:05:51.334Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ba/e37462d8c33bbbd6c152a0390ec6911a3d9614ded3d2bc6f6a48e147e833/rasterio-1.5.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b4ccfcc8ed9400e4f14efdf2005533fcf72048748b727f85ff89b9291ecdf98a", size = 22920107, upload-time = "2026-01-05T16:05:53.773Z" }, + { url = "https://files.pythonhosted.org/packages/66/dc/7bfa9cf96ac39b451b2f94dfc584c223ec584c52c148df2e4bab60c3341b/rasterio-1.5.0-cp313-cp313t-macosx_15_0_x86_64.whl", hash = "sha256:2f57c36ca4d3c896f7024226bd71eeb5cd10c8183c2a94508534d78cc05ff9e7", size = 24508993, upload-time = "2026-01-05T16:05:57.062Z" }, + { url = "https://files.pythonhosted.org/packages/e5/55/7293743f3b69de4b726c67b8dc9da01fc194070b6becc51add4ca8a20a27/rasterio-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cc1395475e4bb7032cd81dda4d5558061c4c7d5a50b1b5e146bdf9716d0b9353", size = 36565784, upload-time = "2026-01-05T16:06:00.019Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ef/5354c47de16c6e289728c3a3d6961ffcf7a9ad6313aef7e8db5d6a40c46e/rasterio-1.5.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:592a485e2057b1aaeab4f843c9897628e60e3ff45e2509325c3e1479116599cb", size = 37686456, upload-time = "2026-01-05T16:06:02.772Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fc/fe1f034b1acd1900d9fbd616826d001a3d5811f1d0c97c785f88f525853e/rasterio-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0c739e70a72fb080f039ee1570c5d02b974dde32ded1a3216e1f13fe38ac4844", size = 30355842, upload-time = "2026-01-05T16:06:06.359Z" }, + { url = "https://files.pythonhosted.org/packages/e0/cb/4dee9697891c9c6474b240d00e27688e03ecd882d3c83cc97eb25c2266ff/rasterio-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:a3539a2f401a7b4b2e94ff2db334878c0e15a2d1c9fe90bb0879c52f89367ae5", size = 28589538, upload-time = "2026-01-05T16:06:09.662Z" }, + { url = "https://files.pythonhosted.org/packages/77/9f/f84dfa54110c1c82f9f4fd929465d12519569b6f5d015273aa0957013b2e/rasterio-1.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:597be8df418d5ba7b6a927b6b9febfcb42b192882448a8d5b2e2e75a1296631f", size = 22788832, upload-time = "2026-01-05T16:06:12.247Z" }, + { url = "https://files.pythonhosted.org/packages/20/f1/de55255c918b17afd7292f793a3500c4aea7e9530b2b3f5b3a57836c7d49/rasterio-1.5.0-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:dd292030d39d685c0b35eddef233e7f1cb8b43052578a3ec97a2da57799693be", size = 24405917, upload-time = "2026-01-05T16:06:14.603Z" }, + { url = "https://files.pythonhosted.org/packages/a9/57/054087a9d5011ad5dfa799277ba8814e41775e1967d37a59ab7b8e2f1876/rasterio-1.5.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:62c3f97a3c72643c74f2d0f310621a09c35c0c412229c327ae6bcc1ee4b9c3bc", size = 35987536, upload-time = "2026-01-05T16:06:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/c9/72/5fbe5f67ae75d7e89ffb718c500d5fecbaa84f6ba354db306de689faf961/rasterio-1.5.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:19577f0f0c5f1158af47b57f73356961cbd1782a5f6ae6f3adf6f2650f4eb369", size = 37408048, upload-time = "2026-01-05T16:06:20.82Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/0c4ef19980204bdcbc8f9e084056adebc97916ff4edcc718750ef34e5bf9/rasterio-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:015c1ab6e5453312c5e29692752e7ad73568fe4d13567cbd448d7893128cbd2d", size = 30949590, upload-time = "2026-01-05T16:06:23.425Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d8/2e6b81505408926c00e629d7d3d73fd0454213201bd9907450e0fe82f3dd/rasterio-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:ff677c0a9d3ba667c067227ef2b76872488b37ff29b061bc3e576fad9baa3286", size = 29337287, upload-time = "2026-01-05T16:06:26.599Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/7b6e6afb28d4e3f69f2229f990ed87dfdc21a3e15ca63b96b2fd9ba17d89/rasterio-1.5.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:508251b9c746d8d008771a30c2160ff321bfc3b41f6a1aa8e8ef1dd4a00d97ba", size = 22926149, upload-time = "2026-01-05T16:06:29.617Z" }, + { url = "https://files.pythonhosted.org/packages/24/30/19345d8bc7d2b96c1172594026b9009702e9ab9f0baf07079d3612aaadae/rasterio-1.5.0-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:742841ed48bc70f6ef517b8fa3521f231780bf408fde0aa6d73770337a36374e", size = 24516040, upload-time = "2026-01-05T16:06:32.964Z" }, + { url = "https://files.pythonhosted.org/packages/9e/43/dc7a4518fa78904bc41952cbf346c3c2a88a20e61b479154058392914c0b/rasterio-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c9a9eee49ce9410c2f352b34c370bb3a96bb518b6a7f97b3a72ee4c835fd4b5c", size = 36589519, upload-time = "2026-01-05T16:06:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/8f706083c6c163054d12c7ed6d5ac4e4ed02252b761288d74e6158871b34/rasterio-1.5.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b9fd87a0b63ab5c6267dfb0bc96f54fdf49d000651b9ee85ed37798141cff046", size = 37714599, upload-time = "2026-01-05T16:06:38.818Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d5/bbca726d5fea5864f7e4bcf3ee893095369e93ad51120495e8c40e2aa1a0/rasterio-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f459db8953ba30ca04fcef2b5e1260eeeff0eae8158bd9c3d6adbe56289765cc", size = 31233931, upload-time = "2026-01-05T16:06:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d1/8b017856e63ccaff3cbd0e82490dbb01363a42f3a462a41b1d8a391e1443/rasterio-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f4b9c2c3b5f10469eb9588f105086e68f0279e62cc9095c4edd245e3f9b88c8a", size = 29418321, upload-time = "2026-01-05T16:06:44.758Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "rio-cogeo" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "morecantile" }, + { name = "pydantic" }, + { name = "rasterio", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "rasterio", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/87/0040337ae4a5adb8c52f29aa3ff7d6e1648685ed1a4d9be6fe77cb75904b/rio_cogeo-7.0.1.tar.gz", hash = "sha256:3bfe448e221579e8b2ee569114c8c5cc4bdabe00f99fe5dc10782e75eba31dd0", size = 19274, upload-time = "2025-12-15T14:22:36.701Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/f8/fc54603209c05aadbf35c5f5a63d209e27ed37e0045c17c44d4b0a4689e1/rio_cogeo-7.0.1-py3-none-any.whl", hash = "sha256:10ca7a5d23e185db597438ebb51d1c2c30928e75c635800e834a5c14241134e2", size = 21827, upload-time = "2025-12-15T14:22:35.556Z" }, +] + +[[package]] +name = "rio-stac" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac" }, + { name = "rasterio", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "rasterio", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/32/fdb3dc918fdb3d4385190e47950f6accb82a6c75dca5d6e939f83234070d/rio_stac-0.12.0.tar.gz", hash = "sha256:02cf7ff4e2228752277cfd08cca26d4f4f7253ded3b49fb1db3ea207050d7cda", size = 13972, upload-time = "2025-09-17T08:35:05.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/3c/0219fc20bd63ebce3ad4ea4cc4764d31aa2ad94677cd3b0fa660f40c77da/rio_stac-0.12.0-py3-none-any.whl", hash = "sha256:4453281c30a3a644f45fd71281b49a48aa8bc29daa185a50854ce1822df9a8a3", size = 11765, upload-time = "2025-09-17T08:35:04.451Z" }, +] + +[[package]] +name = "rio-tiler" +version = "9.0.0rc2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cachetools" }, + { name = "color-operations" }, + { name = "httpx" }, + { name = "morecantile" }, + { name = "numexpr" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "pystac" }, + { name = "rasterio", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "rasterio", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/88/89806fc203b1ecff3a0f6a0cce60b22711ea62bc601ea7785dc49d3f5d44/rio_tiler-9.0.0rc2.tar.gz", hash = "sha256:9e953a7b30591776de0d45066172bca934fd1a87ea6d2dbf48b61ef62ea7ae9d", size = 190574, upload-time = "2026-03-06T13:38:14.507Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/9f/4a6db514455f6a864760247c659add75c9413cff326745de4a8140b52471/rio_tiler-9.0.0rc2-py3-none-any.whl", hash = "sha256:77f9e3eca065b9eadef111239ce1848e6ce422ebbdbe57e26b0824353203d210", size = 286918, upload-time = "2026-03-06T13:38:13.071Z" }, +] + +[[package]] +name = "rioxarray" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version < '3.12'" }, + { name = "pyproj", marker = "python_full_version < '3.12'" }, + { name = "rasterio", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "xarray", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/8e/fe4e87460f8c62d8d5c683e09f19fbde5d9cfcfd0342d02df1f452999b5d/rioxarray-0.19.0.tar.gz", hash = "sha256:7819a0036fd874c8c8e280447cbbe43d8dc72fc4a14ac7852a665b1bdb7d4b04", size = 54600, upload-time = "2025-04-21T17:46:54.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2f/63d2cacc0e525f8e3398bcf32bd3620385f22cd1600834ec49d7f3597a7b/rioxarray-0.19.0-py3-none-any.whl", hash = "sha256:494ee4fff1781072d55ee5276f5d07b63d93b05093cb33b926a12186ba5bb8ef", size = 62151, upload-time = "2025-04-21T17:46:52.801Z" }, +] + +[[package]] +name = "rioxarray" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pyproj", marker = "python_full_version >= '3.12'" }, + { name = "rasterio", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "xarray", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/ad/d9f7a6d197a44a2c8f53174bdea919b7df3c70ef5c14a13702888516609a/rioxarray-0.20.0.tar.gz", hash = "sha256:8bfc7e979edc7e30b4671d638a9be0e5a7d673dab2ea88e2445d3c7745599c02", size = 55038, upload-time = "2025-10-24T18:14:41.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/e5/4f4fc949e7eb8415a57091767969e1d314dcf06b74b85bbbf29991395af4/rioxarray-0.20.0-py3-none-any.whl", hash = "sha256:197b0638146dfc6093ef52f8bf8afb42757ca16bc2e0d87b6282ce54170c9799", size = 62690, upload-time = "2025-10-24T18:14:40.73Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "s3fs" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiobotocore" }, + { name = "aiohttp" }, + { name = "fsspec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/be/392c8c5e0da9bfa139e41084690dd49a5e3e931099f78f52d3f6070105c6/s3fs-2026.2.0.tar.gz", hash = "sha256:91cb2a9f76e35643b76eeac3f47a6165172bb3def671f76b9111c8dd5779a2ac", size = 84152, upload-time = "2026-02-05T21:57:57.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/e1/64c264db50b68de8a438b60ceeb921b2f22da3ebb7ad6255150225d0beac/s3fs-2026.2.0-py3-none-any.whl", hash = "sha256:65198835b86b1d5771112b0085d1da52a6ede36508b1aaa6cae2aedc765dfe10", size = 31328, upload-time = "2026-02-05T21:57:56.532Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + +[[package]] +name = "send2trash" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, + { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, + { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[package]] +name = "simplejson" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649", size = 85784, upload-time = "2025-09-26T16:29:36.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/3e/96898c6c66d9dca3f9bd14d7487bf783b4acc77471b42f979babbb68d4ca/simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f", size = 92633, upload-time = "2025-09-26T16:27:45.028Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a2/cd2e10b880368305d89dd540685b8bdcc136df2b3c76b5ddd72596254539/simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8", size = 75309, upload-time = "2025-09-26T16:27:46.142Z" }, + { url = "https://files.pythonhosted.org/packages/5d/02/290f7282eaa6ebe945d35c47e6534348af97472446951dce0d144e013f4c/simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a", size = 75308, upload-time = "2025-09-26T16:27:47.542Z" }, + { url = "https://files.pythonhosted.org/packages/43/91/43695f17b69e70c4b0b03247aa47fb3989d338a70c4b726bbdc2da184160/simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51", size = 143733, upload-time = "2025-09-26T16:27:48.673Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4b/fdcaf444ac1c3cbf1c52bf00320c499e1cf05d373a58a3731ae627ba5e2d/simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd", size = 153397, upload-time = "2025-09-26T16:27:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/c4/83/21550f81a50cd03599f048a2d588ffb7f4c4d8064ae091511e8e5848eeaa/simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013", size = 141654, upload-time = "2025-09-26T16:27:51.168Z" }, + { url = "https://files.pythonhosted.org/packages/cf/54/d76c0e72ad02450a3e723b65b04f49001d0e73218ef6a220b158a64639cb/simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81", size = 144913, upload-time = "2025-09-26T16:27:52.331Z" }, + { url = "https://files.pythonhosted.org/packages/3f/49/976f59b42a6956d4aeb075ada16ad64448a985704bc69cd427a2245ce835/simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa", size = 144568, upload-time = "2025-09-26T16:27:53.41Z" }, + { url = "https://files.pythonhosted.org/packages/60/c7/30bae30424ace8cd791ca660fed454ed9479233810fe25c3f3eab3d9dc7b/simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb", size = 146239, upload-time = "2025-09-26T16:27:54.502Z" }, + { url = "https://files.pythonhosted.org/packages/79/3e/7f3b7b97351c53746e7b996fcd106986cda1954ab556fd665314756618d2/simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2", size = 154497, upload-time = "2025-09-26T16:27:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/1d/48/7241daa91d0bf19126589f6a8dcbe8287f4ed3d734e76fd4a092708947be/simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413", size = 148069, upload-time = "2025-09-26T16:27:57.039Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f4/ef18d2962fe53e7be5123d3784e623859eec7ed97060c9c8536c69d34836/simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961", size = 74158, upload-time = "2025-09-26T16:27:58.265Z" }, + { url = "https://files.pythonhosted.org/packages/35/fd/3d1158ecdc573fdad81bf3cc78df04522bf3959758bba6597ba4c956c74d/simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c", size = 75911, upload-time = "2025-09-26T16:27:59.292Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9e/1a91e7614db0416885eab4136d49b7303de20528860ffdd798ce04d054db/simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6", size = 93523, upload-time = "2025-09-26T16:28:00.356Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2b/d2413f5218fc25608739e3d63fe321dfa85c5f097aa6648dbe72513a5f12/simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259", size = 75844, upload-time = "2025-09-26T16:28:01.756Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f1/efd09efcc1e26629e120fef59be059ce7841cc6e1f949a4db94f1ae8a918/simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8", size = 75655, upload-time = "2025-09-26T16:28:03.037Z" }, + { url = "https://files.pythonhosted.org/packages/97/ec/5c6db08e42f380f005d03944be1af1a6bd501cc641175429a1cbe7fb23b9/simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9", size = 150335, upload-time = "2025-09-26T16:28:05.027Z" }, + { url = "https://files.pythonhosted.org/packages/81/f5/808a907485876a9242ec67054da7cbebefe0ee1522ef1c0be3bfc90f96f6/simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868", size = 158519, upload-time = "2025-09-26T16:28:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/66/af/b8a158246834645ea890c36136584b0cc1c0e4b83a73b11ebd9c2a12877c/simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b", size = 148571, upload-time = "2025-09-26T16:28:07.715Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/ed9b2571bbf38f1a2425391f18e3ac11cb1e91482c22d644a1640dea9da7/simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e", size = 152367, upload-time = "2025-09-26T16:28:08.921Z" }, + { url = "https://files.pythonhosted.org/packages/81/2c/bad68b05dd43e93f77994b920505634d31ed239418eb6a88997d06599983/simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c", size = 150205, upload-time = "2025-09-26T16:28:10.086Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/90c7fc878061adafcf298ce60cecdee17a027486e9dce507e87396d68255/simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970", size = 151823, upload-time = "2025-09-26T16:28:11.329Z" }, + { url = "https://files.pythonhosted.org/packages/ab/27/b85b03349f825ae0f5d4f780cdde0bbccd4f06c3d8433f6a3882df887481/simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e", size = 158997, upload-time = "2025-09-26T16:28:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/71/ad/d7f3c331fb930638420ac6d236db68e9f4c28dab9c03164c3cd0e7967e15/simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544", size = 154367, upload-time = "2025-09-26T16:28:14.393Z" }, + { url = "https://files.pythonhosted.org/packages/f0/46/5c67324addd40fa2966f6e886cacbbe0407c03a500db94fb8bb40333fcdf/simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54", size = 74285, upload-time = "2025-09-26T16:28:15.931Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/5cc2189f4acd3a6e30ffa9775bf09b354302dbebab713ca914d7134d0f29/simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab", size = 75969, upload-time = "2025-09-26T16:28:17.017Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9e/f326d43f6bf47f4e7704a4426c36e044c6bedfd24e072fb8e27589a373a5/simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86", size = 93530, upload-time = "2025-09-26T16:28:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/35/28/5a4b8f3483fbfb68f3f460bc002cef3a5735ef30950e7c4adce9c8da15c7/simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74", size = 75846, upload-time = "2025-09-26T16:28:19.12Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4d/30dfef83b9ac48afae1cf1ab19c2867e27b8d22b5d9f8ca7ce5a0a157d8c/simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726", size = 75661, upload-time = "2025-09-26T16:28:20.219Z" }, + { url = "https://files.pythonhosted.org/packages/09/1d/171009bd35c7099d72ef6afd4bb13527bab469965c968a17d69a203d62a6/simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5", size = 150579, upload-time = "2025-09-26T16:28:21.337Z" }, + { url = "https://files.pythonhosted.org/packages/61/ae/229bbcf90a702adc6bfa476e9f0a37e21d8c58e1059043038797cbe75b8c/simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d", size = 158797, upload-time = "2025-09-26T16:28:22.53Z" }, + { url = "https://files.pythonhosted.org/packages/90/c5/fefc0ac6b86b9108e302e0af1cf57518f46da0baedd60a12170791d56959/simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0", size = 148851, upload-time = "2025-09-26T16:28:23.733Z" }, + { url = "https://files.pythonhosted.org/packages/43/f1/b392952200f3393bb06fbc4dd975fc63a6843261705839355560b7264eb2/simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b", size = 152598, upload-time = "2025-09-26T16:28:24.962Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b4/d6b7279e52a3e9c0fa8c032ce6164e593e8d9cf390698ee981ed0864291b/simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f", size = 150498, upload-time = "2025-09-26T16:28:26.114Z" }, + { url = "https://files.pythonhosted.org/packages/62/22/ec2490dd859224326d10c2fac1353e8ad5c84121be4837a6dd6638ba4345/simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522", size = 152129, upload-time = "2025-09-26T16:28:27.552Z" }, + { url = "https://files.pythonhosted.org/packages/33/ce/b60214d013e93dd9e5a705dcb2b88b6c72bada442a97f79828332217f3eb/simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3", size = 159359, upload-time = "2025-09-26T16:28:28.667Z" }, + { url = "https://files.pythonhosted.org/packages/99/21/603709455827cdf5b9d83abe726343f542491ca8dc6a2528eb08de0cf034/simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769", size = 154717, upload-time = "2025-09-26T16:28:30.288Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f9/dc7f7a4bac16cf7eb55a4df03ad93190e11826d2a8950052949d3dfc11e2/simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661", size = 74289, upload-time = "2025-09-26T16:28:31.809Z" }, + { url = "https://files.pythonhosted.org/packages/87/10/d42ad61230436735c68af1120622b28a782877146a83d714da7b6a2a1c4e/simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608", size = 75972, upload-time = "2025-09-26T16:28:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017", size = 57309, upload-time = "2025-09-26T16:29:35.312Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "starlette-cramjam" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cramjam" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/f8/ffcc61b1448f9ecb00134a27bdae2e156c2180fdf5f8de765657d9e1bcf1/starlette_cramjam-0.5.1.tar.gz", hash = "sha256:538fe9fad7a58af97ac32517fb0981d9462490cbaf332f73648005dd0a14eaef", size = 8038, upload-time = "2025-10-27T13:33:03.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/c8/3ad3cc9e2c431234fb940f31486f7ce3ebf3fe248d6d50a1e338ed2ecccf/starlette_cramjam-0.5.1-py3-none-any.whl", hash = "sha256:d3488b5648c7b3263c6b7d0f41d7856af6d6566c0cbd851f165838db0692c324", size = 7458, upload-time = "2025-10-27T13:33:02.902Z" }, +] + +[[package]] +name = "supermorecado" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "morecantile" }, + { name = "rasterio", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "rasterio", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/cc/97693e7c34e56a9f3c869ff10a962e14d38c48b2c56436f0483ee77c3cc4/supermorecado-0.1.2.tar.gz", hash = "sha256:b51664d2eb12326e657a9d80c6849857c42ef3876545515ef988e266e6cc9654", size = 12171, upload-time = "2023-09-11T14:40:15.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/25/909292b8a012282d3b535e51c2626095f0ff0cfea796da637b34512fb9f6/supermorecado-0.1.2-py3-none-any.whl", hash = "sha256:5116b9ff8c8aa0b0da235cb5962449dc878515c8155adf1440268c3cf271bed6", size = 14486, upload-time = "2023-09-11T14:40:13.349Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "titiler" +version = "2.0.0b2" +source = { virtual = "." } +dependencies = [ + { name = "titiler-application" }, + { name = "titiler-core" }, + { name = "titiler-extensions" }, + { name = "titiler-mosaic", extra = ["mosaicjson"] }, + { name = "titiler-xarray" }, +] + +[package.dev-dependencies] +dev = [ + { name = "aiohttp" }, + { name = "boto3" }, + { name = "brotlipy" }, + { name = "folium" }, + { name = "fsspec" }, + { name = "h5netcdf" }, + { name = "h5py" }, + { name = "httpx" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "jupyter" }, + { name = "matplotlib" }, + { name = "obstore" }, + { name = "owslib" }, + { name = "pre-commit" }, + { name = "pystac", extra = ["validation"] }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "requests" }, + { name = "s3fs" }, + { name = "zarr" }, +] +docs = [ + { name = "black" }, + { name = "griffe-inherited-docstrings" }, + { name = "mkdocs" }, + { name = "mkdocs-jupyter" }, + { name = "mkdocs-material", extra = ["imaging"] }, + { name = "mkdocstrings", extra = ["python"] }, +] +server = [ + { name = "uvicorn" }, +] +telemetry = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-logging" }, + { name = "opentelemetry-sdk" }, +] + +[package.metadata] +requires-dist = [ + { name = "titiler-application", editable = "src/titiler/application" }, + { name = "titiler-core", editable = "src/titiler/core" }, + { name = "titiler-extensions", editable = "src/titiler/extensions" }, + { name = "titiler-mosaic", extras = ["mosaicjson"], editable = "src/titiler/mosaic" }, + { name = "titiler-xarray", editable = "src/titiler/xarray" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "aiohttp" }, + { name = "boto3" }, + { name = "brotlipy" }, + { name = "folium" }, + { name = "fsspec" }, + { name = "h5netcdf" }, + { name = "h5py" }, + { name = "httpx" }, + { name = "ipython" }, + { name = "jupyter" }, + { name = "matplotlib" }, + { name = "obstore" }, + { name = "owslib" }, + { name = "pre-commit" }, + { name = "pystac", extras = ["validation"], specifier = ">=1.0.0,<2.0.0" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "requests" }, + { name = "s3fs", specifier = ">=2025.2.0" }, + { name = "zarr", specifier = ">=3,<4.0" }, +] +docs = [ + { name = "black", specifier = ">=23.10.1" }, + { name = "griffe-inherited-docstrings", specifier = ">=1.0.0" }, + { name = "mkdocs", specifier = ">=1.4.3" }, + { name = "mkdocs-jupyter", specifier = ">=0.24.5" }, + { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.5" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.25.1" }, +] +server = [{ name = "uvicorn" }] +telemetry = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-logging" }, + { name = "opentelemetry-sdk" }, +] + +[[package]] +name = "titiler-application" +source = { editable = "src/titiler/application" } +dependencies = [ + { name = "pydantic-settings" }, + { name = "starlette-cramjam" }, + { name = "titiler-core", extra = ["telemetry"] }, + { name = "titiler-extensions", extra = ["cogeo", "stac"] }, + { name = "titiler-mosaic", extra = ["mosaicjson"] }, + { name = "titiler-xarray" }, +] + +[package.optional-dependencies] +server = [ + { name = "uvicorn", extra = ["standard"] }, +] + +[package.dev-dependencies] +test = [ + { name = "boto3" }, + { name = "brotlipy" }, + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic-settings", specifier = "~=2.0" }, + { name = "starlette-cramjam", specifier = ">=0.4,<0.6" }, + { name = "titiler-core", extras = ["telemetry"], editable = "src/titiler/core" }, + { name = "titiler-extensions", extras = ["cogeo", "stac"], editable = "src/titiler/extensions" }, + { name = "titiler-mosaic", extras = ["mosaicjson"], editable = "src/titiler/mosaic" }, + { name = "titiler-xarray", editable = "src/titiler/xarray" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.12.0" }, +] +provides-extras = ["server"] + +[package.metadata.requires-dev] +test = [ + { name = "boto3" }, + { name = "brotlipy" }, + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + +[[package]] +name = "titiler-core" +source = { editable = "src/titiler/core" } +dependencies = [ + { name = "fastapi" }, + { name = "geojson-pydantic" }, + { name = "jinja2" }, + { name = "morecantile" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "rasterio", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "rasterio", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "rio-tiler" }, + { name = "simplejson" }, +] + +[package.optional-dependencies] +telemetry = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-logging" }, + { name = "opentelemetry-sdk" }, +] + +[package.dev-dependencies] +test = [ + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.108.0" }, + { name = "geojson-pydantic", specifier = ">=1.1.2,<3.0" }, + { name = "jinja2", specifier = ">=2.11.2,<4.0.0" }, + { name = "morecantile" }, + { name = "numpy" }, + { name = "opentelemetry-api", marker = "extra == 'telemetry'" }, + { name = "opentelemetry-exporter-otlp", marker = "extra == 'telemetry'" }, + { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'telemetry'" }, + { name = "opentelemetry-instrumentation-logging", marker = "extra == 'telemetry'" }, + { name = "opentelemetry-sdk", marker = "extra == 'telemetry'" }, + { name = "pydantic", specifier = "~=2.0" }, + { name = "rasterio" }, + { name = "rio-tiler", specifier = ">=9.0.0rc2,<10.0" }, + { name = "simplejson" }, +] +provides-extras = ["telemetry"] + +[package.metadata.requires-dev] +test = [ + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + +[[package]] +name = "titiler-extensions" +source = { editable = "src/titiler/extensions" } +dependencies = [ + { name = "titiler-core" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] + +[package.optional-dependencies] +cogeo = [ + { name = "rio-cogeo" }, +] +stac = [ + { name = "rio-stac" }, +] + +[package.dev-dependencies] +test = [ + { name = "httpx" }, + { name = "owslib" }, + { name = "pystac", extra = ["validation"] }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "rio-cogeo", marker = "extra == 'cogeo'", specifier = ">=7.0,<8.0" }, + { name = "rio-stac", marker = "extra == 'stac'", specifier = ">=0.12,<0.13" }, + { name = "titiler-core", editable = "src/titiler/core" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +provides-extras = ["cogeo", "stac"] + +[package.metadata.requires-dev] +test = [ + { name = "httpx" }, + { name = "owslib" }, + { name = "pystac", extras = ["validation"], specifier = ">=1.0.0,<2.0.0" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + +[[package]] +name = "titiler-mosaic" +source = { editable = "src/titiler/mosaic" } +dependencies = [ + { name = "titiler-core" }, +] + +[package.optional-dependencies] +mosaicjson = [ + { name = "cogeo-mosaic" }, +] + +[package.dev-dependencies] +test = [ + { name = "httpx" }, + { name = "owslib" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "cogeo-mosaic", marker = "extra == 'mosaicjson'", specifier = ">=9.0,<10.0" }, + { name = "titiler-core", editable = "src/titiler/core" }, +] +provides-extras = ["mosaicjson"] + +[package.metadata.requires-dev] +test = [ + { name = "httpx" }, + { name = "owslib" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + +[[package]] +name = "titiler-xarray" +source = { editable = "src/titiler/xarray" } +dependencies = [ + { name = "obstore" }, + { name = "rioxarray", version = "0.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "rioxarray", version = "0.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "titiler-core" }, + { name = "xarray" }, + { name = "zarr" }, +] + +[package.optional-dependencies] +fs = [ + { name = "aiohttp" }, + { name = "fsspec" }, + { name = "gcsfs" }, + { name = "h5netcdf" }, + { name = "h5py" }, + { name = "requests" }, + { name = "s3fs" }, +] +telemetry = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-logging" }, + { name = "opentelemetry-sdk" }, +] + +[package.dev-dependencies] +test = [ + { name = "aiohttp" }, + { name = "fsspec" }, + { name = "h5netcdf" }, + { name = "h5py" }, + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "requests" }, + { name = "s3fs" }, +] +upstream = [ + { name = "numcodecs" }, + { name = "ujson" }, + { name = "universal-pathlib" }, + { name = "xarray" }, + { name = "zarr" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", marker = "extra == 'fs'" }, + { name = "fsspec", marker = "extra == 'fs'" }, + { name = "gcsfs", marker = "extra == 'fs'" }, + { name = "h5netcdf", marker = "extra == 'fs'" }, + { name = "h5py", marker = "extra == 'fs'" }, + { name = "obstore" }, + { name = "opentelemetry-api", marker = "extra == 'telemetry'" }, + { name = "opentelemetry-exporter-otlp", marker = "extra == 'telemetry'" }, + { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'telemetry'" }, + { name = "opentelemetry-instrumentation-logging", marker = "extra == 'telemetry'" }, + { name = "opentelemetry-sdk", marker = "extra == 'telemetry'" }, + { name = "requests", marker = "extra == 'fs'" }, + { name = "rioxarray" }, + { name = "s3fs", marker = "extra == 'fs'", specifier = ">=2025.2.0" }, + { name = "titiler-core", editable = "src/titiler/core" }, + { name = "xarray" }, + { name = "zarr", specifier = ">=3.1,<4.0" }, +] +provides-extras = ["fs", "telemetry"] + +[package.metadata.requires-dev] +test = [ + { name = "aiohttp" }, + { name = "fsspec" }, + { name = "h5netcdf" }, + { name = "h5py" }, + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "requests" }, + { name = "s3fs" }, +] +upstream = [ + { name = "numcodecs", git = "https://github.com/zarr-developers/numcodecs" }, + { name = "ujson", git = "https://github.com/ultrajson/ultrajson" }, + { name = "universal-pathlib", git = "https://github.com/fsspec/universal_pathlib" }, + { name = "xarray", git = "https://github.com/pydata/xarray" }, + { name = "zarr", git = "https://github.com/zarr-developers/zarr-python" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "ujson" +version = "5.11.1.dev24" +source = { git = "https://github.com/ultrajson/ultrajson#a465ed7654bb13fd283681e0e7928e4c11ee2cf4" } + +[[package]] +name = "universal-pathlib" +version = "0.3.10.post2+g827574045" +source = { git = "https://github.com/fsspec/universal_pathlib#827574045b1fecad52f7af3cfeeab3ecd9a0d0e8" } +dependencies = [ + { name = "fsspec" }, + { name = "pathlib-abc" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { 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, upload-time = "2024-11-01T14:06:31.756Z" }, + { 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, upload-time = "2024-11-01T14:06:32.99Z" }, + { 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, upload-time = "2024-11-01T14:06:34.963Z" }, + { 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, upload-time = "2024-11-01T14:06:37.745Z" }, + { 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, upload-time = "2024-11-01T14:06:39.748Z" }, + { 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, upload-time = "2024-11-01T14:06:41.009Z" }, + { 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, upload-time = "2024-11-01T14:06:42.952Z" }, + { 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, upload-time = "2024-11-01T14:06:45.084Z" }, + { 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, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "webcolors" +version = "25.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "xarray" +version = "2026.2.1.dev19+g37f2d49b5" +source = { git = "https://github.com/pydata/xarray#37f2d49b5cfbf5ca7e24dbad6347f99f5a24a368" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, +] + +[[package]] +name = "xyzservices" +version = "2025.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/0f/022795fc1201e7c29e742a509913badb53ce0b38f64b6db859e2f6339da9/xyzservices-2025.11.0.tar.gz", hash = "sha256:2fc72b49502b25023fd71e8f532fb4beddbbf0aa124d90ea25dba44f545e17ce", size = 1135703, upload-time = "2025-11-22T11:31:51.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/5c/2c189d18d495dd0fa3f27ccc60762bbc787eed95b9b0147266e72bb76585/xyzservices-2025.11.0-py3-none-any.whl", hash = "sha256:de66a7599a8d6dad63980b77defd1d8f5a5a9cb5fc8774ea1c6e89ca7c2a3d2f", size = 93916, upload-time = "2025-11-22T11:31:50.525Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[package]] +name = "zarr" +version = "3.1.6.dev54+gca53f8e5b" +source = { git = "https://github.com/zarr-developers/zarr-python#ca53f8e5bc002797ed531a1c49c162189f65f9d5" } +dependencies = [ + { name = "donfig" }, + { name = "google-crc32c" }, + { name = "numcodecs" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]