Full Code of stac-utils/pystac-client for AI

main 07582d0a303b cached
168 files
45.2 MB
5.5M tokens
360 symbols
1 requests
Copy disabled (too large) Download .txt
Showing preview only (22,190K chars total). Download the full file to get everything.
Repository: stac-utils/pystac-client
Branch: main
Commit: 07582d0a303b
Files: 168
Total size: 45.2 MB

Directory structure:
gitextract_yi101d5s/

├── .adr-dir
├── .codespellignore
├── .gitattributes
├── .github/
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── continuous-integration.yml
│       └── release.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yml
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── RELEASING.md
├── docs/
│   ├── _static/
│   │   └── custom.css
│   ├── api.rst
│   ├── conf.py
│   ├── contributing.rst
│   ├── design/
│   │   ├── 0001-record-architecture-decisions.md
│   │   ├── 0002-choose-stac-library.md
│   │   └── design_decisions.rst
│   ├── index.rst
│   ├── quickstart.rst
│   ├── tutorials/
│   │   ├── aoi-coverage.ipynb
│   │   ├── authentication.md
│   │   ├── cql2-filter.ipynb
│   │   ├── item-search-intersects.ipynb
│   │   ├── pystac-client-introduction.ipynb
│   │   └── stac-metadata-viz.ipynb
│   ├── tutorials.rst
│   └── usage.rst
├── pyproject.toml
├── pystac_client/
│   ├── __init__.py
│   ├── _utils.py
│   ├── cli.py
│   ├── client.py
│   ├── collection_client.py
│   ├── collection_search.py
│   ├── conformance.py
│   ├── errors.py
│   ├── exceptions.py
│   ├── free_text.py
│   ├── item_search.py
│   ├── mixins.py
│   ├── py.typed
│   ├── stac_api_io.py
│   ├── version.py
│   └── warnings.py
├── scripts/
│   ├── build-docs
│   ├── format
│   ├── lint
│   └── test
└── tests/
    ├── __init__.py
    ├── cassettes/
    │   ├── test_cli/
    │   │   ├── TestCLICollections.test_collection_search[inprocess].yaml
    │   │   ├── TestCLICollections.test_collections[inprocess].yaml
    │   │   ├── TestCLICollections.test_save[inprocess].yaml
    │   │   ├── TestCLISearch.test_altering_conforms_to[inprocess---clear-conforms-to].yaml
    │   │   ├── TestCLISearch.test_altering_conforms_to[inprocess---remove-conforms-to=ITEM_SEARCH].yaml
    │   │   ├── TestCLISearch.test_fields[inprocess].yaml
    │   │   ├── TestCLISearch.test_filter[inprocess].yaml
    │   │   ├── TestCLISearch.test_intersects[inprocess-netherlands_aoi.json].yaml
    │   │   ├── TestCLISearch.test_intersects[inprocess-sample-item.json].yaml
    │   │   ├── TestCLISearch.test_intersects_despite_warning[inprocess].yaml
    │   │   ├── TestCLISearch.test_item_search[inprocess].yaml
    │   │   ├── TestCLISearch.test_matched[inprocess].yaml
    │   │   ├── TestCLISearch.test_matched_not_available[inprocess].yaml
    │   │   ├── TestCLISearch.test_non_conformant_can_be_fixed[inprocess].yaml
    │   │   ├── TestCLISearch.test_non_conformant_can_be_ignored[inprocess---ignore=no-conforms-to].yaml
    │   │   ├── TestCLISearch.test_non_conformant_can_be_ignored[inprocess---ignore].yaml
    │   │   ├── TestCLISearch.test_non_conformant_raises_by_default[inprocess].yaml
    │   │   ├── TestCLISearch.test_non_conformant_raises_if_warning_set_to_error[inprocess---error=no-conforms-to].yaml
    │   │   ├── TestCLISearch.test_non_conformant_raises_if_warning_set_to_error[inprocess---error].yaml
    │   │   └── TestCLISearch.test_save[inprocess].yaml
    │   ├── test_client/
    │   │   ├── TestAPI.test_collections_fallback.yaml
    │   │   ├── TestAPI.test_environment_variable.yaml
    │   │   ├── TestAPI.test_from_file.yaml
    │   │   ├── TestAPI.test_instance.yaml
    │   │   ├── TestAPI.test_links.yaml
    │   │   ├── TestAPICollectionSearch.test_search.yaml
    │   │   ├── TestAPISearch.test_search_max_items_unlimited_default.yaml
    │   │   ├── TestConformsTo.test_changing_conforms_to_changes_behavior.yaml
    │   │   ├── TestQueryables.test_get_queryables.yaml
    │   │   ├── TestQueryables.test_get_queryables_collections.yaml
    │   │   ├── TestSigning.test_sign_with_return_warns.yaml
    │   │   ├── TestSigning.test_signing.yaml
    │   │   ├── test_collections_are_clients.yaml
    │   │   ├── test_fallback_strategy.yaml
    │   │   ├── test_get_collection_returns_none_if_not_found.yaml
    │   │   ├── test_get_items_without_ids.yaml
    │   │   ├── test_non_recursion_on_fallback.yaml
    │   │   └── test_query_string_in_collections_url.yaml
    │   ├── test_collection_client/
    │   │   ├── TestCollectionClient.test_get_item.yaml
    │   │   ├── TestCollectionClient.test_get_item_with_item_search.yaml
    │   │   ├── TestCollectionClient.test_get_items.yaml
    │   │   ├── TestCollectionClient.test_get_items_with_ids.yaml
    │   │   ├── TestCollectionClient.test_get_queryables.yaml
    │   │   └── TestCollectionClient.test_instance.yaml
    │   ├── test_collection_search/
    │   │   ├── TestCollectionSearch.test_bbox_results.yaml
    │   │   ├── TestCollectionSearch.test_client_side_bbox.yaml
    │   │   ├── TestCollectionSearch.test_client_side_datetime.yaml
    │   │   ├── TestCollectionSearch.test_client_side_q.yaml
    │   │   ├── TestCollectionSearch.test_datetime_results.yaml
    │   │   ├── TestCollectionSearch.test_enabled_but_client_side_q.yaml
    │   │   ├── TestCollectionSearch.test_matched.yaml
    │   │   ├── TestCollectionSearch.test_q_results.yaml
    │   │   ├── TestCollectionSearch.test_result_paging.yaml
    │   │   └── TestCollectionSearch.test_result_paging_max_collections.yaml
    │   ├── test_item_search/
    │   │   ├── TestItemSearch.test_datetime_results.yaml
    │   │   ├── TestItemSearch.test_deprecations[get_all_items-item_collection-False-True].yaml
    │   │   ├── TestItemSearch.test_deprecations[get_all_items_as_dict-item_collection_as_dict-False-False].yaml
    │   │   ├── TestItemSearch.test_deprecations[get_item_collections-pages-True-True].yaml
    │   │   ├── TestItemSearch.test_deprecations[get_items-items-True-True].yaml
    │   │   ├── TestItemSearch.test_deprecations[item_collections-pages-True-True].yaml
    │   │   ├── TestItemSearch.test_get_all_items.yaml
    │   │   ├── TestItemSearch.test_get_all_items_deprecated.yaml
    │   │   ├── TestItemSearch.test_ids_results.yaml
    │   │   ├── TestItemSearch.test_intersects_results.yaml
    │   │   ├── TestItemSearch.test_item_collection.yaml
    │   │   ├── TestItemSearch.test_items_as_dicts.yaml
    │   │   ├── TestItemSearch.test_result_paging.yaml
    │   │   ├── TestItemSearch.test_result_paging_max_items.yaml
    │   │   ├── TestItemSearch.test_results.yaml
    │   │   ├── TestItemSearchParams.test_collection_object.yaml
    │   │   ├── TestItemSearchParams.test_mixed_collection_object_and_string.yaml
    │   │   ├── TestItemSearchQuery.test_query_json_syntax.yaml
    │   │   ├── TestItemSearchQuery.test_query_shortcut_syntax.yaml
    │   │   ├── test_fields.yaml
    │   │   └── test_multiple_collections.yaml
    │   └── test_stac_api_io/
    │       ├── TestSTAC_IOOverride.test_request_input.yaml
    │       ├── TestSTAC_IOOverride.test_str_input.yaml
    │       ├── TestSTAC_IOOverride.test_timeout_smoke_test.yaml
    │       └── test_stac_io_in_pystac.yaml
    ├── data/
    │   ├── astraea_api.json
    │   ├── fedeo_clearinghouse.json
    │   ├── invalid-collection.json
    │   ├── netherlands_aoi.json
    │   ├── planetary-computer-aster-l1t-collection.json
    │   ├── planetary-computer-collection.json
    │   ├── planetary-computer-root.json
    │   ├── sample-item-collection.json
    │   ├── sample-item.json
    │   └── test-case-1/
    │       ├── catalog.json
    │       ├── country-1/
    │       │   ├── area-1-1/
    │       │   │   ├── area-1-1-imagery/
    │       │   │   │   └── area-1-1-imagery.json
    │       │   │   ├── area-1-1-labels/
    │       │   │   │   └── area-1-1-labels.json
    │       │   │   └── collection.json
    │       │   ├── area-1-2/
    │       │   │   ├── area-1-2-imagery/
    │       │   │   │   └── area-1-2-imagery.json
    │       │   │   ├── area-1-2-labels/
    │       │   │   │   └── area-1-2-labels.json
    │       │   │   └── collection.json
    │       │   └── catalog.json
    │       └── country-2/
    │           ├── area-2-1/
    │           │   ├── area-2-1-imagery/
    │           │   │   └── area-2-1-imagery.json
    │           │   ├── area-2-1-labels/
    │           │   │   └── area-2-1-labels.json
    │           │   └── collection.json
    │           ├── area-2-2/
    │           │   ├── area-2-2-imagery/
    │           │   │   └── area-2-2-imagery.json
    │           │   ├── area-2-2-labels/
    │           │   │   └── area-2-2-labels.json
    │           │   └── collection.json
    │           └── catalog.json
    ├── helpers.py
    ├── test_base_search.py
    ├── test_cli.py
    ├── test_client.py
    ├── test_collection_client.py
    ├── test_collection_search.py
    ├── test_conformance.py
    ├── test_free_text.py
    ├── test_item_search.py
    ├── test_stac_api_io.py
    └── test_warnings.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .adr-dir
================================================
docs/source/design


================================================
FILE: .codespellignore
================================================
filetest

================================================
FILE: .gitattributes
================================================
tests/cassettes/**/*.yaml linguist-generated=true


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: github-actions
    directory: "/"
    schedule:
      interval: weekly
  - package-ecosystem: pip
    directory: "/.github/workflows"
    schedule:
      interval: weekly
  - package-ecosystem: pip
    directory: "."
    schedule:
      interval: daily
    versioning-strategy: increase-if-necessary


================================================
FILE: .github/pull_request_template.md
================================================
**Related Issue(s):** 

- #


**Description:**


**PR Checklist:**

- [ ] Code is formatted
- [ ] Tests pass
- [ ] Changes are added to CHANGELOG.md


================================================
FILE: .github/workflows/continuous-integration.yml
================================================
name: CI

on:
  push:
    branches:
      - main
  pull_request:

concurrency:
  group: ${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    name: build
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        python-version:
          - "3.10"
          - "3.11"
          - "3.12"
        os:
          - ubuntu-latest
          - windows-latest
          - macos-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
        with:
          python-version: ${{ matrix.python-version }}
      - name: Sync
        run: uv sync
      - name: Run pre-commit
        run: uv run pre-commit run --all-files
      - name: Run pytest
        run: uv run pytest -Werror -s --block-network --cov pystac_client --cov-report term-missing
      - name: Run coverage
        run: uv run coverage xml
      - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage.xml
          fail_ci_if_error: false

  min-versions:
    name: min-versions
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
        with:
          activate-environment: true
          python-version: "3.10"
      - name: Install with min requirements
        run: uv sync --no-dev --resolution=lowest-direct
      - name: Run smoke test
        run: stac-client search https://landsatlook.usgs.gov/stac-server -c landsat-c2l2-sr --max-items 1

  pre-release:
    name: pre-release
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
      - name: Sync
        run: uv sync
      - name: Install any pre-releases of pystac
        run: uv pip install -U --pre pystac
      - name: Run pytest
        run: uv run pytest -Werror -s --block-network

  upstream:
    name: upstream
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request'
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
      - name: Sync
        run: uv sync
      - name: Install pystac from main
        run: uv pip install --force-reinstall git+https://github.com/stac-utils/pystac.git
      - name: Run pytest
        run: uv run pytest -Werror -s --block-network

  docs:
    name: docs
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash -el {0}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
      - name: Install pandoc
        run: sudo apt-get install -y pandoc
      - name: Sync
        run: uv sync --group docs
      - name: Build docs
        run: ./scripts/build-docs


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  push:
    tags:
      - "*"

jobs:
  release:
    name: release
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - name: Set up Python 3.11
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
        with:
          python-version: "3.11"
      - name: Install release dependencies
        run: |
          python -m pip install --upgrade pip
          pip install build twine
      - name: Build and publish package
        env:
          TWINE_USERNAME: ${{ secrets.PYPI_STACUTILS_USERNAME }}
          TWINE_PASSWORD: ${{ secrets.PYPI_STACUTILS_PASSWORD }}
        run: |
          python -m build
          twine upload dist/*


================================================
FILE: .gitignore
================================================
.python-version
**/*.private*

# Ignore everything JetBrains
.idea

# Created by .ignore support plugin (hsz.mobi)
### VisualStudio template
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Mono auto generated files
mono_crash.*

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/

# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# Visual Studio 2017 auto generated files
Generated\ Files/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# Benchmark Results
BenchmarkDotNet.Artifacts/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/

# ASP.NET Scaffolding
ScaffoldingReadMe.txt

# StyleCop
StyleCopReport.xml

# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# Visual Studio Trace Files
*.e2e

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json

# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info

# Visual Studio code coverage results
*.coverage
*.coveragexml

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj

# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/

# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/

# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak

# SQL Server files
*.mdf
*.ldf
*.ndf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# Paket dependency manager
.paket/paket.exe
paket-files/

# FAKE - F# Make
.fake/

# CodeRush personal settings
.cr/personal

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config

# Tabs Studio
*.tss

# Telerik's JustMock configuration file
*.jmconfig

# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs

# OpenCover UI analysis results
OpenCover/

# Azure Stream Analytics local run output
ASALocalRun/

# MSBuild Binary and Structured Log
*.binlog

# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder
.mfractor/

# Local History for Visual Studio
.localhistory/

# BeatPulse healthcheck temp database
healthchecksdb

# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd

### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839


# CMake
cmake-build-*/

# File-based project format
*.iws

# IntelliJ
out/

# JIRA plugin
atlassian-ide-plugin.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

### VirtualEnv template
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
.Python
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
pyvenv.cfg
.venv
pip-selfcheck.json

### JupyterNotebooks template
# gitignore template for Jupyter Notebooks
# website: http://jupyter.org/

.ipynb_checkpoints
*/.ipynb_checkpoints/*

# IPython
profile_default/
ipython_config.py

# Remove previous ipynb_checkpoints
#   git rm -r .ipynb_checkpoints/

### Python template
# Byte-compiled / optimized / DLL files
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook

# IPython

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

### Linux template

# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*

# KDE directory preferences
.directory

# Linux trash folder which might appear on any partition or disk
.Trash-*

# .nfs files are created when an open file is removed but is still being accessed
.nfs*

### Windows template
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db

# Dump file
*.stackdump

# Folder config file
[Dd]esktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp

# Windows shortcuts
*.lnk


================================================
FILE: .pre-commit-config.yaml
================================================
# Configuration file for pre-commit (https://pre-commit.com/).
# Please run `pre-commit run --all-files` when adding or changing entries.

repos:
  - repo: https://github.com/charliermarsh/ruff-pre-commit
    rev: "v0.9.6"
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format
  - repo: https://github.com/codespell-project/codespell
    rev: v2.4.1
    hooks:
      - id: codespell
        args: [--ignore-words=.codespellignore]
        types_or: [jupyter, markdown, python, shell]
  - repo: https://github.com/PyCQA/doc8
    rev: v1.1.2
    hooks:
      - id: doc8
        args: [--ignore=D004]
        additional_dependencies:
          - importlib_metadata < 5; python_version == "3.7"
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.15.0
    hooks:
      - id: mypy
        files: ".*\\.py$"
        additional_dependencies:
          - pystac
          - pytest-vcr
          - types-requests
          - types-python-dateutil


================================================
FILE: .readthedocs.yml
================================================
version: 2

build:
  os: ubuntu-24.04
  tools:
    python: "3.12"
  jobs:
    pre_create_environment:
      - asdf plugin add uv
      - asdf install uv latest
      - asdf global uv latest
    create_environment:
      - uv venv "${READTHEDOCS_VIRTUALENV_PATH}"
    install:
      - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --group docs --link-mode=copy

sphinx:
  configuration: docs/conf.py
  fail_on_warning: false

formats:
  - pdf
  - htmlzip


================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Add comprehensive test coverage for warning context managers (`ignore()` and `strict()`) ([#832](https://github.com/stac-utils/pystac-client/pull/832))
- Moved -Werror to pyproject.toml ([#841](https://github.com/stac-utils/pystac-client/pull/841))

### Changed

- Make `get_collection` raise if `collection_id` is empty ([#809](https://github.com/stac-utils/pystac-client/pull/809))
- Update `get_collection` to return None if no collection exists ([#861](https://github.com/stac-utils/pystac-client/pull/861))

### Added

- Add comprehensive test coverage for ConformanceClasses enum ([#834](https://github.com/stac-utils/pystac-client/pull/834))

### Documentation

- Update contributing guide to consistently use `uv` workflow ([#822](https://github.com/stac-utils/pystac-client/pull/822))
- Fix format script to use `ruff-format` instead of deprecated `black` hook ([#822](https://github.com/stac-utils/pystac-client/pull/822))
- Add "How to..." section to usage documentation with latest datetime search across multiple collections example ([#823](https://github.com/stac-utils/pystac-client/pull/823))


## [v0.9.0] - 2025-07-17

### Added

- Coerce cql2 style to match HTTP method using `cql2` library ([#804](https://github.com/stac-utils/pystac-client/pull/804))

### Fixed

- Fix usage documentation of `ItemSearch` ([#790](https://github.com/stac-utils/pystac-client/pull/790))
- Fix fields argument to CLI  ([#797](https://github.com/stac-utils/pystac-client/pull/797))
- Clarify recursive behaviour of the `get_items` method in the method docstring ([#800](https://github.com/stac-utils/pystac-client/pull/800))

## [v0.8.6] - 2025-02-11

### Changed

- Use [uv](https://docs.astral.sh/uv/) for development ([#784](https://github.com/stac-utils/pystac-client/pull/784))
- Updated to Python 3.10 syntax with **pyupgrade** ([#783](https://github.com/stac-utils/pystac-client/pull/783/))

### Fixed

- `Client.get_collection` for static catalogs ([#782](https://github.com/stac-utils/pystac-client/pull/782))
- Permissive collection reading ([#787](https://github.com/stac-utils/pystac-client/pull/787))

## [v0.8.5] - 2024-10-23

### Fixed

- Use urljoin to build hrefs ([#746](https://github.com/stac-utils/pystac-client/pull/746))

## [v0.8.4] - 2024-10-16

### Added

- Support for collection search via `CollectionSearch` class and associated client methods ([#735](https://github.com/stac-utils/pystac-client/pull/735))

### Removed

- Python 3.9 support ([#724](https://github.com/stac-utils/pystac-client/pull/724))

## [v0.8.3] - 2024-07-01

### Fixed

- Actually set `timeout` when initializing `StacApiIO` ([#709](https://github.com/stac-utils/pystac-client/pull/709))
- Set `_stac_io` on `CollectionClient` when reading ([#709](https://github.com/stac-utils/pystac-client/pull/709))

## [v0.8.2] - 2024-05-30

### Added

- Support for "Feature" type `intersects` dictionaries ([#696](https://github.com/stac-utils/pystac-client/pull/696))

## [v0.8.1] - 2024-05-23

### Fixed

- Use singular `include` and `exclude` Field extension key names ([#690](https://github.com/stac-utils/pystac-client/pull/690))

## [v0.8.0] - 2024-05-17

### Fixed

- Update `requires-python` in metadata to require >= 3.9 ([#684](https://github.com/stac-utils/pystac-client/pull/684))
- Interpret naive datetimes as UTC ([#686](https://github.com/stac-utils/pystac-client/pull/686))

## [v0.7.7] - 2024-04-19

### Changed

- Updated to **pystac** v1.10.0 ([#661](https://github.com/stac-utils/pystac-client/pull/661))
- Use [uv](https://github.com/astral-sh/uv) for CI ([#663](https://github.com/stac-utils/pystac-client/pull/663))
- use `APILayoutStrategy` as fallback strategy ([#666](https://github.com/stac-utils/pystac-client/pull/666))
- Updated grammar in README ([#676](https://github.com/stac-utils/pystac-client/pull/676))

### Fixed

- Respect the `REQUESTS_CA_BUNDLE` and `CURL_CA_BUNDLE` environment variables when sending requests ([#669](https://github.com/stac-utils/pystac-client/pull/669))

## [v0.7.6] - 2024-02-28

### Fixed

- Recursion error in `get_items` ([#608](https://github.com/stac-utils/pystac-client/pull/608))
- Fixes inconsistent behavior in `CollectionClient.get_items` when Item IDs are supplied ([#639](https://github.com/stac-utils/pystac-client/pull/639))
- Logic determining if `context.matched` in `ItemSearch.matched` response ([#646](https://github.com/stac-utils/pystac-client/pull/646))

### Removed

- Passing `-q` to the CLI rather than `--query` ([#614](https://github.com/stac-utils/pystac-client/pull/614))

## [v0.7.5] - 2023-09-05

### Fixed

- `--matched` flag in CLI ([#588](https://github.com/stac-utils/pystac-client/pull/588))

## [v0.7.4] - 2023-09-05

### Changed

- Don't provide a `limit` by default, trust the server to know its limits ([#584](https://github.com/stac-utils/pystac-client/pull/584))

## [v0.7.3] - 2023-08-21

### Changed

- Raise more informative errors from CLI ([#554](https://github.com/stac-utils/pystac-client/pull/554))
- Add `Retry` type hint to `StacApiIO` ([#577](https://github.com/stac-utils/pystac-client/pull/577))

### Fixed

- Updated `get_items` signatures for PySTAC v1.8 ([#559](https://github.com/stac-utils/pystac-client/pull/559), [#579](https://github.com/stac-utils/pystac-client/pull/579))

## [v0.7.2] - 2023-06-23

### Fixed

- Remove troublesome assertion ([#549](https://github.com/stac-utils/pystac-client/pull/549))

## [v0.7.1] - 2023-06-13

### Fixed

- Remove unnecessary `typing_extensions` import ([#541](https://github.com/stac-utils/pystac-client/pull/541))

## [v0.7.0] - 2023-06-12

### Added

- Timeout option added to `Client.open` ([#463](https://github.com/stac-utils/pystac-client/pull/463), [#538](https://github.com/stac-utils/pystac-client/pull/538))
- Support for fetching catalog queryables ([#477](https://github.com/stac-utils/pystac-client/pull/477))
- PySTAC Client specific warnings ([#480](https://github.com/stac-utils/pystac-client/pull/480))
- Support for fetching and merging a selection of queryables ([#511](https://github.com/stac-utils/pystac-client/pull/511))
- Better error messages for the CLI ([#531](https://github.com/stac-utils/pystac-client/pull/531))
- `Modifiable` to our public API ([#534](https://github.com/stac-utils/pystac-client/pull/534))
- `max_retries` parameter to `StacApiIO` ([#532](https://github.com/stac-utils/pystac-client/pull/532))

### Changed

- Switched to Ruff from isort/flake8 ([#457](https://github.com/stac-utils/pystac-client/pull/457))
- Move to `FutureWarning` from `DeprecationWarning` for item search interface functions that are to be removed ([#464](https://github.com/stac-utils/pystac-client/pull/464))
- Consolidate contributing docs into one place ([#478](https://github.com/stac-utils/pystac-client/issues/478))
- Use `pyproject.toml` instead of `setup.py` ([#501](https://github.com/stac-utils/pystac-client/pull/501))
- Enable Ruff import sorting ([#518](https://github.com/stac-utils/pystac-client/pull/518))

### Fixed

- `query` parameter in GET requests ([#362](https://github.com/stac-utils/pystac-client/pull/362))
- Double encoding of `intersects` parameter in GET requests ([#362](https://github.com/stac-utils/pystac-client/pull/362))
- Fix geometry instantiation in item-search-intersects.ipynb ([#484](https://github.com/stac-utils/pystac-client/pull/484))
- Three tests that were false positives due to out-of-date cassettes ([#491](https://github.com/stac-utils/pystac-client/pull/491))
- Max items checks when paging ([#492](https://github.com/stac-utils/pystac-client/pull/492))
- `ItemSearch.url_with_parameters` no longer unquotes the url ([#530](https://github.com/stac-utils/pystac-client/pull/530))

### Removed

- `pystac_client.conformance.CONFORMANCE_URIS` dictionary ([#480](https://github.com/stac-utils/pystac-client/pull/480))

## [v0.6.1] - 2023-03-14

### Changed

- Bumped PySTAC dependency to >= 1.7.0 ([#449](https://github.com/stac-utils/pystac-client/pull/449))

### Fixed

- Fix parse fail when header has multiple '=' characters ([#440](https://github.com/stac-utils/pystac-client/pull/440))
- `Client.open` and `Client.from_file` now apply `headers`, etc to existing `stac_io` instances (([#439](https://github.com/stac-utils/pystac-client/pull/439)))

## [v0.6.0] - 2023-01-27

### Added

- Python 3.11 support ([#347](https://github.com/stac-utils/pystac-client/pull/347))
- `request_modifier` to `StacApiIO` to allow for additional authentication mechanisms (e.g. AWS SigV4) ([#372](https://github.com/stac-utils/pystac-client/pull/372))
- _Authentication_ tutorial, demonstrating how to use to the provided hooks to use both basic and AWS SigV4 authentication ([#372](https://github.com/stac-utils/pystac-client/pull/372))
- CI checks for Windows and MacOS ([#378](https://github.com/stac-utils/pystac-client/pull/378))
- Fallback to `STAC API - Item Search` when finding a single item in `CollectionClient` if `STAC API - Features` is not implemented ([#379](https://github.com/stac-utils/pystac-client/pull/379))

### Fixed

- Stop iteration on an empty page ([#338](https://github.com/stac-utils/pystac-client/pull/338))
- Some mishandled cases for datetime intervals ([#363](https://github.com/stac-utils/pystac-client/pull/363))
- Collection requests when the Client's url ends in a '/' ([#373](https://github.com/stac-utils/pystac-client/pull/373), [#405](https://github.com/stac-utils/pystac-client/pull/405))
- Parse datetimes more strictly ([#364](https://github.com/stac-utils/pystac-client/pull/364))

### Removed

- Python 3.7 support ([#347](https://github.com/stac-utils/pystac-client/pull/347))

## [v0.5.1] - 2022-09-19

### Added

- Added `ItemSearch.url_with_parameters` to get a formatted search url ([#304](https://github.com/stac-utils/pystac-client/issues/304))

### Fixed

- Fix variable name in quickstart example ([#316](https://github.com/stac-utils/pystac-client/pull/316))
- `StacApiIO.write_text_to_href` ([#312](https://github.com/stac-utils/pystac-client/pull/312))
- Removed mention of STAC_URL in `Client.open` docstring ([#317](https://github.com/stac-utils/pystac-client/pull/317))

## [v0.5.0] - 2022-08-19

### Added

- Added a new keyword `modifier` to various constructors like `Client.open()` ([#259](https://github.com/stac-utils/pystac-client/issues/259))

### Fixed

- Fix type annotation of `Client._stac_io` and avoid implicit re-exports in `pystac_client.__init__.py` ([#249](https://github.com/stac-utils/pystac-client/pull/249))
- Added `ItemSearch.pages`, `ItemSearch.pages_as_dicts`, `ItemSearch.item_collection`, and `ItemSearch.item_collection_as_dict`
  as replacements for various deprecated methods ([#237](https://github.com/stac-utils/pystac-client/issues/237))
- Restored the previous behavior of `Client.search()` to return an unlimited number of items by default. ([#273](https://github.com/stac-utils/pystac-client/pull/273))

### Deprecated

- `ItemSearch.item_collections` has been deprecated in favor of `ItemSearch.pages`. ([#237](https://github.com/stac-utils/pystac-client/issues/237))

## [v0.4.0] - 2022-06-08

### Added

- Significantly improved type hints
- lru_cache to several methods ([#167](https://github.com/stac-utils/pystac-client/pull/167))
- Direct item GET via ogcapi-features, if conformant ([#166](https://github.com/stac-utils/pystac-client/pull/166))
- `py.typed` for downstream type checking ([#163](https://github.com/stac-utils/pystac-client/pull/163))
- Added tutorial for using various geometry objects (e.g., shapely, geojson) as an Item Search intersects argument ([#232](https://github.com/stac-utils/pystac-client/pull/232))

### Changed

- Item Search no longer defaults to returning an unlimited number of result Items from
  its "items" methods. The `max_items` parameter now defaults to 100 instead of None.
  Since the `limit` parameter also defaults to 100, in an ideal situation, only one request
  will be made to the server to retrieve all 100 items. Both of these parameters can be
  carefully adjusted upwards to align with the server's capabilities and the expected
  number of search results. ([#208](https://github.com/stac-utils/pystac-client/pull/208))
- Better error message when trying to search a non-item-search-conforming catalog ([#164](https://github.com/stac-utils/pystac-client/pull/164))
- Search `filter-lang` defaults to `cql2-json` instead of `cql-json` ([#169](https://github.com/stac-utils/pystac-client/pull/169))
- Search `filter-lang` will be set to `cql2-json` if the `filter` is a dict, or `cql2-text` if it is a string ([#169](https://github.com/stac-utils/pystac-client/pull/169))
- Search parameter `intersects` is now typed to only accept a str, dict, or object that implements `__geo_interface__` ([#174](https://github.com/stac-utils/pystac-client/pull/174))
- Better error message when trying to open a Collection with `Client.open` ([#222](https://github.com/stac-utils/pystac-client/pull/222))

### Deprecated

- Item Search methods `get_items()` and `get_item_collections()` have been renamed to
  `items()` and `item_collections()`. The original methods are now deprecated
  and may be removed as early as v0.5.0. ([#206](https://github.com/stac-utils/pystac-client/pull/206))
- Item Search methods `get_all_items()` and `get_all_items_as_dict()` are now deprecated,
  and may be removed as early as v0.5.0.
  These have been deprecated because they have the potential to perform a large number
  of requests to the server and instantiate a large number of objects in memory.
  To a user, this is only visible as a large delay in the method call and/or the
  exhaustion of all available memory. The iterator methods `items()` or
  `item_collections()` should be used instead. ([#206](https://github.com/stac-utils/pystac-client/pull/206))
- CLI parameter `-q` is now deprecated and may be removed as early as v0.5.0. Use `--query` instead. ([#215](https://github.com/stac-utils/pystac-client/pull/215))

## Removed

- Client parameter `require_geojson_link` has been removed. ([#165](https://github.com/stac-utils/pystac-client/pull/165))

### Fixed

- Search query parameter now has correct typing and handles Query Extension JSON format. ([#220](https://github.com/stac-utils/pystac-client/pull/220))
- Search sortby parameter now has correct typing and handles both GET and POST JSON parameter formats. ([#175](https://github.com/stac-utils/pystac-client/pull/175))
- Search fields parameter now has correct typing and handles both GET and POST JSON parameter formats. ([#184](https://github.com/stac-utils/pystac-client/pull/184))
- Use pytest configuration to skip benchmarks by default (instead of a `skip` mark). ([#168](https://github.com/stac-utils/pystac-client/pull/168))
- Methods retrieving collections incorrectly checked the existence of the OAFeat OpenAPI 3.0 conformance class
  (<http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30>) instead of the `STAC API - Collections`
  (<https://api.stacspec.org/v1.0.0-beta.1/collections>) or `STAC API - Features`
  (<https://api.stacspec.org/v1.0.0-beta.1/ogcapi-features>) conformance classes. ([#223](https://github.com/stac-utils/pystac-client/pull/223))

## [v0.3.5] - 2022-05-26

### Fixed

- Search against earth-search v0 failed with message "object of type 'Link' has no len()" ([#179](https://github.com/stac-utils/pystac-client/pull/179))

## [v0.3.4] - 2022-05-18

### Changed

- Relaxed media type requirement for search links ([#160](https://github.com/stac-utils/pystac-client/pull/160), [#165](https://github.com/stac-utils/pystac-client/pull/165))

## [v0.3.3] - 2022-04-28

### Added

- Add `--filter-lang` parameter to allow specifying other filter language to be used within the `--filter` parameter ([#140](https://github.com/stac-utils/pystac-client/pull/140))
- CI checks against minimum versions of all dependencies and any pre-release versions of PySTAC ([#144](https://github.com/stac-utils/pystac-client/pull/144))

### Changed

- Relaxed upper bound on PySTAC dependency ([#144](https://github.com/stac-utils/pystac-client/pull/144))
- Bumped PySTAC dependency to >= 1.4.0 ([#147](https://github.com/stac-utils/pystac-client/pull/147))

## [v0.3.2] - 2022-01-11

### Added

- `Client.search` accepts an optional `filter_lang` argument for `filter` requests ([#128](https://github.com/stac-utils/pystac-client/pull/128))
- Added support for filtering the search endpoint using the `media_type` in `Client.open` ([#142](https://github.com/stac-utils/pystac-client/pull/142))

### Fixed

- Values from `parameters` and `headers` arguments to `Client.open` and `Client.from_file` are now also used in requests made from `CollectionClient` instances
  fetched from the same API (([#126](https://github.com/stac-utils/pystac-client/pull/126)))
- The tests folder is no longer installed as a package.

## [v0.3.1] - 2021-11-17

### Added

- Adds `--block-network` option to all test commands to ensure no network requests are made during unit tests
  ([#119](https://github.com/stac-utils/pystac-client/pull/119))
- `parameters` argument to `StacApiIO`, `Client.open`, and `Client.from_file` to allow query string parameters to be passed to all requests
  ([#118](https://github.com/stac-utils/pystac-client/pull/118))

### Changed

- Update min PySTAC version to 1.2
- Default page size limit set to 100 rather than relying on the server default
- Fetch single collection directly from endpoint in API rather than iterating through children [Issue #114](https://github.com/stac-utils/pystac-client/issues/114)

### Fixed

- `Client.get_collections` raised an exception when API did not publish `/collections` conformance class instead of falling back to using child links
  ([#120](https://github.com/stac-utils/pystac-client/pull/120))

## [v0.3.0] - 2021-09-28

### Added

- Jupyter Notebook tutorials
- Basic CQL-JSON filtering ([#100](https://github.com/stac-utils/pystac-client/pull/100))

### Changed

- Improved performance when constructing `pystac.ItemCollection` objects.
- Relax `requests` dependency ([#87](https://github.com/stac-utils/pystac-client/pull/87))
- Use regular expressions for checking conformance classes ([#97](https://github.com/stac-utils/pystac-client/pull/97))
- Reorganized documentation, updated all docs

### Fixed

- `ItemSearch` now correctly handles times without a timezone specifier ([#92](https://github.com/stac-utils/pystac-client/issues/92))
- queries including `gsd` cast to string to float when using shortcut query syntax (i.e., "key=val" strings). ([#98](https://github.com/stac-utils/pystac-client/pull/97))
- Documentation lints ([#108](https://github.com/stac-utils/pystac-client/pull/108))

## [v0.2.0] - 2021-08-04

### Added

- `Client.open` falls back to the `STAC_URL` environment variable if no url is provided as an argument ([#48](https://github.com/stac-utils/pystac-client/pull/48))
- New Search.get_pages() iterator function to retrieve pages as raw JSON, not as ItemCollections
- `StacApiIO` class added, subclass from PySTAC `StacIO`. A `StacApiIO` instance is used for all IO for a Client instance, and all requests
  are in a single HTTP session, handle pagination and respects conformance
- `conformance.CONFORMANCE_CLASSES` dictionary added containing all STAC API Capabilities from stac-api-spec
- `collections` subcommand to CLI, for saving all Collections in catalog as JSON
- `Client.get_collections` overrides Catalog to use /collections endpoint if API conforms
- `Client.get_collection(<collection_id>)` for getting specific collection
- `Client.get_items` and `Client.get_all_items` override Catalog functions to use search endpoint instead of traversing catalog

### Changed

- Update to use PySTAC 1.1.0
- IO changed to use PySTAC's new StacIO base class.
- `Search.item_collections()` renamed to `Search.get_item_collections()`
- `Search.item()` renamed to `Search.get_items()`
- Conformance is checked by each individual function that requires a particular conformance
- STAC API testing URLs changed to updated APIs
- `ItemSearch.get_pages()` function moved to StacApiIO class for general use
- Logging is now enabled in the CLI in all cases.
  If data are being printed to stdout, logging goes to stderr.
  ([#79](https://github.com/stac-utils/pystac-client/pull/79))
- Improved logging for GET requests (prints encoded URL)

### Removed

- `get_pages` and `simple_stac_resolver` functions from `pystac_client.stac_io` (The new StacApiIO class understands `Link` objects)
- `Client.search()` no longer accepts a `next_resolver` argument
- pystac.extensions modules, which were based on PySTAC's previous extension implementation, replaced in 1.0.0
- `stac_api_object.StacApiObjectMixin`, replaced with conformance checking in `StacApiIO`
- PySTAC Collection objects can no longer be passed in as `collections` arguments to the `ItemSearch` class (just pass ids)
- `Catalog.get_collection_list` (was alias to `get_child_links`) because made assumption about this being an API only. Also redundant with `Catalog.get_collections`
- `Search.item_collections()`
- `Search.items()`
- STAC_URL environment variable in Client.open(). url parameter in Client is now required
- STAC_URL environment variable in CLI. CLI now has a required positional argument for the URL

### Fixed

- Running `stac-client` with no arguments no longer raises a confusing exception ([#52](https://github.com/stac-utils/pystac-client/pull/52))
- `Client.get_collections_list` ([#44](https://github.com/stac-utils/pystac-client/issues/44))
- The regular expression used for datetime parsing ([#59](https://github.com/stac-utils/pystac-client/pull/59))
- `Client.from_file` now works as expected, using `Client.open` is not required, although it will fetch STAC_URL from an envvar

## [v0.1.1] - 2021-04-16

### Added

- `ItemSearch.items_as_collection` ([#37](https://github.com/stac-utils/pystac-client/pull/37))
- Documentation [published on ReadTheDocs](https://pystac-client.readthedocs.io/en/latest/) ([#46](https://github.com/stac-utils/pystac-client/pull/46))

### Changed

- CLI: pass in headers as list of KEY=VALUE pairs

### Fixed

- Include headers in STAC_IO ([#38](https://github.com/stac-utils/pystac-client/pull/38))
- README updated to reflect actual CLI behavior

## [v0.1.0] - 2021-04-14

Initial release.

[Unreleased]: https://github.com/stac-utils/pystac-client/compare/v0.9.0...main
[v0.9.0]: https://github.com/stac-utils/pystac-client/compare/v0.8.6...v0.9.0
[v0.8.6]: https://github.com/stac-utils/pystac-client/compare/v0.8.5...v0.8.6
[v0.8.5]: https://github.com/stac-utils/pystac-client/compare/v0.8.4...v0.8.5
[v0.8.4]: https://github.com/stac-utils/pystac-client/compare/v0.8.3...v0.8.4
[v0.8.3]: https://github.com/stac-utils/pystac-client/compare/v0.8.2...v0.8.3
[v0.8.2]: https://github.com/stac-utils/pystac-client/compare/v0.8.1...v0.8.2
[v0.8.1]: https://github.com/stac-utils/pystac-client/compare/v0.8.0...v0.8.1
[v0.8.0]: https://github.com/stac-utils/pystac-client/compare/v0.7.7...v0.8.0
[v0.7.7]: https://github.com/stac-utils/pystac-client/compare/v0.7.6...v0.7.7
[v0.7.6]: https://github.com/stac-utils/pystac-client/compare/v0.7.5...v0.7.6
[v0.7.5]: https://github.com/stac-utils/pystac-client/compare/v0.7.4...v0.7.5
[v0.7.4]: https://github.com/stac-utils/pystac-client/compare/v0.7.3...v0.7.4
[v0.7.3]: https://github.com/stac-utils/pystac-client/compare/v0.7.2...v0.7.3
[v0.7.2]: https://github.com/stac-utils/pystac-client/compare/v0.7.1...v0.7.2
[v0.7.1]: https://github.com/stac-utils/pystac-client/compare/v0.7.0...v0.7.1
[v0.7.0]: https://github.com/stac-utils/pystac-client/compare/v0.6.1...v0.7.0
[v0.6.1]: https://github.com/stac-utils/pystac-client/compare/v0.6.0...v0.6.1
[v0.6.0]: https://github.com/stac-utils/pystac-client/compare/v0.5.1...v0.6.0
[v0.5.1]: https://github.com/stac-utils/pystac-client/compare/v0.5.0...v0.5.1
[v0.5.0]: https://github.com/stac-utils/pystac-client/compare/v0.4.0...v0.5.0
[v0.4.0]: https://github.com/stac-utils/pystac-client/compare/v0.3.5...v0.4.0
[v0.3.5]: https://github.com/stac-utils/pystac-client/compare/v0.3.4...v0.3.5
[v0.3.4]: https://github.com/stac-utils/pystac-client/compare/v0.3.3...v0.3.4
[v0.3.3]: https://github.com/stac-utils/pystac-client/compare/v0.3.2...v0.3.3
[v0.3.2]: https://github.com/stac-utils/pystac-client/compare/v0.3.1...v0.3.2
[v0.3.1]: https://github.com/stac-utils/pystac-client/compare/v0.3.0...v0.3.1
[v0.3.0]: https://github.com/stac-utils/pystac-client/compare/v0.2.0...v0.3.0
[v0.2.0]: https://github.com/stac-utils/pystac-client/compare/v0.1.1...v0.2.0
[v0.1.1]: https://github.com/stac-utils/pystac-client/compare/v0.1.0...v0.1.1
[v0.1.0]: https://github.com/stac-utils/pystac-client/tree/v0.1.0


================================================
FILE: LICENSE
================================================
Copyright 2021 Jon Duckworth

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


================================================
FILE: MANIFEST.in
================================================
global-include *.typed


================================================
FILE: README.md
================================================
# pystac-client

[![CI](https://github.com/stac-utils/pystac-client/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/stac-utils/pystac-client/actions/workflows/continuous-integration.yml)
[![Release](https://github.com/stac-utils/pystac-client/actions/workflows/release.yml/badge.svg)](https://github.com/stac-utils/pystac-client/actions/workflows/release.yml)
[![PyPI version](https://badge.fury.io/py/pystac-client.svg)](https://badge.fury.io/py/pystac-client)
[![Documentation](https://readthedocs.org/projects/pystac-client/badge/?version=stable)](https://pystac-client.readthedocs.io)
[![codecov](https://codecov.io/gh/stac-utils/pystac-client/branch/main/graph/badge.svg)](https://codecov.io/gh/stac-utils/pystac-client)

A Python client for working with [STAC](https://stacspec.org/) APIs.

## Installation

PySTAC Client is published to PyPi as [pystac-client](https://pypi.org/project/pystac-client/).

The only direct Python dependencies of **pystac-client** are [PySTAC](https://pystac.readthedocs.io),
[requests](https://docs.python-requests.org), and [dateutil](https://dateutil.readthedocs.io).

To install with pip, run:

```shell
python -m pip install pystac-client
```

## Documentation

See the [documentation page](https://pystac-client.readthedocs.io/en/latest/) for the latest docs.

## Development

See the [contributing page](https://pystac-client.readthedocs.io/en/latest/contributing.html) for the latest development instructions.


================================================
FILE: RELEASING.md
================================================
# Releasing

1. Determine the next version.
   We follow [semantic versioning](https://semver.org/).
2. Create a release branch named `release/vX.Y.Z`, where `X.Y.Z` is the next version.
3. Update [version.py](pystac_client/version.py) with the new version.
4. Update all cassettes: `pytest --record-mode rewrite`
5. Update the pre-commit hook versions: `pre-commit autoupdate`
6. Update [CHANGELOG.md](CHANGELOG.md).
   1. Add a new header under "Unreleased" with the new version and the date, e.g. `## [vX.Y.Z] - YYYY-MM-DD`.
   2. Audit the changelog section to ensure all changes are captured.
   3. Add a link reference for the new version after the Unreleased link reference at the bottom of the file.
      Follow the format from the previous version links.
7. If necessary, update the versions table in [the documentation](docs/index.rst)
8. (optional) Build the package locally and inspect its contents: `pip install build && python -m build`
9. Open a pull request for your `release/vX.Y.Z` branch against the appropriate branch (either `main` or a version branch, e.g. `v0.3`).
10. After pull request merge, create an annotated tag for your version, e.g. `git tag -a vX.Y.Z`.
11. Push the tag.
   This will trigger [the Github release workflow](.github/workflows/release.yml) and publish to PyPI.
12. [Create a new release on Github](https://github.com/stac-utils/pystac-client/releases/new) pointing to the new tag.


================================================
FILE: docs/_static/custom.css
================================================
.no-sidebar {
    display: none;
}

.card-text a {
    font-weight: 600;
    color: rgb(255, 26, 26);
}

.card-header p {
    font-size: large;
    font-weight: 600;
}

.navbar-brand img {
    height: 50px;
}

.navbar-brand {
    height: 75px;
}


================================================
FILE: docs/api.rst
================================================
API Reference
=============

This section is autogenerated from in-line code documentation. It is mostly useful as a
reference for the various classes, methods, and other objects in the library, but is
not intended to function as a starting point for working with ``pystac_client``.

Client
------

Client is the base PySTAC-Client that inherits from :class:`Catalog <pystac.Catalog>`.
In addition to the PySTAC functionality, Client allows opening of API URLs,
understanding of conformance, and support for searching and paging through results.

.. autoclass:: pystac_client.Client
   :members:
   :undoc-members:
   :show-inheritance:

Collection Client
-----------------

Client is the a PySTAC-Client that inherits from
:class:`Collection <pystac.Collection>`. In addition to the PySTAC functionality,
CollectionClient allows opening of API URLs, and iterating through items at a search
endpoint, if supported.

.. autoclass:: pystac_client.CollectionClient
   :members:
   :undoc-members:

Collection Search
-----------------

The `CollectionSearch` class represents a search of collections in a STAC API.

.. autoclass:: pystac_client.CollectionSearch
   :members:
   :undoc-members:
   :member-order: bysource

Item Search
-----------

The `ItemSearch` class represents a search of a STAC API.

.. autoclass:: pystac_client.ItemSearch
   :members:
   :undoc-members:
   :member-order: bysource


STAC API IO
-----------

The StacApiIO class inherits from the :class:`Collection <pystac.DefaultStacIO>`
class and allows for reading over http, such as with REST APIs.

.. autoclass:: pystac_client.stac_api_io.StacApiIO
   :members:
   :undoc-members:
   :show-inheritance:


Conformance
-----------

.. automodule:: pystac_client.conformance
    :members:
    :undoc-members:
    :show-inheritance:


Exceptions
----------

.. automodule:: pystac_client.exceptions
    :members:
    :undoc-members:
    :show-inheritance:

Warnings
--------

.. automodule:: pystac_client.warnings
    :members:
    :undoc-members:
    :show-inheritance:


================================================
FILE: docs/conf.py
================================================
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

import re
import subprocess
import sys
from pathlib import Path

# -- Path setup --------------------------------------------------------------

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
sys.path.insert(0, str(Path(__file__).parent.parent.parent.resolve()))
from pystac_client import __version__  # noqa: E402

git_branch = (
    subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])
    .decode("utf-8")
    .strip()
)

# -- Project information -----------------------------------------------------

project = "pystac-client"
copyright = "2021, Jon Duckworth"
author = "Matthew Hanson, Jon Duckworth"
github_user = "stac-utils"
github_repo = "pystac-client"
package_description = "A Python client for the STAC and STAC-API specs"

# The full version, including alpha/beta/rc tags
version = re.fullmatch(r"^(\d+\.\d+\.\d).*$", __version__).group(1)  # type: ignore
release = __version__


# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    "sphinx.ext.autodoc",
    "sphinx.ext.viewcode",
    "sphinx.ext.intersphinx",
    "sphinx.ext.napoleon",
    "sphinx.ext.extlinks",
    "sphinxcontrib.fulltoc",
    "nbsphinx",
    "myst_parser",
]

extlinks = {
    "tutorial": (
        "https://github.com/stac-utils/pystac-client/tree/{}/docs/tutorials/%s".format(
            git_branch
        ),
        "tutorial",
    )
}

nbsphinx_allow_errors = False

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
source_suffix = [".rst", "*.md", "*.ipynb"]
exclude_patterns = ["build/*"]


# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = "pydata_sphinx_theme"

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
    "icon_links": [
        {
            "name": "GitHub",
            "url": "https://github.com/stac-utils/pystac-client",
            "icon": "fab fa-github-square",
        },
        {
            "name": "Gitter",
            "url": "https://gitter.im/SpatioTemporal-Asset-Catalog/"
            "python?utm_source=share-link&utm_medium=link&utm_campaign=share-link",
            "icon": "fab fa-gitter",
        },
    ],
    "external_links": [
        {"name": "STAC Spec", "url": "https://github.com/radiantearth/stac-spec"}
    ],
    "navigation_with_keys": False,
    # "navbar_end": ["navbar-icon-links.html", "search-field.html"]
}

html_logo = "_static/STAC-03.png"

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path: list[str] = ["_static"]


# -- Options for intersphinx extension ---------------------------------------

intersphinx_mapping = {
    "python": ("https://docs.python.org/3", None),
    "requests": ("https://requests.readthedocs.io/en/latest", None),
    "pystac": ("https://pystac.readthedocs.io/en/latest", None),
    "dateutil": ("https://dateutil.readthedocs.io/en/stable/", None),
}

# -- Options for autodoc extension -------------------------------------------

autodoc_typehints = "none"


================================================
FILE: docs/contributing.rst
================================================
Contributing
============

A list of issues and ongoing work is available on the PySTAC Client `issues page
<https://github.com/stac-utils/pystac-client/issues>`_. If you want to contribute code, the best
way is to coordinate with the core developers via an issue or pull request conversation.

Development installation
^^^^^^^^^^^^^^^^^^^^^^^^
Fork PySTAC Client into your GitHub account. Clone the repo, install `uv
<https://docs.astral.sh/uv/getting-started/installation/>`_ then install the package in editable mode with development dependencies:

.. code-block:: bash

    $ git clone git@github.com:your_user_name/pystac-client.git
    $ cd pystac-client
    $ uv pip install -e . --group dev
    $ uv run pre-commit install

Testing
^^^^^^^
tl;dr: Run ``./scripts/test`` to run all tests and linters.

PySTAC Client runs tests using `pytest <https://docs.pytest.org/en/latest/>`_. You can find unit tests in the ``tests/``
directory.

To run the tests and generate the coverage report:

.. code-block:: bash

    $ uv run pytest -v -s --block-network --cov pystac_client --cov-report term-missing

The PySTAC Client tests use `vcrpy <https://vcrpy.readthedocs.io/en/latest/>`_ to mock API calls
with "pre-recorded" API responses. When adding new tests use the ``@pytest.mark.vcr`` decorator
function to indicate ``vcrpy`` should be used. Record the new responses and commit them to the
repository.

.. code-block:: bash

    $ uv run pytest -v -s --record-mode new_episodes
    $ git add <new files here>
    $ git commit -a -m 'new test episodes'


To update PySTAC Client to use future versions of STAC API, the existing recorded API responses
should be "re-recorded":

.. code-block:: bash

    $ uv run pytest -v -s --record-mode rewrite --block-network
    $ git commit -a -m 'updated test episodes'


Code quality checks
^^^^^^^^^^^^^^^^^^^

`pre-commit <https://pre-commit.com/>`_ is used to ensure a standard set of formatting and
linting is run before every commit. These hooks should be installed with:

.. code-block:: bash

    $ uv run pre-commit install

These can then be run independent of a commit with:

.. code-block:: bash

    $ uv run pre-commit run --all-files

PySTAC Client uses

- `ruff <https://docs.astral.sh/ruff/>`_ for Python code formatting and style checks
- `codespell <https://github.com/codespell-project/codespell/>`_ to check code for common misspellings
- `doc8 <https://github.com/pycqa/doc8>`_ for style checking on RST files in the docs
- `mypy <http://www.mypy-lang.org/>`_ for Python type annotation checks

Once installed you can bypass pre-commit by adding the ``--no-verify`` (or ``-n``)
flag to Git commit commands, as in ``git commit --no-verify``.

Pull Requests
^^^^^^^^^^^^^

To make Pull Requests to PySTAC Client, the code must pass linting, formatting, and code tests. To run
the entire suit of checks and tests that will be run the GitHub Action Pipeline, use the ``test`` script.

.. code-block:: bash

    $ uv run scripts/test

If automatic formatting is desired (incorrect formatting will cause the GitHub Action to fail),
use the format script (which uses ruff) and commit the resulting files:

.. code-block:: bash

    $ uv run scripts/format
    $ git commit -a -m 'formatting updates'


To build the documentation, `install Pandoc <https://pandoc.org/installing.html>`_, install the
Python documentation requirements via uv, then use the ``build-docs`` script:

.. code-block:: bash

    $ uv pip install -e . --group docs
    $ uv run scripts/build-docs

CHANGELOG
^^^^^^^^^

PySTAC Client maintains a
`changelog  <https://github.com/stac-utils/pystac-client/blob/main/CHANGELOG.md>`_
to track changes between releases. All Pull Requests should make a changelog entry unless
the change is trivial (e.g. fixing typos) or is entirely invisible to users who may
be upgrading versions (e.g. an improvement to the CI system).

For changelog entries, please link to the PR of that change. This needs to happen in a
few steps:

- Make a Pull Request (see above) to PySTAC Client with your changes
- Record the link to the Pull Request
- Push an additional commit to your branch with the changelog entry with the link to the
  Pull Request.

For more information on changelogs and how to write a good entry, see `keep a changelog
<https://keepachangelog.com/en/1.0.0/>`_.

Benchmark
^^^^^^^^^

By default, PySTAC Client benchmarks are skipped during test runs.
To run the benchmarks, use the ``--benchmark-only`` flag:

.. code-block:: bash

    $ uv run pytest --benchmark-only
    ============================= test session starts ==============================
    platform darwin -- Python 3.9.13, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
    benchmark: 3.4.1 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
    rootdir: /Users/gadomski/Code/pystac-client, configfile: pytest.ini
    plugins: benchmark-3.4.1, recording-0.11.0, console-scripts-1.1.0, requests-mock-1.9.3, cov-2.11.1, typeguard-2.13.3
    collected 75 items

    tests/test_cli.py ss                                                     [  2%]
    tests/test_client.py ssssssssssssssss                                    [ 24%]
    tests/test_collection_client.py ss                                       [ 26%]
    tests/test_item_search.py ...sssssssssssssssssssssssssssssssssssssssssss [ 88%]
    s                                                                        [ 89%]
    tests/test_stac_api_io.py ssssssss                                       [100%]


    --------------------------------------------------------------------------------------- benchmark: 3 tests --------------------------------------------------------------------------------------
    Name (time in ms)                Min                 Max                Mean              StdDev              Median                IQR            Outliers     OPS            Rounds  Iterations
    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    test_single_item_search     213.4729 (1.0)      284.8732 (1.0)      254.9405 (1.0)       32.9424 (3.27)     271.0926 (1.0)      58.2907 (4.95)          1;0  3.9225 (1.0)           5           1
    test_single_item            314.6746 (1.47)     679.7592 (2.39)     563.9692 (2.21)     142.7451 (14.18)    609.5605 (2.25)     93.9942 (7.98)          1;1  1.7731 (0.45)          5           1
    test_requests               612.9212 (2.87)     640.5024 (2.25)     625.6871 (2.45)      10.0637 (1.0)      625.1143 (2.31)     11.7822 (1.0)           2;0  1.5982 (0.41)          5           1
    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    Legend:
    Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
    OPS: Operations Per Second, computed as 1 / Mean
    ======================== 3 passed, 72 skipped in 11.86s ========================


For more information on running and comparing benchmarks, see the `pytest-benchmark documentation <https://pytest-benchmark.readthedocs.io/en/latest/>`_.


================================================
FILE: docs/design/0001-record-architecture-decisions.md
================================================
# 1. Record architecture decisions

Date: 2021-03-01

## Status

Accepted

## Context

We need to record the architectural decisions made on this project.

## Decision

We will use Architecture Decision Records, as 
[described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).

## Consequences

See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's 
[adr-tools](https://github.com/npryce/adr-tools).


================================================
FILE: docs/design/0002-choose-stac-library.md
================================================
# 2. Choose STAC library

Date: 2021-03-01

## Status

Accepted

## Context

We would like to use an existing Python library for working with STAC objects as Python objects. Ideally this library 
will meet the following requirements:

* **Actively maintained** to ensure that it is up-to-date with the latest STAC spec
* **Well-documented** to reduce the documentation burden for this library
* **Easily extensible** to allow us to take advantage of existing STAC object functionality

The 2 most obvious choices for Python STAC clients are 
[`PySTAC`](https://github.com/stac-utils/pystac) and [`sat-stac`](https://github.com/sat-utils/sat-stac). 

### `PySTAC`

`PySTAC` is an actively maintained STAC client for Python 3. Its last release was less than 2 months ago and its last 
commit was less than 2 weeks ago. It has had 15 contributors within the last year and it supports the latest release 
candidate for the STAC spec (as well as previous releases). `PySTAC` hosts 
[documentation on ReadTheDocs](https://pystac.readthedocs.io/en/latest/) that includes a description of programming 
concepts related to the library, quickstart instructions, tutorials, and an API reference.

`PySTAC` supports both reading and writing of STAC objects. Extending `PySTAC` classes through inheritance is made a 
bit more difficult by the fact that some class methods (like ``from_dict``) have references to specific classes 
(like ``Catalog``) rather than using the ``cls`` argument. This will require us to overwrite some of these methods to 
implement inheritance. It has robust support for traversing catalogs using links. There is no existing support for the 
STAC API spec in `PySTAC` or any related tooling.

### `sat-stac`

`sat-stac` is an actively maintained STAC client for Python 3. Its last release was less than 2 months ago and its 
last commit was at that same time. It has had 2 contributors within the last year and it supports the latest release 
candidate for the STAC spec (as well as previous releases). `sat-stac` has installation documented in the GitHub repo's 
main README and has 2 tutorials in the form of Jupyter Notebooks in that repo. 

`sat-stac` supports reading STAC catalogs (support for writing STAC catalogs was removed in v0.4.0). There is also an 
existing library for working with STAC API - Item Search results called 
[sat-search](https://github.com/sat-utils/sat-search) that uses `sat-stac` as its backend. 

## Decision

We will use `PySTAC` as our "backend" for working with STAC objects in Python. While both libraries are well-maintained, 
`PySTAC` has more thorough documentation and seems to be more widely supported through community contribution. 

## Consequences

We will need to create our own implementation of a STAC API - Item Search client since there is not existing tooling 
based on `PySTAC`. We will be able to point to the hosted documentation for `PySTAC` to guide users through existing 
functionality for navigating STAC Catalogs. Special care should be taken to ensure that we do not break any of 
`PySTAC`'s functionality through inheritance.


================================================
FILE: docs/design/design_decisions.rst
================================================
Design Decisions
================

`Architectural Design Records (ADRs)
<https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions>`__ for major
design decisions related to the library. In general, this library makes an attempt to
follow the design patterns laid out in the PySTAC library.

.. toctree::
   :glob:
   :maxdepth: 1

   *


================================================
FILE: docs/index.rst
================================================
PySTAC Client Documentation
===========================

PySTAC Client is a Python package for working with `STAC <https://github.com/radiantearth/stac-spec>`__
Catalogs and `STAC APIs <https://github.com/radiantearth/stac-api-spec>`__.
PySTAC Client builds upon PySTAC through higher-level functionality and ability to
access STAC API search endpoints.

STAC Versions
=============

+---------------+---------------+-----------------------------+
| pystac-client |   STAC spec   |        STAC API Spec        |
+===============+===============+=============================+
| 0.9.x         | 1.0.x - 1.1.x | 1.0.0-beta.1 - 1.0.0        |
+---------------+---------------+-----------------------------+
| 0.8.x         | 1.0.x         | 1.0.0-beta.1 - 1.0.0        |
+---------------+---------------+-----------------------------+
| 0.7.x         | 1.0.x         | 1.0.0-beta.1 - 1.0.0        |
+---------------+---------------+-----------------------------+
| 0.6.x         | 1.0.x         | 1.0.0-beta.1 - 1.0.0-rc.2   |
+---------------+---------------+-----------------------------+
| 0.5.x         | 1.0.x         | 1.0.0-beta.1 - 1.0.0-rc.1   |
+---------------+---------------+-----------------------------+
| 0.4.x         | 1.0.x         | 1.0.0-beta.1 - 1.0.0-rc.1   |
+---------------+---------------+-----------------------------+
| 0.3.x         | 1.0.x         | 1.0.0-beta.1 - 1.0.0-beta.4 |
+---------------+---------------+-----------------------------+
| 0.2.x         | 1.0.x         | 1.0.0-beta.1 - 1.0.0-beta.2 |
+---------------+---------------+-----------------------------+

Installation
------------

``pystac-client`` requires `Python >=3.10 <https://www.python.org/>`__.

To install with pip, run:

.. code-block:: console

   $ python -m pip install pystac-client

This will install the dependencies :doc:`pystac <pystac:index>`,
:doc:`python-dateutil <dateutil:index>`, and :doc:`requests <requests:index>`.

Acknowledgements
----------------

This package builds upon the great work of the PySTAC library for working with
STAC objects in Python. It also uses concepts from the
`sat-search <https://github.com/sat-utils/sat-search>`__ library for working with STAC
API search endpoints.

Table of Contents
-----------------

.. toctree::
   :maxdepth: 2

   quickstart
   usage
   api
   tutorials
   contributing
   design/design_decisions


================================================
FILE: docs/quickstart.rst
================================================
Quickstart
----------

PySTAC Client can be used as either a Command Line Interface (CLI) or a
Python library.

CLI
~~~

Use the CLI to quickly perform Item or Collection searches and
output or save the results.

The ``--matched`` switch performs a search with limit=1 so does not get
any Items, but gets the total number of matches which will be output to
the screen (if supported by the STAC API).

.. code-block:: console

    $ stac-client search https://earth-search.aws.element84.com/v1 -c sentinel-2-l2a --bbox -72.5 40.5 -72 41 --matched
    3141 items matched

The ``--matched`` flag can also be used for collection search to get
the total number of collections that match your search terms.


.. code-block:: console

    $ stac-client collections https://emc.spacebel.be --q sentinel-2 --matched
    76 collections matched

If the same URL is to be used over and over, define an environment
variable to be used in the CLI call:

.. code-block:: console

    $ export STAC_API_URL=https://earth-search.aws.element84.com/v1
    $ stac-client search ${STAC_API_URL} -c sentinel-2-l2a --bbox -72.5 40.5 -72 41 --datetime 2020-01-01/2020-01-31 --matched
    48 items matched

Without the ``--matched`` switch, all items will be fetched, paginating
if necessary. If the ``--max-items`` switch is provided it will stop
paging once that many items has been retrieved. It then prints all items
to stdout as an ItemCollection. This can be useful to pipe output to
another process such as
`stac-terminal <https://github.com/stac-utils/stac-terminal>`__,
`geojsonio-cli <https://github.com/mapbox/geojsonio-cli>`__, or
`jq <https://stedolan.github.io/jq/>`__.

.. code-block:: console

    $ stac-client search ${STAC_API_URL} -c sentinel-2-l2a --bbox -72.5 40.5 -72 41 --datetime 2020-01-01/2020-01-31 | stacterm cal --label platform

.. figure:: images/stacterm-cal.png
   :alt:

If the ``--save`` switch is provided instead, the results will not be
output to stdout, but instead will be saved to the specified file.

.. code-block:: console

    $ stac-client search ${STAC_API_URL} -c sentinel-2-l2a --bbox -72.5 40.5 -72 41 --datetime 2020-01-01/2020-01-31 --save items.json

If the Catalog supports the `Query
extension <https://github.com/radiantearth/stac-api-spec/tree/master/fragments/query>`__,
any Item property can also be included in the search. Rather than
requiring the JSON syntax the Query extension uses, pystac-client uses a
simpler syntax that it will translate to the JSON equivalent. Note
however that when the simple syntax is used it sends all property values
to the server as strings, except for ``gsd`` which it casts to
``float``. This means that if there are extensions in use with numeric
properties these will be sent as strings. Some servers may automatically
cast this to the appropriate data type, others may not.

The query filter will also accept complete JSON as per the specification.

::

    <property><operator><value>

    where operator is one of `>=`, `<=`, `>`, `<`, `=`

    Examples:
    eo:cloud_cover<10
    created=2021-01-06
    view:sun_elevation<20

Any number of properties can be included, and each can be included more
than once to use additional operators.

.. code-block:: console

    $ stac-client search ${STAC_API_URL} -c sentinel-2-l2a --bbox -72.5 40.5 -72 41 --datetime 2020-01-01/2020-01-31 ---query "eo:cloud_cover<10" --matched
    10 items matched

.. code-block:: console

    $ stac-client search ${STAC_API_URL} -c sentinel-2-l2a --bbox -72.5 40.5 -72 41 --datetime 2020-01-01/2020-01-31 --query "eo:cloud_cover<10" "eo:cloud_cover>5" --matched
    4 items matched


Collection searches can also use multiple filters like this example
search for collections that include the term ``"biomass"`` and have
a spatial extent that intersects Scandinavia.

.. code-block:: console

    $ stac-client collections https://emc.spacebel.be --q biomass --bbox 0.09 54.72 33.31 71.36  --matched
    43 items matched

Since most STAC APIs have not yet implemented the `collection search 
extension <https://github.com/stac-api-extensions/collection-search>`__, 
``pystac-client`` will perform a limited client-side 
filter on the full list of collections using only the ``bbox``, 
``datetime``, and ``q`` (free-text search) parameters.
In the case that the STAC API does not support collection search, a
warning will be displayed to inform you that the filter is being
applied client-side.


Python
~~~~~~

First, create a Client instance configured to use a specific STAC API by the root URL of that API. For this example, we
will use `Earth Search <https://earth-search.aws.element84.com/v1>`__.

.. code-block:: python

    from pystac_client import Client

    client = Client.open("https://earth-search.aws.element84.com/v1")

Create an Item Search instance that represents a search to run. This does not actually run a search yet --
that does not happen until a method is called that requires data from the STAC API.

.. code-block:: python

    search = client.search(
        max_items=10,
        collections=["sentinel-2-c1-l2a"],
        bbox=[-72.5,40.5,-72,41]
    )

Calling ``matched()`` will send a request to the STAC API and retrieve a single item and metadata about how many Items
match the search criteria.

.. code-block:: python

    print(f"{search.matched()} items found")

The ``items()`` iterator method can be used to iterate through all resulting items. This iterator
hides the pagination behavior that the may occur if there are sufficient results. Be careful with this
method -- you could end up iterating over the entire catalog if ``max_items`` is not set!

.. code-block:: python

    for item in search.items():
        print(item.id)

Use ``item_collection()`` to convert all Items from a search into a single `PySTAC
ItemCollection <https://pystac.readthedocs.io/en/latest/api/pystac.html#pystac.ItemCollection>`__.
The ``ItemCollection`` can then be saved as a GeoJSON FeatureCollection. This requires retrieving all
of the results from the search, so it may take some time to retrieve all the paginated responses.

.. code-block:: python

    item_collection = search.item_collection()
    item_collection.save_object("my_itemcollection.json")

Some STAC APIs also implement the Collection Search Extension. Earth Search does not, so we use the
ORNL_CLOUD CMR-STAC Catalog instead:

.. code-block:: python

    client = Client.open("https://cmr.earthdata.nasa.gov/stac/ORNL_CLOUD")
    collection_search = client.collection_search(
        q="rain",
    )
    print(f"{collection_search.matched()} collections found")


The ``collections()`` iterator method can be used to iterate through all
resulting collections.

.. code-block:: python

    for collection in collection_search.collections():
        print(collection.id)



================================================
FILE: docs/tutorials/aoi-coverage.ipynb
================================================
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "e06a27bf",
   "metadata": {},
   "source": [
    "# Calculating Coverage Percentage of the AOI by an Item\n",
    "\n",
    "This notebook demonstrates the use of pystac-client to calculate the percentage an Item's geometry that intesects with the area of interest (AOI) specified in the search by the `intersects` parameter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b65de617",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Any, Dict\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "from pystac.item import Item\n",
    "from shapely.geometry import shape\n",
    "\n",
    "from pystac_client import Client"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98942e75",
   "metadata": {},
   "outputs": [],
   "source": [
    "def intersection_percent(item: Item, aoi: Dict[str, Any]) -> float:\n",
    "    \"\"\"The percentage that the Item's geometry intersects the AOI. An Item that\n",
    "    completely covers the AOI has a value of 100.\n",
    "    \"\"\"\n",
    "    geom_item = shape(item.geometry)\n",
    "    geom_aoi = shape(aoi)\n",
    "\n",
    "    intersected_geom = geom_aoi.intersection(geom_item)\n",
    "\n",
    "    intersection_percent = (intersected_geom.area * 100) / geom_aoi.area\n",
    "\n",
    "    return intersection_percent\n",
    "\n",
    "\n",
    "# STAC API root URL\n",
    "URL = \"https://planetarycomputer.microsoft.com/api/stac/v1\"\n",
    "\n",
    "# geometry of the AOI to search over\n",
    "intersects_geometry = {\n",
    "    \"type\": \"Polygon\",\n",
    "    \"coordinates\": [\n",
    "        [\n",
    "            [-73.21, 43.99],\n",
    "            [-73.21, 47.05],\n",
    "            [-70.12, 47.05],\n",
    "            [-70.12, 43.99],\n",
    "            [-73.21, 43.99],\n",
    "        ]\n",
    "    ],\n",
    "}\n",
    "\n",
    "# Create a Client and an ItemSearch representing our search\n",
    "# No search operations will be performed until we call the items() method\n",
    "client = Client.open(URL)\n",
    "item_search = client.search(\n",
    "    collections=[\"sentinel-2-l2a\"], intersects=intersects_geometry, max_items=100\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0b0883fe",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\n",
    "    [\n",
    "        f\"{intersection_percent(item, intersects_geometry):.2f}\"\n",
    "        for item in item_search.items()\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03254847",
   "metadata": {},
   "outputs": [],
   "source": [
    "# create a generator that filters to only those Items that intersect more than 5%\n",
    "items_gt_5_percent = (\n",
    "    i for i in item_search.items() if intersection_percent(i, intersects_geometry) > 5\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e1de7a09",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Render the AOI and Item results\n",
    "# The green shape is the AOI\n",
    "# The blue shapes are the Item geometries\n",
    "# If there are no blue shapes, adjust the intersection percent filter above\n",
    "# until there are\n",
    "\n",
    "cm = plt.get_cmap(\"RdBu\")\n",
    "fig, axs = plt.subplots()\n",
    "axs.set_aspect(\"equal\", \"datalim\")\n",
    "\n",
    "for item in items_gt_5_percent:\n",
    "    xs, ys = shape(item.geometry).exterior.xy\n",
    "    axs.fill(xs, ys, alpha=0.5, fc=\"b\", ec=\"none\")\n",
    "\n",
    "geom_intersects = shape(intersects_geometry)\n",
    "xs, ys = geom_intersects.exterior.xy\n",
    "axs.fill(xs, ys, alpha=0.5, fc=\"g\", ec=\"none\")\n",
    "\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "interpreter": {
   "hash": "6b6313dbab648ff537330b996f33bf845c0da10ea77ae70864d6ca8e2699c7ea"
  },
  "kernelspec": {
   "display_name": "Python 3.9.11 ('.venv': venv)",
   "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.9.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}


================================================
FILE: docs/tutorials/authentication.md
================================================
# Authentication

While not integrated into this library directly, pystac-client provides a series of hooks that support a wide variety of authentication mechanisms. These can be used when interacting with stac API implementations behind various authorization walls.

## Basic auth

Pystac-client supports HTTP basic authentication by simply exposing the ability to define headers to be used when sending requests.  Simply encode the token and provide the header.

```python
import base64
import pystac_client

# encode credentials
user_name = "yellowbeard"
password = "yaarg"
userpass = f"{user_name}:{password}"
b64_userpass = base64.b64encode(userpass.encode()).decode()

# create the client
client = pystac_client.Client.open(
    url="https://planetarycomputer.microsoft.com/api/stac/v1",
    headers={
        'Authorization': f"Basic {b64_userpass}"
    }
)
```

## Token auth

Providing a authentication token can be accomplished using the same mechanism as described above for [basic auth](#basic-auth). Simply provide the token in the `Authorization` header to the client in the same manner.

## AWS SigV4

Accessing a stac api protected by AWS IAM often requires signing the request using [AWS SigV4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html). Unlike basic and token authentication, the entire request is part of the signing process. Thus the `Authorization` header cannot be added when the client is created, rather it must be generated and added after the request is fully formed.

Pystac-client provides a lower-level hook, the `request_modifier` parameter, which can mutate the request, adding the necessary header after the request has been generated but before it is sent.

The code cell below demonstrates this, using the `boto3` module.

```python
import boto3
import botocore.auth
import botocore.awsrequest
import pystac_client
import requests

# Details regarding the private stac api
region = "us-east-1"
service_name = "execute-api"
endpoint_id = "xxxxxxxx"
deployment_stage = "dev"
stac_api_url = f"https://{endpoint_id}.{service_name}.{region}.amazonaws.com/{deployment_stage}"

# load AWS credentials
credentials = boto3.Session(region_name=region).get_credentials()
signer = botocore.auth.SigV4Auth(credentials, service_name, region)

def sign_request(request: requests.Request) -> requests.Request:
    """Sign the request using AWS SigV4.

    Args:
        request (requests.Request): The fully populated request to sign.

    Returns:
        requests.Request: The provided request object, with auth header added.
    """
    aws_request = botocore.awsrequest.AWSRequest(
        method=request.method,
        url=request.url,
        params=request.params,
        data=request.data,
        headers=request.headers
    )
    signer.add_auth(aws_request)
    request.headers = aws_request.headers
    return request

# create the client
client = pystac_client.Client.open(
    url=stac_api_url,
    request_modifier=sign_request
)
```


================================================
FILE: docs/tutorials/cql2-filter.ipynb
================================================
{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "e06a27bf",
   "metadata": {},
   "source": [
    "# CQL2 Filtering\n",
    "\n",
    "This notebook demonstrates using pystac-client to filter STAC items with [CQL2](https://docs.ogc.org/is/21-065r2/21-065r2.html) as described in the [STAC API Filter Extension](https://github.com/stac-api-extensions/filter). \n",
    "\n",
    "Note: Not all STAC APIs support the Filter Extension. APIs advertise conformance by including `https://api.stacspec.org/v1.0.0/item-search#filter` in the `conformsTo` attribute of the root API."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b65de617",
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "\n",
    "import geopandas as gpd\n",
    "import pandas as pd\n",
    "\n",
    "from pystac_client import Client"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8ac88bb",
   "metadata": {},
   "source": [
    "The first step as always with pystac-client is opening the catalog:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98942e75",
   "metadata": {},
   "outputs": [],
   "source": [
    "# STAC API root URL\n",
    "URL = \"https://planetarycomputer.microsoft.com/api/stac/v1\"\n",
    "\n",
    "catalog = Client.open(URL)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e16077c",
   "metadata": {},
   "source": [
    "## Initial Search Parameters\n",
    "\n",
    "Here we set up some initial search parameters to use with the `Client.search` function. We are providing a maximum number of items to return (`max_items`), a collection to look within (`collections`), a geometry (`intersects`), and a datetime range (`datetime`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5e961981",
   "metadata": {},
   "outputs": [],
   "source": [
    "# AOI around Delfzijl, in the north of The Netherlands\n",
    "geom = {\n",
    "    \"type\": \"Polygon\",\n",
    "    \"coordinates\": [\n",
    "        [\n",
    "            [6.42425537109375, 53.174765470134616],\n",
    "            [7.344360351562499, 53.174765470134616],\n",
    "            [7.344360351562499, 53.67393435835391],\n",
    "            [6.42425537109375, 53.67393435835391],\n",
    "            [6.42425537109375, 53.174765470134616],\n",
    "        ]\n",
    "    ],\n",
    "}\n",
    "\n",
    "params = {\n",
    "    \"max_items\": 100,\n",
    "    \"collections\": \"landsat-8-c2-l2\",\n",
    "    \"intersects\": geom,\n",
    "    \"datetime\": \"2018-01-01/2020-12-31\",\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d6f1dd5f",
   "metadata": {},
   "source": [
    "## Using Filters\n",
    "\n",
    "In addition to the parameters described above in the following examples we will filter by Item properties (`filter`) using CQL2-JSON. Here is a little function that does the search constructs a `GeoDataFrame` of the results and then plots `datetime` vs `eo:cloud_cover`.\n",
    "\n",
    "Remember that in this whole notebook we are only looking at STAC metadata, there is no part where we are reading the data itself."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8b26e89b",
   "metadata": {},
   "outputs": [],
   "source": [
    "def search_and_plot(filter):\n",
    "    search = catalog.search(**params, filter=filter)\n",
    "\n",
    "    gdf = gpd.GeoDataFrame.from_features(search.item_collection_as_dict())\n",
    "    gdf[\"datetime\"] = pd.to_datetime(gdf[\"datetime\"])\n",
    "    print(f\"Found {len(gdf)} items\")\n",
    "\n",
    "    gdf.plot.line(x=\"datetime\", y=\"eo:cloud_cover\", title=json.dumps(filter))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "11afcc19",
   "metadata": {},
   "source": [
    "We can test out the function by passing an empty dict to do no filtering at all."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b6293c11",
   "metadata": {},
   "outputs": [],
   "source": [
    "search_and_plot({})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "44d3bc04",
   "metadata": {},
   "source": [
    "## CQL2 Filters\n",
    "\n",
    "We will use `eo:cloud_cover` as an example and filter for all the STAC Items where `eo:cloud_cover <= 10%`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dfc0e759",
   "metadata": {},
   "outputs": [],
   "source": [
    "filter = {\"op\": \"<=\", \"args\": [{\"property\": \"eo:cloud_cover\"}, 10]}\n",
    "\n",
    "search_and_plot(filter)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75e835f1",
   "metadata": {},
   "source": [
    "Next let's look for all the STAC Items where `eo:cloud_cover >= 80%`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9c2f9ca1",
   "metadata": {},
   "outputs": [],
   "source": [
    "filter = {\"op\": \">=\", \"args\": [{\"property\": \"eo:cloud_cover\"}, 80]}\n",
    "\n",
    "search_and_plot(filter)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0ad984bf",
   "metadata": {},
   "source": [
    "We can combine multiple CQL2 statements to express more complicated logic:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "109f673c",
   "metadata": {},
   "outputs": [],
   "source": [
    "filter = {\n",
    "    \"op\": \"and\",\n",
    "    \"args\": [\n",
    "        {\"op\": \"<=\", \"args\": [{\"property\": \"eo:cloud_cover\"}, 60]},\n",
    "        {\"op\": \">=\", \"args\": [{\"property\": \"eo:cloud_cover\"}, 40]},\n",
    "    ],\n",
    "}\n",
    "\n",
    "search_and_plot(filter)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "617c7416",
   "metadata": {},
   "source": [
    "You can see the power of this syntax. Indeed we can replace `datetime` and `intersects` from our original search parameters with a more complex CQL2 statement."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7b0dc965",
   "metadata": {},
   "outputs": [],
   "source": [
    "filter = {\n",
    "    \"op\": \"and\",\n",
    "    \"args\": [\n",
    "        {\"op\": \"s_intersects\", \"args\": [{\"property\": \"geometry\"}, geom]},\n",
    "        {\"op\": \">=\", \"args\": [{\"property\": \"datetime\"}, \"2018-01-01\"]},\n",
    "        {\"op\": \"<=\", \"args\": [{\"property\": \"datetime\"}, \"2020-12-31\"]},\n",
    "        {\"op\": \"<=\", \"args\": [{\"property\": \"eo:cloud_cover\"}, 60]},\n",
    "        {\"op\": \">=\", \"args\": [{\"property\": \"eo:cloud_cover\"}, 40]},\n",
    "    ],\n",
    "}\n",
    "search = catalog.search(max_items=100, collections=\"landsat-8-c2-l2\", filter=filter)\n",
    "\n",
    "print(f\"Found {len(search.item_collection())} items\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "56503c7b",
   "metadata": {},
   "source": [
    "### CQL2 Text\n",
    "\n",
    "The examples above all use CQL2-json but pystac-client also supports passing `filter` as CQL2 text.\n",
    "\n",
    "NOTE: As of right now in pystac-client if you use CQL2 text you need to change the search HTTP method to GET."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5e8f62f5",
   "metadata": {},
   "outputs": [],
   "source": [
    "search = catalog.search(**params, method=\"GET\", filter=\"eo:cloud_cover<=10\")\n",
    "\n",
    "print(f\"Found {len(search.item_collection())} items\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b865c1f",
   "metadata": {},
   "source": [
    "Just like CQL2 json, CQL2 text statements can be combined to express more complex logic:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c06f40cf",
   "metadata": {},
   "outputs": [],
   "source": [
    "search = catalog.search(\n",
    "    **params, method=\"GET\", filter=\"eo:cloud_cover<=60 and eo:cloud_cover>=40\"\n",
    ")\n",
    "\n",
    "print(f\"Found {len(search.item_collection())} items\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "35cbf612",
   "metadata": {},
   "source": [
    "## Queryables\n",
    "\n",
    "pystac-client provides a method for accessing all the arguments that can be used within CQL2 filters for a particular collection. These are provided as a json schema document, but for readability we are mostly interested in the names of the fields within `properties`.\n",
    "\n",
    "NOTE: When getting the collection, you might notice that we use \"landsat-c2-l2\" as the collection id rather than \"landsat-8-c2-l2\". This is because \"landsat-8-c2-l2\" doesn't actually exist as a collection. It is just used in some places as a collection id on items. This is likely a remnant of some former setup in the Planetary Computer STAC."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "90f1cc6d",
   "metadata": {},
   "outputs": [],
   "source": [
    "collection = catalog.get_collection(\"landsat-c2-l2\")\n",
    "queryables = collection.get_queryables()\n",
    "\n",
    "list(queryables[\"properties\"].keys())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c407ffec",
   "metadata": {},
   "source": [
    "## Read More\n",
    "\n",
    "- For more involved CQL2 examples in a STAC context read the [STAC API Filter Extension Examples](https://github.com/stac-api-extensions/filter?tab=readme-ov-file#examples)\n",
    "\n",
    "- For examples of all the different CQL2 operations take a look at the [playground on the CQL2-rs docs](https://developmentseed.org/cql2-rs/latest/playground/)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}


================================================
FILE: docs/tutorials/item-search-intersects.ipynb
================================================
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "e06a27bf",
   "metadata": {},
   "source": [
    "# Item Search with Intersects\n",
    "\n",
    "This notebook shows the use of pystac-client to perform item search with the `intersects` parameter, to restrict the results to an Area of Interest (AOI)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e16077c",
   "metadata": {},
   "source": [
    "# Client\n",
    "\n",
    "We first connect to an API by retrieving the root catalog, or landing page, of the API with the `Client.open` function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98942e75",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "import json\n",
    "from typing import Any, Dict\n",
    "\n",
    "from pystac_client import Client\n",
    "\n",
    "# STAC API root URL\n",
    "URL = \"https://planetarycomputer.microsoft.com/api/stac/v1\"\n",
    "\n",
    "client = Client.open(URL)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62e26114",
   "metadata": {},
   "source": [
    "# Item Search\n",
    "\n",
    "When the Catalog is a STAC API, we have the ability to search for items based on spatio-temporal properties.\n",
    "\n",
    "The STAC API endpoint `/search` accepts a parameter `intersects` that is a GeoJSON Geometry representing the AOI of the search.\n",
    "\n",
    "The `search` method of the pystac_client `Client` class accepts several different types of objects:\n",
    "\n",
    "1. a string representing a GeoJSON geometry\n",
    "2. a dictionary representing a GeoJSON geometry\n",
    "3. any object that implements a ``__geo_interface__`` property, [an informal specification](https://gist.github.com/sgillies/2217756)    \n",
    "   supported by several libraries for generating a GeoJSON representation from an object. Several prominent libraries support this\n",
    "   protocol for their objects that represent geometries, including [Shapely](https://shapely.readthedocs.io), [ArcPy](https://pro.arcgis.com/en/pro-app/2.8/arcpy/get-started/what-is-arcpy-.htm), [PySAL](https://pysal.org/), [geojson](https://github.com/jazzband/geojson), [pyshp](https://pypi.org/project/pyshp/), [descartes](https://docs.descarteslabs.com/), and [pygeoif](https://github.com/cleder/pygeoif)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d27fabf",
   "metadata": {},
   "source": [
    "## Item Search with AOI as a Dictionary"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d8af6334",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# AOI around Delfzijl, in northern Netherlands\n",
    "aoi_as_dict: Dict[str, Any] = {\n",
    "    \"type\": \"Polygon\",\n",
    "    \"coordinates\": [[[6, 53], [7, 53], [7, 54], [6, 54], [6, 53]]],\n",
    "}\n",
    "\n",
    "search = client.search(\n",
    "    max_items=25,\n",
    "    collections=\"aster-l1t\",\n",
    "    intersects=aoi_as_dict,\n",
    ")\n",
    "\n",
    "print(f\"AOI as dictionary, found {len(list(search.items()))} items\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9da4956b",
   "metadata": {},
   "source": [
    "## Item Search with AOI as a String"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9a7c9336",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "aoi_as_str: str = json.dumps(aoi_as_dict)\n",
    "\n",
    "search = client.search(\n",
    "    max_items=25,\n",
    "    collections=\"aster-l1t\",\n",
    "    intersects=aoi_as_str,\n",
    ")\n",
    "\n",
    "print(f\"AOI as string, found {len(list(search.items()))} items\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f22ffb5a",
   "metadata": {},
   "source": [
    "## Item Search with AOI as a Shapely Geometry Object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a44598ef",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "import shapely.geometry\n",
    "\n",
    "aoi_as_shapely_shape = shapely.geometry.shape(aoi_as_dict)\n",
    "\n",
    "search = client.search(\n",
    "    max_items=25,\n",
    "    collections=\"aster-l1t\",\n",
    "    intersects=aoi_as_shapely_shape,\n",
    ")\n",
    "\n",
    "print(\n",
    "    \"AOI as Shapely Geometry object from shape(), \"\n",
    "    f\"found {len(list(search.items()))} items\"\n",
    ")\n",
    "\n",
    "aoi_as_shapely_polygon = shapely.geometry.Polygon(aoi_as_dict[\"coordinates\"][0])\n",
    "\n",
    "search = client.search(\n",
    "    max_items=25,\n",
    "    collections=\"aster-l1t\",\n",
    "    intersects=aoi_as_shapely_polygon,\n",
    ")\n",
    "\n",
    "print(\n",
    "    \"AOI as Shapely Geometry object with Polygon, \"\n",
    "    f\"found {len(list(search.items()))} items\"\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f236254",
   "metadata": {},
   "source": [
    "## Item Search with AOI as a \"geojson\" library object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84ce4395",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "import geojson\n",
    "\n",
    "aoi_as_geojson_polygon = geojson.Polygon(aoi_as_dict[\"coordinates\"])\n",
    "\n",
    "search = client.search(\n",
    "    max_items=25,\n",
    "    collections=\"aster-l1t\",\n",
    "    intersects=aoi_as_geojson_polygon,\n",
    ")\n",
    "\n",
    "print(f\"AOI as geojson Polygon, found {len(list(search.items()))} items\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3fe9c255",
   "metadata": {},
   "source": [
    "## Item Search with AOI as a pygeoif Object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a27dcb72",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "import pygeoif\n",
    "\n",
    "aoi_as_pygeoif_polygon = pygeoif.geometry.Polygon(aoi_as_dict[\"coordinates\"][0])\n",
    "\n",
    "search = client.search(\n",
    "    max_items=25,\n",
    "    collections=\"aster-l1t\",\n",
    "    intersects=aoi_as_pygeoif_polygon,\n",
    ")\n",
    "\n",
    "print(f\"AOI as pygeoif Polygon, found {len(list(search.items()))} items\")"
   ]
  }
 ],
 "metadata": {
  "interpreter": {
   "hash": "6b6313dbab648ff537330b996f33bf845c0da10ea77ae70864d6ca8e2699c7ea"
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.11.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}


================================================
FILE: docs/tutorials/pystac-client-introduction.ipynb
================================================
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "e06a27bf",
   "metadata": {},
   "source": [
    "# PySTAC-Client Introduction\n",
    "\n",
    "This notebook shows basic use of pystac-client to open an API, iterate through Collections and Items, and perform simple spatio-temporal searches."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b65de617",
   "metadata": {},
   "outputs": [],
   "source": [
    "# set pystac_client logger to DEBUG to see API calls\n",
    "import logging\n",
    "\n",
    "from pystac_client import Client\n",
    "\n",
    "logging.basicConfig()\n",
    "logger = logging.getLogger(\"pystac_client\")\n",
    "logger.setLevel(logging.DEBUG)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e16077c",
   "metadata": {},
   "source": [
    "# Client\n",
    "\n",
    "We first connect to an API by retrieving the root catalog, or landing page, of the API with the `Client.open` function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98942e75",
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# STAC API root URL\n",
    "URL = \"https://planetarycomputer.microsoft.com/api/stac/v1\"\n",
    "\n",
    "# custom headers\n",
    "headers = []\n",
    "\n",
    "cat = Client.open(URL, headers=headers)\n",
    "cat"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f6efbb86",
   "metadata": {},
   "source": [
    "# CollectionClient\n",
    "\n",
    "As with a static catalog the `get_collections` function will iterate through the Collections in the Catalog. Notice that because this is an API it can get all the Collections through a single call, rather than having to fetch each one individually."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bb7693fb",
   "metadata": {},
   "outputs": [],
   "source": [
    "for collection in cat.get_collections():\n",
    "    print(collection)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ebab2724-cab3-4fba-b25b-fdfb4e537014",
   "metadata": {},
   "source": [
    "# Collection Search\n",
    "\n",
    "Sometimes, it can be challenging to identify which collection you want to work with. The `collection_search` method allows you to discover collections by applying search filters that will help you find the specific collection(s) you need. Since many STAC APIs have not implemented the collection search extension, `pystac-client` will perform a limited client-side filter if the API does not conform to the collection search spec."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a23a53ec-5b5f-421d-9f0e-01dbde8c3697",
   "metadata": {},
   "outputs": [],
   "source": [
    "collection_search = cat.collection_search(\n",
    "    q=\"ASTER\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "90b3d014-9c8f-4c5b-a94e-bfb7f17380ad",
   "metadata": {},
   "source": [
    "The `collections` method lets you iterate through the results of the search so you can inspect the details of matching collections."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "006f13fd-5e58-4f3f-bd5a-707cd830caa1",
   "metadata": {},
   "outputs": [],
   "source": [
    "for result in collection_search.collections():\n",
    "    print(result.id, f\"{collection.description}\", sep=\"\\n\")\n",
    "    print(\"\\n\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fef20a46",
   "metadata": {},
   "outputs": [],
   "source": [
    "collection = cat.get_collection(\"aster-l1t\")\n",
    "collection"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "47a540fe",
   "metadata": {},
   "source": [
    "# Items\n",
    "\n",
    "The main functions for getting items return iterators, where pystac-client will handle retrieval of additional pages when needed. Note that one request is made for the first ten items, then a second request for the next ten."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17d6de4b",
   "metadata": {},
   "outputs": [],
   "source": [
    "items = collection.get_items()\n",
    "\n",
    "\n",
    "# flush stdout so we can see the exact order that things happen\n",
    "def get_ten_items(items):\n",
    "    for i, item in enumerate(items):\n",
    "        print(f\"{i}: {item}\", flush=True)\n",
    "        if i == 9:\n",
    "            return\n",
    "\n",
    "\n",
    "print(\"First page\", flush=True)\n",
    "get_ten_items(items)\n",
    "\n",
    "print(\"Second page\", flush=True)\n",
    "get_ten_items(items)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62e26114",
   "metadata": {},
   "source": [
    "# API Search\n",
    "\n",
    "If the Catalog is an API, we have the ability to search for items based on spatio-temporal properties."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d8af6334",
   "metadata": {},
   "outputs": [],
   "source": [
    "# AOI around Delfzijl, in northern Netherlands\n",
    "geom = {\n",
    "    \"type\": \"Polygon\",\n",
    "    \"coordinates\": [\n",
    "        [\n",
    "            [6.42425537109375, 53.174765470134616],\n",
    "            [7.344360351562499, 53.174765470134616],\n",
    "            [7.344360351562499, 53.67393435835391],\n",
    "            [6.42425537109375, 53.67393435835391],\n",
    "            [6.42425537109375, 53.174765470134616],\n",
    "        ]\n",
    "    ],\n",
    "}\n",
    "\n",
    "# limit sets the # of items per page so we can see multiple pages getting fetched\n",
    "search = cat.search(\n",
    "    max_items=15,\n",
    "    limit=5,\n",
    "    collections=\"aster-l1t\",\n",
    "    intersects=geom,\n",
    "    datetime=\"2000-01-01/2010-12-31\",\n",
    ")\n",
    "\n",
    "items = list(search.items())\n",
    "\n",
    "len(items)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "275f316f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# note that this will work in JupyterLab, but not in a Jupyter Notebook\n",
    "\n",
    "import IPython.display\n",
    "\n",
    "IPython.display.JSON([i.to_dict() for i in items])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "68bf65f2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# this cell can be used in Jupyter Notebook. Use above if using JupyterLab\n",
    "\n",
    "import json\n",
    "import uuid\n",
    "\n",
    "from IPython.display import display_html, display_javascript\n",
    "\n",
    "\n",
    "class RenderJSON(object):\n",
    "    def __init__(self, json_data):\n",
    "        if isinstance(json_data, dict) or isinstance(json_data, list):\n",
    "            self.json_str = json.dumps(json_data)\n",
    "        else:\n",
    "            self.json_str = json_data\n",
    "        self.uuid = str(uuid.uuid4())\n",
    "\n",
    "    def _ipython_display_(self):\n",
    "        display_html(\n",
    "            '<div id=\"{}\" style=\"height: 600px; width:100%;font: 12px/18px monospace '\n",
    "            '!important;\"></div>'.format(self.uuid),\n",
    "            raw=True,\n",
    "        )\n",
    "        display_javascript(\n",
    "            \"\"\"\n",
    "        require([\"https://rawgit.com/caldwell/renderjson/master/renderjson.js\"],\n",
    "            function() {\n",
    "                renderjson.set_show_to_level(2);\n",
    "                document.getElementById('%s').appendChild(renderjson(%s))\n",
    "        });\n",
    "      \"\"\"\n",
    "            % (self.uuid, self.json_str),\n",
    "            raw=True,\n",
    "        )\n",
    "\n",
    "\n",
    "RenderJSON([i.to_dict() for i in items])"
   ]
  }
 ],
 "metadata": {
  "interpreter": {
   "hash": "6b6313dbab648ff537330b996f33bf845c0da10ea77ae70864d6ca8e2699c7ea"
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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": 5
}


================================================
FILE: docs/tutorials/stac-metadata-viz.ipynb
================================================
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ab31574b",
   "metadata": {},
   "source": [
    "# STAC Metadata Visualizations\n",
    "\n",
    "This notebook illustrates a simple way to display footprints of discovered Items after searching a STAC API, and making simple plots using Pandas and Holoviews. Only the metadata is visualized in these examples through maps and plots. The actual STAC data (i.e., Item Assets) are not accessed.\n",
    "\n",
    "The libraries GeoPandas and hvplot are used for visualizations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3188b5a0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# set pystac_client logger to DEBUG to see API calls\n",
    "import logging\n",
    "\n",
    "from pystac_client import Client\n",
    "\n",
    "logging.basicConfig()\n",
    "logger = logging.getLogger(\"pystac_client\")\n",
    "logger.setLevel(logging.DEBUG)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d404982",
   "metadata": {},
   "source": [
    "Define the STAC API to use, along with any custom headers (such as for authentication)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d40ed5c1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# STAC API root URL\n",
    "URL = \"https://planetarycomputer.microsoft.com/api/stac/v1\"\n",
    "\n",
    "# custom headers\n",
    "headers = []\n",
    "\n",
    "cat = Client.open(URL, headers=headers)\n",
    "cat"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e513e548",
   "metadata": {},
   "source": [
    "## Search\n",
    "\n",
    "Perform a spatio-temporal search of ASTER data for a small AOI in the northern part of The Netherlands between 2000 and 2010."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ae7a3eca",
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# AOI around Delfzijl, in northern Netherlands\n",
    "geom = {\n",
    "    \"type\": \"Polygon\",\n",
    "    \"coordinates\": [\n",
    "        [\n",
    "            [6.42425537109375, 53.174765470134616],\n",
    "            [7.344360351562499, 53.174765470134616],\n",
    "            [7.344360351562499, 53.67393435835391],\n",
    "            [6.42425537109375, 53.67393435835391],\n",
    "            [6.42425537109375, 53.174765470134616],\n",
    "        ]\n",
    "    ],\n",
    "}\n",
    "\n",
    "# limit sets the # of items per page so we can see multiple pages getting fetched\n",
    "search = cat.search(\n",
    "    max_items=50,\n",
    "    collections=\"aster-l1t\",\n",
    "    intersects=geom,\n",
    "    datetime=\"2000-01-01/2010-12-31\",\n",
    ")\n",
    "\n",
    "# retrieve the items as dictionaries, rather than Item objects\n",
    "items = list(search.items_as_dicts())\n",
    "len(items)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2a23660f",
   "metadata": {},
   "source": [
    "## GeoPandas\n",
    "\n",
    "A GeoDataFrame is constructed from the AOI geometry, in order to make visualizations easy.\n",
    "\n",
    "The STAC Items, which are a GeoJSON FeatureCollection can be converted to a GeoDataFrame."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ed0da77b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from copy import deepcopy\n",
    "\n",
    "import geopandas as gpd\n",
    "import pandas as pd\n",
    "from shapely.geometry import shape\n",
    "\n",
    "\n",
    "# convert a list of STAC Items into a GeoDataFrame\n",
    "def items_to_geodataframe(items):\n",
    "    _items = []\n",
    "    for i in items:\n",
    "        _i = deepcopy(i)\n",
    "        _i[\"geometry\"] = shape(_i[\"geometry\"])\n",
    "        _items.append(_i)\n",
    "    gdf = gpd.GeoDataFrame(pd.json_normalize(_items))\n",
    "    for field in [\"properties.datetime\", \"properties.created\", \"properties.updated\"]:\n",
    "        if field in gdf:\n",
    "            gdf[field] = pd.to_datetime(gdf[field])\n",
    "    gdf.set_index(\"properties.datetime\", inplace=True)\n",
    "    return gdf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c4bed793",
   "metadata": {},
   "outputs": [],
   "source": [
    "# convert geometry to a GeoDataFrame\n",
    "aoi_gdf = gpd.GeoDataFrame([{\"geometry\": shape(geom)}])\n",
    "aoi_gdf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d1fc8732",
   "metadata": {},
   "outputs": [],
   "source": [
    "# convert found items to a GeoDataFrame\n",
    "items_gdf = items_to_geodataframe(items)\n",
    "items_gdf.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4d570626",
   "metadata": {},
   "source": [
    "## Plot Geometries on a Map\n",
    "\n",
    "Holoviews is used to display geometries on a map by using `hvplot`. The The `*` Holoviews operator to overlay two plots"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e8fbfad6",
   "metadata": {},
   "outputs": [],
   "source": [
    "import hvplot.pandas  # noqa: F401\n",
    "\n",
    "\n",
    "# plot polygons on a map with background tiles.\n",
    "def plot_polygons(data, *args, **kwargs):\n",
    "    return data.hvplot.polygons(\n",
    "        *args,\n",
    "        geo=True,\n",
    "        projection=\"GOOGLE_MERCATOR\",\n",
    "        xaxis=None,\n",
    "        yaxis=None,\n",
    "        frame_width=600,\n",
    "        frame_height=600,\n",
    "        fill_alpha=0,\n",
    "        line_width=4,\n",
    "        **kwargs,\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "72094092",
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_polygons(aoi_gdf, tiles=\"OSM\", line_color=\"red\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d7d6cfc6",
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_polygons(items_gdf, tiles=\"OSM\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a07e87b",
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_polygons(items_gdf, tiles=\"OSM\") * plot_polygons(aoi_gdf, line_color=\"red\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b0604c84",
   "metadata": {},
   "source": [
    "## Line Plots\n",
    "\n",
    "Numeric STAC metadata can also be plotted, most often this will be plotted vs the Item `datetime`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c198e261",
   "metadata": {},
   "outputs": [],
   "source": [
    "items_df = pd.DataFrame(items_gdf)\n",
    "\n",
    "plot_fields = [\n",
    "    \"properties.eo:cloud_cover\",\n",
    "    \"properties.view:sun_azimuth\",\n",
    "    \"properties.view:sun_elevation\",\n",
    "]\n",
    "\n",
    "items_df[plot_fields].hvplot(height=500, width=800).opts(legend_position=\"top_right\")"
   ]
  }
 ],
 "metadata": {
  "interpreter": {
   "hash": "6b6313dbab648ff537330b996f33bf845c0da10ea77ae70864d6ca8e2699c7ea"
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.9.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}


================================================
FILE: docs/tutorials.rst
================================================
.. _tutorials:

Tutorials
#########

PySTAC-Client Introduction
--------------------------

- :tutorial:`GitHub version <pystac-client-introduction.ipynb>`
- :ref:`Docs version </tutorials/pystac-client-introduction.ipynb>`

This tutorial gives an introduction to using pystac-client with a STAC API

STAC Metadata Visualization
---------------------------

- :tutorial:`GitHub version <stac-metadata-viz.ipynb>`
- :ref:`Docs version </tutorials/stac-metadata-viz.ipynb>`

This tutorial gives an introduction to using hvplot to visualize
STAC metadata and Item geometries on a map.

CQL2 Filtering
---------------------------

- :tutorial:`GitHub version <cql2-filter.ipynb>`
- :ref:`Docs version </tutorials/cql2-filter.ipynb>`

This tutorial gives an introduction to using CQL2-JSON filtering in searches to
search by arbitrary STAC Item properties.

Item Search with Intersects
-----------------------------------------------------

- :tutorial:`GitHub version <item-search-intersects.ipynb>`
- :ref:`Docs version </tutorials/item-search-intersects.ipynb>`

This tutorial demonstrates the use of different Python library
object types that can be used for the `intersects`
search parameter.

Calculating Coverage Percentage of the AOI by an Item
-----------------------------------------------------

- :tutorial:`GitHub version <aoi-coverage.ipynb>`
- :ref:`Docs version </tutorials/aoi-coverage.ipynb>`

This tutorial demonstrates the use of pystac-client to calculate the
percentage an Item's geometry that intesects with the area of interest
(AOI) specified in the search by the `intersects` parameter.

Authentication
--------------

- :tutorial:`GitHub version <authentication.md>`
- :ref:`Docs version </tutorials/authentication.md>`

This tutorial demontrates different ways the pystac-client can be
used to access a private stac api, when protected with various
authentication mechanisms.


================================================
FILE: docs/usage.rst
================================================
Usage
#####

PySTAC-Client (pystac-client) builds upon
`PySTAC <https://github.com/stac-utils/pystac>`_ library to add support
for STAC APIs in addition to static STAC catalogs. PySTAC-Client can be used with static
or dynamic (i.e., API) catalogs. Currently, pystac-client does not offer much in the way
of additional functionality if using with static catalogs, as the additional features
are for support STAC API endpoints such as `search`. However, in the future it is
expected that pystac-client will offer additional convenience functions that may be
useful for static and dynamic catalogs alike.

The most basic implementation of a STAC API is an endpoint that returns a valid STAC
Catalog, but also contains a ``"conformsTo"`` attribute that is a list of conformance
URIs for the standards that the API supports.

This section is organized by the classes that are used, which mirror parent classes
from PySTAC:

+------------------+------------+
| pystac-client    | pystac     |
+==================+============+
| Client           | Catalog    |
+------------------+------------+
| CollectionClient | Collection |
+------------------+------------+

The classes offer all of the same functions for accessing and traversing Catalogs as
in PySTAC. The documentation for pystac-client only includes new functions, it does
not duplicate documentation for inherited functions.

Client
++++++

The :class:`pystac_client.Client` class is the main interface for working with services
that conform to the STAC API spec. This class inherits from the :class:`pystac.Catalog`
class and in addition to the methods and attributes implemented by a Catalog, it also
includes convenience methods and attributes for:

* Checking conformance to various specs
* Querying a search endpoint (if the API conforms to the STAC API - Item Search spec)
* Getting jsonschema of queryables from `/queryables` endpoint (if the API conforms
  to the STAC API - Filter spec)

The preferred way to interact with any STAC Catalog or API is to create an
:class:`pystac_client.Client` instance with the ``pystac_client.Client.open`` method
on a root Catalog. This calls the :meth:`pystac.STACObject.from_file` except it
properly configures conformance and IO for reading from remote servers.

The following code creates an instance by making a call to the Microsoft Planetary
Computer root catalog.

.. code-block:: python

    >>> from pystac_client import Client
    >>> catalog = Client.open('https://planetarycomputer.microsoft.com/api/stac/v1')
    >>> catalog.title
    'Microsoft Planetary Computer STAC API'

Some functions, such as ``Client.search`` will throw an error if the provided
Catalog/API does not support the required Conformance Class. In other cases,
such as ``Client.get_collections``, API endpoints will be used if the API
conforms, otherwise it will fall back to default behavior provided by
:class:`pystac.Catalog`.

When a ``Client`` does not conform to a particular Conformance Class, an informative
warning is raised. Similarly when falling back to the :class:`pystac.Catalog`
implementation a warning is raised. You can control the behavior of these warnings
using the standard :py:mod:`warnings` or special context managers :func:`pystac_client.warnings.strict` and
from :func:`pystac_client.warnings.ignore`.

API Conformance
---------------

This library is intended to work with any STAC static catalog or STAC API. A static
catalog will be usable more or less the same as with PySTAC, except that pystac-client
supports providing custom headers to API endpoints. (e.g., authenticating
to an API with a token).

A STAC API is a STAC Catalog that is required to advertise its capabilities in a
`conformsTo` field and implements the `STAC API - Core` spec along with other
optional specifications:

* `CORE <https://github.com/radiantearth/stac-api-spec/tree/master/core>`__
* `ITEM_SEARCH <https://github.com/radiantearth/stac-api-spec/tree/master/item-search>`__
   * `FIELDS <https://github.com/radiantearth/stac-api-spec/tree/master/fragments/fields>`__
   * `QUERY <https://github.com/radiantearth/stac-api-spec/tree/master/fragments/query>`__
   * `SORT <https://github.com/radiantearth/stac-api-spec/tree/master/fragments/sort>`__
   * `CONTEXT <https://github.com/radiantearth/stac-api-spec/tree/master/fragments/context>`__
   * `FILTER <https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter>`__
* `COLLECTIONS <https://github.com/radiantearth/stac-api-spec/tree/master/collections>`__ (based on
  the `Features Collection section of OGC APO -  Features <http://docs.opengeospatial.org/is/17-069r3/17-069r3.html#_collections_>__`)
* `FEATURES <https://github.com/radiantearth/stac-api-spec/tree/master/ogcapi-features>`__ (based on
  `OGC API - Features <https://www.ogc.org/standards/ogcapi-features>`__)

The :meth:`pystac_client.Client.conforms_to` method is used to check conformance
against conformance classes (specs). To check an API for support for a given spec,
pass the `conforms_to` function the name of a :class:`ConformanceClasses`.

.. code-block:: python

    >>> catalog.conforms_to("ITEM_SEARCH")
    True

If the API does not advertise conformance with a particular spec, but it does support
it you can update `conforms_to` on the client object. For instance in `v0` of earth-search
there are no ``"conformsTo"`` uris set at all. But they can be explicitly set:

.. code-block:: python

    >>> catalog = Client.open("https://earth-search.aws.element84.com/v0")
    <stdin>:1: NoConformsTo: Server does not advertise any conformance classes.
    >>> catalog.conforms_to("ITEM_SEARCH")
    False
    >>> catalog.add_conforms_to("ITEM_SEARCH")

Note, updating ``"conformsTo"`` does not change what the server supports, it just
changes PySTAC client's understanding of what the server supports.

Configuring retry behavior
--------------------------

By default, **pystac-client** will retry requests that fail DNS lookup or have timeouts.
If you'd like to configure this behavior, e.g. to retry on some ``50x`` responses, you can configure the StacApiIO's session:

.. code-block:: python

    from requests.adapters import HTTPAdapter
    from urllib3 import Retry

    from pystac_client import Client
    from pystac_client.stac_api_io import StacApiIO

    retry = Retry(
        total=5, backoff_factor=1, status_forcelist=[502, 503, 504], allowed_methods=None
    )
    stac_api_io = StacApiIO(max_retries=retry)
    client = Client.open(
        "https://planetarycomputer.microsoft.com/api/stac/v1", stac_io=stac_api_io
    )

Automatically modifying results
-------------------------------

Some systems, like the `Microsoft Planetary Computer <http://planetarycomputer.microsoft.com/>`__,
have public STAC metadata but require some `authentication <https://planetarycomputer.microsoft.com/docs/concepts/sas/>`__
to access the actual assets.

``pystac-client`` provides a ``modifier`` keyword that can automatically
modify the STAC objects returned by the STAC API.

.. code-block:: python

   >>> from pystac_client import Client
   >>> import planetary_computer, requests
   >>> catalog = Client.open(
   ...    'https://planetarycomputer.microsoft.com/api/stac/v1',
   ...    modifier=planetary_computer.sign_inplace,
   ... )
   >>> item = next(catalog.get_collection("sentinel-2-l2a").get_all_items())
   >>> requests.head(item.assets["B02"].href).status_code
   200

Without the modifier, we would have received a 404 error because the asset
is in a private storage container.

``pystac-client`` expects that the ``modifier`` callable modifies the result
object in-place and returns no result. A warning is emitted if your
``modifier`` returns a non-None result that is not the same object as the
input.

Here's an example of creating your own modifier.
Because :py:class:`~pystac_client.Modifiable` is a union, the modifier function must handle a few different types of input objects, and care must be taken to ensure that you are modifying the input object (rather than a copy).
Simplifying this interface is a space for future improvement.

.. code-block:: python

    import urllib.parse

    import pystac

    from pystac_client import Client, Modifiable


    def modifier(modifiable: Modifiable) -> None:
        if isinstance(modifiable, dict):
            if modifiable["type"] == "FeatureCollection":
                new_features = list()
                for item_dict in modifiable["features"]:
                    modifier(item_dict)
                    new_features.append(item_dict)
                modifiable["features"] = new_features
            else:
                stac_object = pystac.read_dict(modifiable)
                modifier(stac_object)
                modifiable.update(stac_object.to_dict())
        else:
            for key, asset in modifiable.assets.items():
                url = urllib.parse.urlparse(asset.href)
                if not url.query:
                    asset.href = urllib.parse.urlunparse(url._replace(query="foo=bar"))
                    modifiable.assets[key] = asset


    client = Client.open(
        "https://planetarycomputer.microsoft.com/api/stac/v1", modifier=modifier
    )
    item_search = client.search(collections=["landsat-c2-l2"], max_items=1)
    item = next(item_search.items())
    asset = item.assets["red"]
    assert urllib.parse.urlparse(asset.href).query == "foo=bar"


Using custom certificates
-------------------------

If you need to use custom certificates in your ``pystac-client`` requests, you can
customize the :class:`StacApiIO<pystac_client.stac_api_io.StacApiIO>` instance before
creating your :class:`Client<pystac_client.Client>`.

.. code-block:: python

    >>> from pystac_client.stac_api_io import StacApiIO
    >>> from pystac_client.client import Client
    >>> stac_api_io = StacApiIO()
    >>> stac_api_io.session.verify = "/path/to/certfile"
    >>> client = Client.open("https://planetarycomputer.microsoft.com/api/stac/v1", stac_io=stac_api_io)

CollectionClient
++++++++++++++++

STAC APIs may optionally implement a ``/collections`` endpoint as described in the
`STAC API - Collections spec
<https://github.com/radiantearth/stac-api-spec/tree/release/v1.0.0/ogcapi-features#stac-api---collections>`__. 
This endpoint allows clients to search or inspect items within a particular collection.

.. code-block:: python

    >>> catalog = Client.open('https://planetarycomputer.microsoft.com/api/stac/v1')
    >>> collection = catalog.get_collection("sentinel-2-l2a")
    >>> collection.title
    'Sentinel-2 Level-2A'

:class:`pystac_client.CollectionClient` overrides :meth:`pystac.Collection.get_items`.
PySTAC will get items by iterating through all children until it gets to an ``item`` link.
PySTAC client will use the API endpoint instead: `/collections/<collection_id>/items`
(as long as `STAC API - Item Search spec
<https://github.com/radiantearth/stac-api-spec/tree/release/v1.0.0/item-search>`__ is supported).

.. code-block:: python

    >>> item = next(collection.get_items(), None)

Note that calling list on this iterator will take a really long time since it will be retrieving
every itme for the whole ``"sentinel-2-l2a"`` collection.

CollectionSearch
++++++++++++++++

STAC API services may optionally implement a ``/collections`` endpoint as described in the
`STAC API - Collections spec
<https://github.com/radiantearth/stac-api-spec/tree/release/v1.0.0/ogcapi-features#stac-api---collections>`__.
The ``/collections`` endpoint can be extended with the 
`STAC API - Collection Search Extension <https://github.com/stac-api-extensions/collection-search>`__ 
which adds the capability to apply filter parameters to the collection-level metadata. 
See the `Query Parameters and Fields
<https://github.com/stac-api-extensions/collection-search?tab=readme-ov-file#query-parameters-and-fields>`__
from that spec for details on the meaning of each parameter.

The :meth:`pystac_client.Client.collection_search` method provides an interface for making
requests to a service's "collections" endpoint. This method returns a
:class:`pystac_client.CollectionSearch` instance.

.. code-block:: python

    >>> from pystac_client import Client
    >>> catalog = Client.open('https://planetarycomputer.microsoft.com/api/stac/v1')
    >>> results = catalog.collection_search(
    ...     q="biomass",
    ...     datetime="2022/.."
    ... )

Instances of :class:`~pystac_client.CollectionSearch` have a handful of methods for
getting matching collections as Python objects. The right method to use depends on
how many of the matches you want to consume (a single collection at a time, a
page at a time, or everything) and whether you want plain Python dictionaries
representing the collections, or :class:`pystac.Collection` objects.

The following table shows the :class:`~pystac_client.CollectionSearch` methods for fetching
matches, according to which set of matches to return and whether to return them as
``pystac`` objects or plain dictionaries.

====================== ======================================================= ===============================================================
Matches to return      PySTAC objects                                          Plain dictionaries
====================== ======================================================= ===============================================================
**Single collections** :meth:`~pystac_client.CollectionSearch.collections`     :meth:`~pystac_client.CollectionSearch.collections_as_dicts`
**Pages**              :meth:`~pystac_client.CollectionSearch.pages`           :meth:`~pystac_client.CollectionSearch.pages_as_dicts`
**Everything**         :meth:`~pystac_client.CollectionSearch.collection_list` :meth:`~pystac_client.CollectionSearch.collection_list_as_dict`
====================== ======================================================= ===============================================================

Additionally, the ``matched`` method can be used to access result metadata about
how many total items matched the query:

* :meth:`CollectionSearch.matched <pystac_client.CollectionSearch.matched>`: returns the number
  of hits (collections) for this search. If the API supports the STAC API Context Extension this
  value will be returned directly from a search result with ``limit=1``. Otherwise ``pystac-client``
  will count the results and return a value with an associated warning.

.. code-block:: python

    >>> for collection in results.collections():
    ...     print(item.id)
    fia
    modis-13Q1-061
    modis-13A1-061
    sentinel-3-olci-lfr-l2-netcdf

The :meth:`~pystac_client.CollectionSearch.collections` and related methods handle retrieval of
successive pages of results
by finding links with a ``"rel"`` type of ``"next"`` and parsing them to construct the
next request. The default
implementation of this ``"next"`` link parsing assumes that the link follows the spec for
an extended STAC link as
described in the
`STAC API - Collections: Collection Paging <https://github.com/radiantearth/stac-api-spec/blob/main/ogcapi-features/README.md#collection-pagination>`__
section.

Alternatively, the Collections can be returned as a list, where each
list is one page of results retrieved from search:

.. code-block:: python

    >>> for page in results.pages():
    ...     for collection in page.collections():
    ...         print(collection.id)
    fia
    modis-13Q1-061
    modis-13A1-061
    sentinel-3-olci-lfr-l2-netcdf

If you do not need the :class:`pystac.Collection` instances, you can instead use
:meth:`CollectionSearch.collections_as_dicts <pystac_client.CollectionSearch.collections_as_dicts>`
to retrieve dictionary representation of the collections, without incurring the cost of
creating the Collection objects.

.. code-block:: python

    >>> for collection_dict in results.collections_as_dicts():
    ...     print(collection_dict["id"])
    fia
    modis-13Q1-061
    modis-13A1-061
    sentinel-3-olci-lfr-l2-netcdf

ItemSearch
++++++++++

STAC API services may optionally implement a ``/search`` endpoint as describe in the
`STAC API - Item Search spec
<https://github.com/radiantearth/stac-api-spec/tree/main/item-search>`__. This
endpoint allows clients to query STAC Items across the entire service using a variety
of filter parameters. See the `Query Parameter Table
<https://github.com/radiantearth/stac-api-spec/tree/main/item-search#query-parameter-table>`__
from that spec for details on the meaning of each parameter.

The :meth:`pystac_client.Client.search` method provides an interface for making
requests to a service's "search" endpoint. This method returns a
:class:`pystac_client.ItemSearch` instance.

.. code-block:: python

    >>> from pystac_client import Client
    >>> catalog = Client.open('https://planetarycomputer.microsoft.com/api/stac/v1')
    >>> results = catalog.search(
    ...     max_items=5,
    ...     bbox=[-73.21, 43.99, -73.12, 44.05],
    ...     datetime=['2019-01-01T00:00:00Z', '2019-01-02T00:00:00Z'],
    ... )

Instances of :class:`~pystac_client.ItemSearch` have a handful of methods for
getting matching items as Python objects. The right method to use depends on
how many of the matches you want to consume (a single item at a time, a
page at a time, or everything) and whether you want plain Python dictionaries
representing the items, or :class:`pystac.Item` objects.

The following table shows the :class:`~pystac_client.ItemSearch` methods for fetching
matches, according to which set of matches to return and whether to return them as
``pystac`` objects or plain dictionaries.

================= ================================================= =========================================================
Matches to return PySTAC objects                                    Plain dictionaries
================= ================================================= =========================================================
**Single items**  :meth:`~pystac_client.ItemSearch.items`           :meth:`~pystac_client.ItemSearch.items_as_dicts`
**Pages**         :meth:`~pystac_client.ItemSearch.pages`           :meth:`~pystac_client.ItemSearch.pages_as_dicts`
**Everything**    :meth:`~pystac_client.ItemSearch.item_collection` :meth:`~pystac_client.ItemSearch.item_collection_as_dict`
================= ================================================= =========================================================

Additionally, the ``matched`` method can be used to access result metadata about
how many total items matched the query:

* :meth:`ItemSearch.matched <pystac_client.ItemSearch.matched>`: returns the number
  of hits (items) for this search if the API supports the STAC API Context Extension.
  Not all APIs support returning a total count, in which case a warning will be issued.

.. code-block:: python

    >>> for item in results.items():
    ...     print(item.id)
    S2B_OPER_MSI_L2A_TL_SGS__20190101T200120_A009518_T18TXP_N02.11
    MCD43A4.A2019010.h12v04.006.2019022234410
    MCD43A4.A2019009.h12v04.006.2019022222645
    MYD11A1.A2019002.h12v04.006.2019003174703
    MYD11A1.A2019001.h12v04.006.2019002165238

The :meth:`~pystac_client.ItemSearch.items` and related methods handle retrieval of
successive pages of results
by finding links with a ``"rel"`` type of ``"next"`` and parsing them to construct the
next request. The default
implementation of this ``"next"`` link parsing assumes that the link follows the spec for
an extended STAC link as
described in the
`STAC API - Item Search: Paging <https://github.com/radiantearth/stac-api-spec/tree/master/item-search#paging>`__
section. See the :mod:`Paging <pystac_client.paging>` docs for details on how to
customize this behavior.

Alternatively, the Items can be returned within ItemCollections, where each
ItemCollection is one page of results retrieved from search:

.. code-block:: python

    >>> for ic in results.pages():
    ...     for item in ic.items:
    ...         print(item.id)
    S2B_OPER_MSI_L2A_TL_SGS__20190101T200120_A009518_T18TXP_N02.11
    MCD43A4.A2019010.h12v04.006.2019022234410
    MCD43A4.A2019009.h12v04.006.2019022222645
    MYD11A1.A2019002.h12v04.006.2019003174703
    MYD11A1.A2019001.h12v04.006.2019002165238

If you do not need the :class:`pystac.Item` instances, you can instead use
:meth:`ItemSearch.items_as_dicts <pystac_client.ItemSearch.items_as_dicts>`
to retrieve dictionary representation of the items, without incurring the cost of
creating the Item objects.

.. code-block:: python

    >>> for item_dict in results.items_as_dicts():
    ...     print(item_dict["id"])
    S2B_OPER_MSI_L2A_TL_SGS__20190101T200120_A009518_T18TXP_N02.11
    MCD43A4.A2019010.h12v04.006.2019022234410
    MCD43A4.A2019009.h12v04.006.2019022222645
    MYD11A1.A2019002.h12v04.006.2019003174703
    MYD11A1.A2019001.h12v04.006.2019002165238

Query Extension
---------------

If the Catalog supports the `Query
extension <https://github.com/radiantearth/stac-api-spec/tree/master/fragments/query>`__,
any Item property can also be included in the search. Rather than
requiring the JSON syntax the Query extension uses, pystac-client can use a
simpler syntax that it will translate to the JSON equivalent. Note
however that when the simple syntax is used it sends all property values
to the server as strings, except for ``gsd`` which it casts to
``float``. This means that if there are extensions in use with numeric
properties these will be sent as strings. Some servers may automatically
cast this to the appropriate data type, others may not.

The query filter will also accept complete JSON as per the specification.

::

  <property><operator><value>

  where operator is one of `>=`, `<=`, `>`, `<`, `=`

  Examples:
  eo:cloud_cover<10
  view:off_nadir<50
  platform=landsat-8

Any number of properties can be included, and each can be included more
than once to use additional operators.

Sort Extension
---------------

If the Catalog supports the `Sort
extension <https://github.com/radiantearth/stac-api-spec/tree/master/fragments/sort>`__,
the search request can specify the order in which the results should be sorted with
the ``sortby`` parameter.  The ``sortby`` parameter can either be a string
(e.g., ``"-properties.datetime,+id,collection"``), a list of strings
(e.g., ``["-properties.datetime", "+id", "+collection"]``), or a dictionary representing
the POST JSON format of sortby. In the string and list formats, a ``-`` prefix means a
descending sort and a ``+`` prefix or no prefix means an ascending sort.

.. code-block:: python

    >>> from pystac_client import Client
    >>> results = Client.open('https://planetarycomputer.microsoft.com/api/stac/v1').search(
    ...     sortby="properties.datetime"
    ... )
    >>> results = Client.open('https://planetarycomputer.microsoft.com/api/stac/v1').search(
    ...     sortby="-properties.datetime,+id,+collection"
    ... )
    >>> results = Client.open('https://planetarycomputer.microsoft.com/api/stac/v1').search(
    ...     sortby=["-properties.datetime", "+id" , "+collection" ]
    ... )
    >>> results = Client.open('https://planetarycomputer.microsoft.com/api/stac/v1').search(
    ...     sortby=[
                {"direction": "desc", "field": "properties.datetime"},
                {"direction": "asc", "field": "id"},
                {"direction": "asc", "field": "collection"},
            ]
    ... )

Loading data
++++++++++++

Once you've fetched your STAC :class:`Items<pystac.Item>` with ``pystac-client``, you
now can work with the data referenced by your :class:`Assets<pystac.Asset>`.  This is
out of scope for ``pystac-client``, but there's a wide variety of tools and options
available, and the correct choices depend on your type of data, your environment, and
the type of analysis you're doing.

For simple workflows, it can be easiest to load data directly using `rasterio
<https://rasterio.readthedocs.io>`_, `fiona <https://fiona.readthedocs.io/>`_, and
similar tools. Here is a simple example using **rasterio** to display data from a raster
file.

.. code-block:: python

    >>> import rasterio.plot.show
    >>> with rasterio.open(item.assets["data"].href) as dataset:
    ...     rasterio.plot.show(dataset)

For larger sets of data and more complex workflows, a common tool for working with a
large number of raster files is `xarray <https://docs.xarray.dev>`_, which provides data
structures for labelled multi-dimensional arrays. `stackstac
<https://stackstac.readthedocs.io>`_ and `odc-stac <https://odc-stac.readthedocs.io>`_
are two similar tools that can load asset data from :class:`Items<pystac.Item>` or an
:class:`ItemCollection<pystac.ItemCollection>` into an **xarray**. Here's a simple
example from **odc-stac**'s documentation:

.. code-block:: python

    >>> catalog = pystac_client.Client.open(...)
    >>> query = catalog.search(...)
    >>> xx = odc.stac.load(
    ...     query.get_items(),
    ...     bands=["red", "green", "blue"],
    ...     resolution=100,
    ... )
    >>> xx.red.plot.imshow(col="time")


See each packages's respective documentation for more examples and tutorials.

.. _how_to:

How To
++++++++++++

Get the latest dataset across multiple collections
-----------------------------------------

When searching for multiple datasets across different collections, each dataset may be derived from a different time period. You may want to get the latest dataset for each collection instead of all available datasets.

.. code-block:: python

    from pystac_client import Client

    client = Client.open("https://planetarycomputer.microsoft.com/api/stac/v1")
    items = []
    for collection in ["nasadem", "esa-worldcover", "jrc-gsw"]:
        items.append(
            next(
                client.search(
                    collections=[collection], max_items=1, sortby="-properties.datetime"
                ).items()
            )
        )

The result will be a list containing the most recent item from each collection:

    >>> items
    [<Item id=NASADEM_HGT_s56w072>, <Item id=ESA_WorldCover_10m_2021_v200_S60W030>, <Item id=90W_80Nv1_3_2020>]

================================================
FILE: pyproject.toml
================================================
[project]
name = "pystac-client"
description = "Python library for searching SpatioTemporal Asset Catalog (STAC) APIs."
readme = "README.md"
authors = [
    { name = "Jon Duckworth", email = "duckontheweb@gmail.com" },
    { name = "Matthew Hanson", email = "matt.a.hanson@gmail.com" },
]
maintainers = [{ name = "Pete Gadomski", email = "pete.gadomski@gmail.com" }]
keywords = ["pystac", "imagery", "raster", "catalog", "STAC"]
license = { text = "Apache-2.0" }
classifiers = [
    "Programming Language :: Python",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Operating System :: OS Independent",
    "Natural Language :: English",
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: Apache Software License",
    "Topic :: Scientific/Engineering :: GIS",
    "Topic :: Software Development :: Libraries",
    "Topic :: Software Development :: Libraries :: Python Modules",
]
requires-python = ">=3.10"
dependencies = [
    "requests>=2.28.2",
    "pystac[validation]>=1.10.0",
    "python-dateutil>=2.8.2",
]
dynamic = ["version"]

[project.scripts]
stac-client = "pystac_client.cli:cli"

[dependency-groups]
dev = [
    "codespell~=2.4.0",
    "coverage~=7.2",
    "cql2>=0.3.7",
    "doc8~=2.0.0",
    "importlib-metadata~=9.0",
    "mypy~=1.2",
    "orjson~=3.8",
    "pre-commit~=4.0",
    "pytest-benchmark~=5.2.0",
    "pytest-console-scripts~=1.4.0",
    "pytest-cov~=7.0",
    "pytest-recording~=0.13",
    "pytest~=9.0",
    "recommonmark~=0.7.1",
    "requests-mock~=1.12",
    "ruff==0.15.12",
    "tomli~=2.0; python_version<'3.11'",
    "types-python-dateutil>=2.8.19,<2.10.0",
    "types-requests~=2.33.0",
    "urllib3>=2.4",
]
docs = [
    "Sphinx~=8.0",
    "boto3~=1.26",
    "cartopy~=0.21",
    "geojson~=3.2.0",
    "geopandas~=1.1.1",
    "geoviews~=1.9",
    "hvplot~=0.12.0",
    "ipykernel~=7.0",
    "ipython~=8.12",
    "jinja2>=3.0,<4.0",
    "matplotlib~=3.8",
    "myst-parser~=4.0",
    "nbsphinx~=0.9",
    "pydata-sphinx-theme~=0.13",
    "pygeoif~=1.0",
    "scipy~=1.10",
    "sphinxcontrib-fulltoc~=1.2",
]

[project.urls]
homepage = "https://github.com/stac-utils/pystac-client"
documentation = "https://pystac-client.readthedocs.io"
repository = "https://github.com/stac-utils/pystac-client.git"
changelog = "https://github.com/stac-utils/pystac-client/blob/main/CHANGELOG.md"
discussions = "https://github.com/radiantearth/stac-spec/discussions/categories/stac-software"

[tool.setuptools.packages.find]
include = ["pystac_client*"]
exclude = ["tests*"]

[tool.setuptools.dynamic]
version = { attr = "pystac_client.version.__version__" }

[tool.doc8]
ignore-path = "docs/_build,docs/tutorials"
max-line-length = 130

[tool.ruff]
line-length = 88

[tool.ruff.lint]
ignore = ["E722", "E731"]
select = ["E", "F", "W", "I"]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"test_item_search.py" = ["E501"]

[tool.pytest.ini_options]
markers = "vcr: records network activity"
addopts = "--benchmark-skip --block-network -Werror"
filterwarnings = [
    "ignore::ResourceWarning",
    "ignore::pytest.PytestUnraisableExceptionWarning",
]

[tool.mypy]
show_error_codes = true
strict = true

[[tool.mypy.overrides]]
module = ["jinja2"]
ignore_missing_imports = true

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"


================================================
FILE: pystac_client/__init__.py
================================================
__all__ = [
    "Client",
    "CollectionClient",
    "CollectionSearch",
    "ConformanceClasses",
    "ItemSearch",
    "Modifiable",
    "__version__",
]

from pystac_client._utils import Modifiable
from pystac_client.client import Client
from pystac_client.collection_client import CollectionClient
from pystac_client.collection_search import CollectionSearch
from pystac_client.conformance import ConformanceClasses
from pystac_client.item_search import ItemSearch
from pystac_client.version import __version__


================================================
FILE: pystac_client/_utils.py
================================================
import urllib
import warnings
from collections.abc import Callable
from typing import Any, Union

import pystac

from pystac_client.errors import IgnoredResultWarning

Modifiable = Union[
    pystac.Collection, pystac.Item, pystac.ItemCollection, dict[Any, Any]
]


def call_modifier(
    modifier: Callable[[Modifiable], None] | None, obj: Modifiable
) -> None:
    """Calls the user's modifier and validates that the result is None."""
    if modifier is None:
        return None

    result = modifier(obj)
    if result is not None and result is not obj:
        warnings.warn(
            f"modifier '{modifier}' returned a result that's being ignored. "
            "You should ensure that 'modifier' is operating in-place and use "
            "a function that returns 'None' or silence this warning.",
            IgnoredResultWarning,
        )


def urljoin(href: str, name: str) -> str:
    """Joins a path onto an existing href, respecting query strings, etc."""
    url = urllib.parse.urlparse(href)
    path = url.path
    if not path.endswith("/"):
        path += "/"
    return urllib.parse.urlunparse(url._replace(path=path + name))


================================================
FILE: pystac_client/cli.py
================================================
import argparse
import json
import logging
import os
import re
import sys
import warnings
from typing import Any

from pystac import STACError, STACTypeError

from .client import Client
from .conformance import ConformanceClasses
from .item_search import (
    OPS,
    BBoxLike,
    CollectionsLike,
    DatetimeLike,
    FieldsLike,
    FilterLangLike,
    FilterLike,
    IDsLike,
    IntersectsLike,
    QueryLike,
    SortbyLike,
)
from .version import __version__
from .warnings import (
    DoesNotConformTo,
    FallbackToPystac,
    MissingLink,
    NoConformsTo,
    PystacClientWarning,
)

logger = logging.getLogger(__name__)


def search(
    client: Client,
    matched: bool = False,
    save: str | None = None,
    *,
    method: str = "GET",
    max_items: int | None = None,
    limit: int | None = None,
    ids: IDsLike | None = None,
    collections: CollectionsLike | None = None,
    bbox: BBoxLike | None = None,
    intersects: IntersectsLike | None = None,
    datetime: DatetimeLike | None = None,
    query: QueryLike | None = None,
    filter: FilterLike | None = None,
    filter_lang: FilterLangLike | None = None,
    sortby: SortbyLike | None = None,
    fields: FieldsLike | None = None,
) -> int:
    """Main function for performing a search"""

    result = client.search(
        method=method,
        max_items=max_items,
        limit=limit,
        ids=ids,
        collections=collections,
        bbox=bbox,
        intersects=intersects,
        datetime=datetime,
        query=query,
        filter=filter,
        filter_lang=filter_lang,
        sortby=sortby,
        fields=fields,
    )

    if matched:
        if (nmatched := result.matched()) is not None:
            print(f"{nmatched} items matched")
        else:
            raise KeyError("'matched' is not supported for this catalog")
    else:
        feature_collection = result.item_collection_as_dict()
        if save:
            with open(save, "w") as f:
                f.write(json.dumps(feature_collection))
        else:
            print(json.dumps(feature_collection))
    return 0


def collections(
    client: Client,
    save: str | None = None,
    matched: bool = False,
    *,
    max_collections: int | None = None,
    limit: int | None = None,
    bbox: BBoxLike | None = None,
    datetime: DatetimeLike | None = None,
    q: str | None = None,
    query: QueryLike | None = None,
    filter: FilterLike | None = None,
    filter_lang: FilterLangLike | None = None,
    sortby: SortbyLike | None = None,
    fields: FieldsLike | None = None,
) -> int:
    """Fetch collections from collections endpoint"""
    try:
        result = client.collection_search(
            max_collections=max_collections,
            limit=limit,
            bbox=bbox,
            datetime=datetime,
            q=q,
            query=query,
            filter=filter,
            filter_lang=filter_lang,
            sortby=sortby,
            fields=fields,
        )
        if matched:
            if (nmatched := result.matched()) is not None:
                print(f"{nmatched} collections matched")
            else:
                raise KeyError("'matched' is not supported for this catalog")
        else:
            collections_dicts = list(result.collections_as_dicts())
            if save:
                with open(save, "w") as f:
                    f.write(json.dumps(collections_dicts))
            else:
                print(json.dumps(collections_dicts))
    except STACTypeError as e:
        raise STACError(
            f"The client at {client.self_href} is OK, but one or more of the "
            "collections is invalid."
        ) from e
    return 0


def add_warnings_behavior(parser: argparse.ArgumentParser) -> None:
    warnings_group = parser.add_argument_group("warnings behavior")
    warnings_group.add_argument(
        "--error",
        nargs="*",
        choices=[
            "no-conforms-to",
            "does-not-conform-to",
            "missing-link",
            "fallback-to-pystac",
        ],
        help="Error on all stac-client warnings or specific warnings.",
    )
    warnings_group.add_argument(
        "--ignore",
        nargs="*",
        choices=[
            "no-conforms-to",
            "does-not-conform-to",
            "missing-link",
            "fallback-to-pystac",
        ],
        help="Ignore all stac-client warnings or specific warnings.",
    )


def set_warnings(error: list[str] | None, ignore: list[str] | None) -> None:
    # First set filters on the base class
    if ignore is not None and len(ignore) == 0:
        warnings.filterwarnings("ignore", category=PystacClientWarning)
    if error is not None and len(error) == 0:
        warnings.filterwarnings("error", category=PystacClientWarning)

    # Then set filters on any specific classes
    category_options = {
        "no-conforms-to": NoConformsTo,
        "does-not-conform-to": DoesNotConformTo,
        "missing-link": MissingLink,
        "fallback-to-pystac": FallbackToPystac,
    }
    if ignore is not None:
        for w in ignore:
            warnings.filterwarnings("ignore", category=category_options[w])
    if error is not None:
        for w in error:
            warnings.filterwarnings("error", category=category_options[w])


def set_conforms_to(
    client: Client, clear: bool, remove: list[str] | None, add: list[str] | None
) -> None:
    """Alters conforms_to settings on client in-place"""
    if clear:
        client.clear_conforms_to()
    if remove is not None:
        for conformance_class in remove:
            client.remove_conforms_to(conformance_class)
    if add is not None:
        for conformance_class in add:
            client.add_conforms_to(conformance_class)


def parse_args(args: list[str]) -> dict[str, Any]:
    desc = "STAC Client"
    dhf = argparse.ArgumentDefaultsHelpFormatter
    parser0 = argparse.ArgumentParser(description=desc)
    parser0.add_argument(
        "--version",
        help="Print version and exit",
        action="version",
        version=__version__,
    )

    parent = argparse.ArgumentParser(add_help=False)
    parent.add_argument("url", help="Root Catalog URL")
    parent.add_argument(
        "--logging", default="INFO", help="DEBUG, INFO, WARN, ERROR, CRITICAL"
    )
    parent.add_argument(
        "--headers",
        nargs="*",
        help="Additional request headers (KEY=VALUE pairs)",
        default=None,
    )
    parent.add_argument(
        "--add-conforms-to",
        choices=[c.name for c in ConformanceClasses],
        nargs="*",
        help="Specify conformance classes to add to client",
    )
    parent.add_argument(
        "--remove-conforms-to",
        choices=[c.name for c in ConformanceClasses],
        nargs="*",
        help="Specify conformance classes to remove from client",
    )
    parent.add_argument(
        "--clear-conforms-to",
        default=False,
        action="store_true",
    )

    subparsers = parser0.add_subparsers(dest="command")

    # collections command
    parser = subparsers.add_parser(
        "collections",
        help="Get all collections in this Catalog",
        parents=[parent],
        formatter_class=dhf,
    )
    add_warnings_behavior(parser)

    collections_group = parser.add_argument_group("collection search options")
    collections_group.add_argument(
        "--bbox", help="Bounding box (min lon, min lat, max lon, max lat)", nargs=4
    )
    collections_group.add_argument(
        "--datetime",
        help="Single datetime or begin and end datetime (e.g., 2017-01-01/2017-02-15)",
    )
    collections_group.add_argument("--q", help="Free-text search query")
    collections_group.add_argument(
        "--query",
        nargs="*",
        help=f"Query properties of form KEY=VALUE ({','.join(OPS)} supported)",
    )
    collections_group.add_argument(
        "--filter",
        help="Filter on queryables using language specified in filter-lang parameter",
    )
    collections_group.add_argument(
        "--filter-lang",
        help="Filter language used within the filter parameter",
        default="cql2-json",
    )
    collections_group.add_argument("--sortby", help="Sort by fields", nargs="*")
    collections_group.add_argument(
        "--fields", help="Control what fields get returned", nargs="*"
    )
    collections_group.add_argument("--limit", help="Page size limit", type=int)
    collections_group.add_argument(
        "--max-collections",
        dest="max_collections",
        help="Max collections to retrieve from search",
        type=int,
    )

    output_group = parser.add_argument_group("output options")
    output_group.add_argument(
        "--save", help="Filename to save collections to", default=None
    )
    output_group.add_argument(
        "--matched", help="Print number matched", default=False, action="store_true"
    )

    # search command
    parser = subparsers.add_parser(
        "search",
        help="Perform new search of items",
        parents=[parent],
        formatter_class=dhf,
    )

    search_group = parser.add_argument_group("search options")
    search_group.add_argument(
        "-c", "--collections", help="One or more collection IDs", nargs="*"
    )
    search_group.add_argument(
        "--ids", help="One or more Item IDs (ignores other parameters)", nargs="*"
    )
    search_group.add_argument(
        "--bbox", help="Bounding box (min lon, min lat, max lon, max lat)", nargs=4
    )
    search_group.add_argument(
        "--intersects", help="GeoJSON Feature or geometry (file or string)"
    )
    search_group.add_argument(
        "--datetime",
        help="Single datetime or begin and end datetime (e.g., 2017-01-01/2017-02-15)",
    )
    search_group.add_argument(
        "--query",
        nargs="*",
        help=f"Query properties of form KEY=VALUE ({','.join(OPS)} supported)",
    )
    search_group.add_argument(
        "--filter",
        help="Filter on queryables using language specified in filter-lang parameter",
    )
    search_group.add_argument(
        "--filter-lang",
        help="Filter language used within the filter parameter",
        default="cql2-json",
    )
    search_group.add_argument("--sortby", help="Sort by fields", nargs="*")
    search_group.add_argument(
        "--fields", help="Control what fields get returned", nargs="*"
    )
    search_group.add_argument("--limit", help="Page size limit", type=int)
    search_group.add_argument(
        "--max-items",
        dest="max_items",
        help="Max items to retrieve from search",
        type=int,
    )
    search_group.add_argument("--method", help="GET or POST", default="POST")
    add_warnings_behavior(parser)

    output_group = parser.add_argument_group("output options")
    output_group.add_argument(
        "--matched", help="Print number matched", default=False, action="store_true"
    )
    output_group.add_argument(
        "--save", help="Filename to save Item collection to", default=None
    )

    parsed_args = {
        k: v for k, v in vars(parser0.parse_args(args)).items() if v is not None
    }
    if "command" not in parsed_args:
        parser0.print_usage()
        return {}

    # if intersects is JSON file, read it in
    if "intersects" in parsed_args:
        if os.path.exists(parsed_args["intersects"]):
            with open(parsed_args["intersects"]) as f:
                data = json.loads(f.read())
                if data["type"] == "Feature":
                    parsed_args["intersects"] = data["geometry"]
                elif data["type"] == "FeatureCollection":
                    logger.warning(
                        "When the input to intersects is a FeatureCollection, "
                        "only the first feature geometry is used."
                    )
                    parsed_args["intersects"] = data["features"][0]["geometry"]
                else:
                    parsed_args["intersects"] = data

    # if headers provided, parse it
    if "headers" in parsed_args:
        new_headers = {}
        splitter = re.compile("^([^=]+)=(.+)$")
        for head in parsed_args["headers"]:
            match = splitter.match(head)
            if match:
                new_headers[match.group(1)] = match.group(2)
            else:
                logger.warning(f"Unable to parse header {head}")
        parsed_args["headers"] = new_headers

    if "filter" in parsed_args:
        if "json" in parsed_args["filter_lang"]:
            parsed_args["filter"] = json.loads(parsed_args["filter"])

    return parsed_args


def cli() -> int:
    args = parse_args(sys.argv[1:])
    if not args:
        return 1

    loglevel = args.pop("logging")
    if args.get("save", False) or args.get("matched", False):
        logging.basicConfig(level=loglevel)
        # quiet loggers
        logging.getLogger("urllib3").propagate = False

    cmd = args.pop("command")

    try:
        url = args.pop("url")
        headers = args.pop("headers", {})

        set_warnings(args.pop("error", None), args.pop("ignore", None))

        client = Client.open(url, headers=headers)
        set_conforms_to(
            client,
            args.pop("clear_conforms_to"),
            args.pop("remove_conforms_to", None),
            args.pop("add_conforms_to", None),
        )

        if cmd == "search":
            return search(client, **args)
        elif cmd == "collections":
            return collections(client, **args)
        else:
            logger.error(
                f"Command '{cmd}' is not a valid command. "
                "must be 'search' or 'collections'",
            )
            return 1
    except Exception as e:
        logger.error(e, exc_info=True)
        return 1


if __name__ == "__main__":
    return_code = cli()
    if return_code and return_code != 0:
        sys.exit(return_code)


================================================
FILE: pystac_client/client.py
================================================
import re
import warnings
from collections.abc import Callable, Iterator
from functools import lru_cache
from typing import (
    TYPE_CHECKING,
    Any,
    cast,
)

import pystac
import pystac.utils
import pystac.validation
from pystac import CatalogType, Collection
from pystac.layout import APILayoutStrategy, HrefLayoutStrategy
from requests import Request

from pystac_client._utils import Modifiable, call_modifier, urljoin
from pystac_client.collection_client import CollectionClient
from pystac_client.collection_search import CollectionSearch
from pystac_client.conformance import ConformanceClasses
from pystac_client.errors import ClientTypeError
from pystac_client.exceptions import APIError
from pystac_client.item_search import (
    BBoxLike,
    CollectionsLike,
    DatetimeLike,
    FieldsLike,
    FilterLangLike,
    FilterLike,
    IDsLike,
    IntersectsLike,
    ItemSearch,
    QueryLike,
    SortbyLike,
)
from pystac_client.mixins import QUERYABLES_ENDPOINT, QueryablesMixin
from pystac_client.stac_api_io import StacApiIO, Timeout
from pystac_client.warnings import (
    DoesNotConformTo,
    FallbackToPystac,
    NoConformsTo,
    PystacClientWarning,
)

if TYPE_CHECKING:
    from pystac.item import Item as Item_Type


class Client(pystac.Catalog, QueryablesMixin):
    """A Client for interacting with the root of a STAC Catalog or API

    Instances of the ``Client`` class inherit from :class:`pystac.Catalog`
    and provide a convenient way of interacting
    with STAC Catalogs OR STAC APIs that conform to the `STAC API spec
    <https://github.com/radiantearth/stac-api-spec>`_.
    In addition to being a valid
    `STAC Catalog
    <https://github.com/radiantearth/stac-spec/blob/master/catalog-spec/catalog-spec.md>`_
    APIs that have a ``"conformsTo"`` indicate that it supports additional
    functionality on top of a normal STAC Catalog,
    such as searching items (e.g., /search endpoint).
    """

    _stac_io: StacApiIO | None
    _fallback_strategy: HrefLayoutStrategy = APILayoutStrategy()

    def __init__(
        self,
        id: str,
        description: str,
        title: str | None = None,
        stac_extensions: list[str] | None = None,
        extra_fields: dict[str, Any] | None = None,
        href: str | None = None,
        catalog_type: CatalogType = CatalogType.ABSOLUTE_PUBLISHED,
        strategy: HrefLayoutStrategy | None = None,
        *,
        modifier: Callable[[Modifiable], None] | None = None,
        **kwargs: dict[str, Any],
    ):
        super().__init__(
            id,
            description,
            title=title,
            stac_extensions=stac_extensions,
            extra_fields=extra_fields,
            href=href,
            catalog_type=catalog_type,
            strategy=strategy,
            **kwargs,
        )
        self.modifier = modifier

    def __repr__(self) -> str:
        return f"<Client id={self.id}>"

    @classmethod
    def open(
        cls,
        url: str,
        headers: dict[str, str] | None = None,
        parameters: dict[str, Any] | None = None,
        ignore_conformance: bool | None = None,
        modifier: Callable[[Modifiable], None] | None = None,
        request_modifier: Callable[[Request], Request | None] | None = None,
        stac_io: StacApiIO | None = None,
        timeout: Timeout | None = None,
    ) -> "Client":
        """Opens a STAC Catalog or API
        This function will read the root catalog of a STAC Catalog or API

        Args:
            url : The URL of a STAC Catalog.
            headers : A dictionary of additional headers to use in all requests
                made to any part of this Catalog/API.
            parameters: Optional dictionary of query string parameters to
                include in all requests.
            ignore_conformance (DEPRECATED) : Ignore any advertised Conformance Classes
                in this Catalog/API. This means that
                functions will skip checking conformance, and may throw an unknown
                error if that feature is
                not supported, rather than a :class:`NotImplementedError`.

                .. deprecated:: 0.7.0
                    Conformance can be altered rather than ignored using methods like
                    :meth:`clear_conforms_to` and :meth:`add_conforms_to`

            modifier : A callable that modifies the children collection and items
                returned by this Client. This can be useful for injecting
                authentication parameters into child assets to access data
                from non-public sources.

                The callable should expect a single argument, which will be one
                of the following types:

                * :class:`pystac.Collection`
                * :class:`pystac.Item`
                * :class:`pystac.ItemCollection`
                * A STAC item-like :class:`dict`
                * A STAC collection-like :class:`dict`

                The callable should mutate the argument in place and return ``None``.

                ``modifier`` propagates recursively to children of this Client.
                After getting a child collection with, e.g.
                :meth:`Client.get_collection`, the child items of that collection
                will still be signed with ``modifier``.
            request_modifier: A callable that either modifies a `Request` instance or
                returns a new one. This can be useful for injecting Authentication
                headers and/or signing fully-formed requests (e.g. signing requests
                using AWS SigV4).

                The callable should expect a single argument, which will be an instance
                of :class:`requests.Request`.

                If the callable returns a `requests.Request`, that will be used.
                Alternately, the callable may simply modify the provided request object
                and return `None`.
            stac_io: A `StacApiIO` object to use for I/O requests. Generally, leave
                this to the default. However in cases where customized I/O processing
                is required, a custom instance can be provided here.
            timeout: Optional float or (float, float) tuple following the semantics
              defined by `Requests
              <https://requests.readthedocs.io/en/latest/api/#main-interface>`__.

        Return:
            catalog : A :class:`Client` instance for this Catalog/API
        """
        client: Client = cls.from_file(
            url,
            headers=headers,
            parameters=parameters,
            modifier=modifier,
            request_modifier=request_modifier,
            stac_io=stac_io,
            timeout=timeout,
        )

        if ignore_conformance is not None:
            warnings.warn(
                (
                    "The `ignore_conformance` option is deprecated and will be "
                    "removed in the next major release. Instead use `set_conforms_to` "
                    "or `add_conforms_to` to control behavior."
                ),
                FutureWarning,
            )

        if not client.has_conforms_to():
            warnings.warn(NoConformsTo())

        return client

    @classmethod
    def from_file(  # type: ignore
        cls,
        href: str,
        stac_io: StacApiIO | None = None,
        headers: dict[str, str] | None = None,
        parameters: dict[str, Any] | None = None,
        modifier: Callable[[Modifiable], None] | None = None,
        request_modifier: Callable[[Request], Request | None] | None = None,
        timeout: Timeout | None = None,
    ) -> "Client":
        """Open a STAC Catalog/API

        Returns:
            Client: A Client (PySTAC Catalog) of the root Catalog for this Catalog/API
        """
        if stac_io is None:
            stac_io = StacApiIO(
                headers=headers,
                parameters=parameters,
                request_modifier=request_modifier,
                timeout=timeout,
            )
        else:
            stac_io.update(
                headers=headers,
                parameters=parameters,
                request_modifier=request_modifier,
                timeout=timeout,
            )

        client: Client = super().from_file(href, stac_io)
        client.modifier = modifier

        return client

    def has_conforms_to(self) -> bool:
        """Whether server contains list of ``"conformsTo"`` URIs"""
        return "conformsTo" in self.extra_fields

    def get_conforms_to(self) -> list[str]:
        """List of ``"conformsTo"`` URIs

        Return:
            List[str]: List of  URIs that the server conforms to
        """
        return cast(list[str], self.extra_fields.get("conformsTo", []).copy())

    def set_conforms_to(self, conformance_uris: list[str]) -> None:
        """Set list of ``"conformsTo"`` URIs

        Args:
            conformance_uris : URIs indicating what the server conforms to
        """
        self.extra_fields["conformsTo"] = conformance_uris

    def clear_conforms_to(self) -> None:
        """Clear list of ``"conformsTo"`` urls

        Removes the entire list, so :py:meth:`has_conforms_to` will
        return False after using this method.
        """
        self.extra_fields.pop("conformsTo", None)

    def add_conforms_to(self, name: str) -> None:
        """Add ``"conformsTo"`` by name.

        Args:
            name : name of :py:class:`ConformanceClasses` keys to add.
        """
        conformance_class = ConformanceClasses.get_by_name(name)

        if not self.conforms_to(conformance_class):
            self.set_conforms_to([*self.get_conforms_to(), conformance_class.valid_uri])

    def remove_conforms_to(self, name: str) -> None:
        """Remove ``"conformsTo"`` by name.

        Args:
            name : name of :py:class:`ConformanceClasses` keys to remove.
        """
        conformance_class = ConformanceClasses.get_by_name(name)

        self.set_conforms_to(
            [
                uri
                for uri in self.get_conforms_to()
                if not re.match(conformance_class.pattern, uri)
            ]
        )

    def conforms_to(self, conformance_class: ConformanceClasses | str) -> bool:
        """Checks whether the API conforms to the given standard.

        This method only checks
        against the ``"conformsTo"`` property from the API landing page and does not
        make any additional calls to a ``/conformance`` endpoint even if the API
        provides such an endpoint.

        Args:
            name : name of :py:class:`ConformanceClasses` keys to check
                conformance against.

        Return:
            bool: Indicates if the API conforms to the given spec or URI.
        """
        if isinstance(conformance_class, str):
            conformance_class = ConformanceClasses.get_by_name(conformance_class)

        return any(
            re.match(conformance_class.pattern, uri) for uri in self.get_conforms_to()
        )

    @classmethod
    def from_dict(
        cls,
        d: dict[str, Any],
        href: str | None = None,
        root: pystac.Catalog | None = None,
        migrate: bool = False,
        preserve_dict: bool = True,
        modifier: Callable[[Modifiable], None] | None = None,
    ) -> "Client":
        try:
            # this will return a Client because we have used a StacApiIO instance
            result = super().from_dict(
                d=d, href=href, root=root, migrate=migrate, preserve_dict=preserve_dict
            )
        except pystac.STACTypeError:
            raise ClientTypeError(
                f"Could not open Client (href={href}), "
                f"expected type=Catalog, found type={d.get('type', None)}"
            )

        result.modifier = modifier
        return result

    def _supports_collections(self) -> bool:
        return self.conforms_to(ConformanceClasses.COLLECTIONS) or self.conforms_to(
            ConformanceClasses.FEATURES
        )

    def _warn_about_fallback(self, *args: str) -> None:
        if self.has_conforms_to():
            warnings.warn(DoesNotConformTo(*args), stacklevel=2)
        warnings.warn(FallbackToPystac(), stacklevel=2)

    def get_merged_queryables(self, collections: list[str]) -> dict[str, Any]:
        """Return the set of queryables in common to the specified collections.

        Queryables from multiple collections are unioned together, except in the case
        when the same queryable key has a different definition, in which case that key
        is dropped.

        Output is a dictionary that can be used in ``jsonshema.validate``

        Args:
            collections List[str]: The IDs of the collections to inspect.

        Return:
            Dict[str, Any]: Dictionary containing queryable fields
        """
        if not collections:
            raise ValueError("cannot get_merged_queryables from empty Iterable")

        if not self.conforms_to(ConformanceClasses.FILTER):
            raise DoesNotConformTo(ConformanceClasses.FILTER.name)
        response = self.get_queryables_from(
            self._get_collection_queryables_href(collections[0])
        )
        response.pop("$id")
        addl_props = response.get("additionalProperties", False)
        for collection in collections[1:]:
            resp = self.get_queryables_from(
                self._get_collection_queryables_href(collection)
            )

            # additionalProperties is false if any collection doesn't support additional
            # properties
            addl_props &= resp.get("additionalProperties", False)

            # drop queryables if their keys match, but the descriptions differ
            for k in set(resp["properties"]).intersection(response["properties"]):
                if resp["properties"][k] != response["properties"][k]:
                    resp["properties"].pop(k)
                    response["properties"].pop(k)
            response["properties"].update(resp["properties"])
        return response

    @lru_cache
    def get_collection(
        self, collection_id: str
    ) -> Collection | CollectionClient | None:
        """Get a single collection from this Catalog/API

        Args:
            collection_id: The Collection ID to get

        Returns:
            Collection | CollectionClient | None: A STAC Collection or None if it does
            not exist.

        """
        collection: Collection | CollectionClient | None = None

        if not collection_id:
            raise ValueError("A `collection_id` must be provided.")

        if self._supports_collections():
            assert self._stac_io is not None

            url = self._collections_href(collection_id)
            try:
                collection = CollectionClient.from_dict(
                    self._stac_io.read_json(url),
                    root=self,
                    modifier=self.modifier,
                )
            except APIError as err:
                if getattr(err, "status_code", None) and err.status_code == 404:
                    return None
                else:
                    raise err
        else:
            self._warn_about_fallback("COLLECTIONS", "FEATURES")
            for collection in super().get_collections():
                if collection.id == collection_id:
                    break

        if collection:
            call_modifier(self.modifier, collection)

        return collection

    def get_collections(self) -> Iterator[Collection]:
        """Get Collections in this Catalog

            Gets the collections from the /collections endpoint if supported,
            otherwise fall back to Catalog behavior of following child links

        Return:
            Iterator[Union[Collection, CollectionClient]]: Collections in Catalog/API
        """
        collection: Collection | CollectionClient

        if self._supports_collections():
            assert self._stac_io is not None

            url = self._collections_href()
            for page in self._stac_io.get_pages(url):
                if "collections" not in page:
                    raise APIError("Invalid response from /collections")
                for col in page["collections"]:
                    collection = CollectionClient.from_dict(
                        col, root=self, modifier=self.modifier
                    )
                    call_modifier(self.modifier, collection)
                    yield collection
        else:
            self._warn_about_fallback("COLLECTIONS", "FEATURES")
            for collection in super().get_collections():
                call_modifier(self.modifier, collection)
                yield collection

    def get_items(
        self, *ids: str, recursive: bool | None = None
    ) -> Iterator["Item_Type"]:
        """Return all items of this catalog.

        Args:
            ids: Zero or more item ids to find.
            recursive: If this client conforms to the ITEM_SEARCH conformance class,
                this is unused and this will always yield items recursively.
                Otherwise, this will only return items recursively if True or None.
        Return:
            Iterator[Item]: Iterator of items whose parent is this
                catalog.
        """
        if self.conforms_to(ConformanceClasses.ITEM_SEARCH):
            if recursive is False:
                warnings.warn(
                    "Getting items recursively using the /search endpoint "
                    "(the recursive argument is being ignored).",
                    PystacClientWarning,
                )
            search = self.search(ids=ids)
            yield from search.items()
        else:
            self._warn_about_fallback("ITEM_SEARCH")
            for item in super().get_items(
                *ids, recursive=recursive is None or recursive
            ):
                call_modifier(self.modifier, item)
                yield item

    def get_all_items(self) -> Iterator["Item_Type"]:
        """Get all items from this catalog and all subcatalogs. Will traverse
        any subcatalogs recursively, or use the /search endpoint if supported

        Returns:
            Iterator[Item]: All items that belong to this catalog, and all
                catalogs or collections connected to this catalog through
                child links.
        """
        yield from self.get_items()

    def search(
        self,
        *,
        method: str | None = "POST",
        max_items: int | None = None,
        limit: int | None = None,
        ids: IDsLike | None = None,
        collections: CollectionsLike | None = None,
        bbox: BBoxLike | None = None,
        intersects: IntersectsLike | None = None,
        datetime: DatetimeLike | None = None,
        query: QueryLike | None = None,
        filter: FilterLike | None = None,
        filter_lang: FilterLangLike | None = None,
        sortby: SortbyLike | None = None,
        fields: FieldsLike | None = None,
    ) -> ItemSearch:
        """Query the ``/search`` endpoint using the given parameters.

        This method returns an :class:`~pystac_client.ItemSearch` instance. See that
        class's documentation for details on how to get the number of matches and
        iterate over results. The ``url``, ``stac_io``, and ``client`` keywords are
        supplied by this Client instance.

        .. warning::

            This method is only implemented if the API conforms to the
            `STAC API - Item Search
            <https://github.com/radiantearth/stac-api-spec/tree/master/item-search>`__
            spec *and* contains a link with a ``"rel"`` type of ``"search"`` in its
            root catalog. If the API does not meet either of these criteria, this
            method will raise a :exc:`NotImplementedError`.

        Args:
            method : The HTTP method to use when making a request to the service.
                This must be either ``"GET"``, ``"POST"``, or
                ``None``. If ``None``, this will default to ``"POST"``.
                If a ``"POST"`` request receives a ``405`` status for
                the response, it will automatically retry with
                ``"GET"`` for all subsequent requests.
            max_items : The maximum number of items to return from the search, even
                if there are more matching results. This client to limit the
                total number of Items returned from the :meth:`items`,
                :meth:`item_collections`, and :meth:`items_as_dicts methods`. The client
                will continue to request pages of items until the number of max items is
                reached. Setting this to ``None`` will allow iteration over a possibly
                very large number of results.
            limit: A recommendation to the service as to the number of items to return
                *per page* of results. Defaults to 100.
            ids: List of one or more Item ids to filter on.
            collections: List of one or more Collection IDs or
                :class:`pystac.Collection` instances. Only Items in one
                of the provided Collections will be searched
            bbox: A list, tuple, or iterator representing a bounding box of 2D
                or 3D coordinates. Results will be filtered
                to only those intersecting the bounding box.
            intersects: A string or dictionary representing a GeoJSON geometry, or
                an object that implements a
                ``__geo_interface__`` property, as supported by several libraries
                including Shapely, ArcPy, PySAL, and
                geojson. Results filtered to only those intersecting the geometry.
            datetime: Either a single datetime or datetime range used to filter results.
                You may express a single datetime using a :class:`datetime.datetime`
                instance, a `RFC 3339-compliant <https://tools.ietf.org/html/rfc3339>`__
                timestamp, or a simple date string (see below). Instances of
                :class:`datetime.datetime` may be either
                timezone aware or unaware. Timezone aware instances will be converted to
                a UTC timestamp before being passed
                to the endpoint. Timezone unaware instances are assumed to represent UTC
                timestamps. You may represent a
                datetime range using a ``"/"`` separated string as described in the
                spec, or a list, tuple, or iterator
                of 2 timestamps or datetime instances. For open-ended ranges, use either
                ``".."`` (``'2020-01-01:00:00:00Z/..'``,
                ``['2020-01-01:00:00:00Z', '..']``) or a value of ``None``
                (``['2020-01-01:00:00:00Z', None]``).

                If using a simple date string, the datetime can be specified in
                ``YYYY-mm-dd`` format, optionally truncating
                to ``YYYY-mm`` or just ``YYYY``. Simple date strings will be expanded to
                include the entire time period, for example:

                - ``2017`` expands to ``2017-01-01T00:00:00Z/2017-12-31T23:59:59Z``
                - ``2017-06`` expands to ``2017-06-01T00:00:00Z/2017-06-30T23:59:59Z``
                - ``2017-06-10`` expands to
                  ``2017-06-10T00:00:00Z/2017-06-10T23:59:59Z``

                If used in a range, the end of the range expands to the end of that
                day/month/year, for example:

                - ``2017/2018`` expands to
                  ``2017-01-01T00:00:00Z/2018-12-31T23:59:59Z``
                - ``2017-06/2017-07`` expands to
                  ``2017-06-01T00:00:00Z/2017-07-31T23:59:59Z``
                - ``2017-06-10/2017-06-11`` expands to
                  ``2017-06-10T00:00:00Z/2017-06-11T23:59:59Z``

            query: List or JSON of query parameters as per the STAC API `query`
                extension
            filter: JSON of query parameters as per the STAC API `filter` extension
            filter_lang: Language variant used in the filter body. If `filter` is a
                dictionary or not provided, defaults
                to 'cql2-json'. If `filter` is a string, defaults to `cql2-text`.
            sortby: A single field or list of fields to sort the response by
            fields: A list of fields to include in the response. Note this may
                result in invalid STAC objects, as they may not have required fields.
                Use `items_as_dicts` to avoid object unmarshalling errors.

        Returns:
            search : An ItemSearch instance that can be used to iterate through Items.

        Raises:
            NotImplementedError: If the API does not conform to the `Item Search spec
                <https://github.com/radiantearth/stac-api-spec/tree/master/item-search>`__
                or does not have a link with
                a ``"rel"`` type of ``"search"``.
        """

        if not self.conforms_to(ConformanceClasses.ITEM_SEARCH):
            raise DoesNotConformTo(
                "ITEM_SEARCH", "There is no fallback option available for search."
            )

        return ItemSearch(
            url=self._search_href(),
            method=method,
            max_items=max_items,
            client=self,
            limit=limit,
            ids=ids,
            collections=collections,
            bbox=bbox,
            intersects=intersects,
            datetime=datetime,
            query=query,
            filter=filter,
            filter_lang=filter_lang,
            sortby=sortby,
            fields=fields,
            modifier=self.modifier,
        )

    def collection_search(
        self,
        *,
        max_collections: int | None = None,
        limit: int | None = None,
        bbox: BBoxLike | None = None,
        datetime: DatetimeLike | None = None,
        q: str | None = None,
        query: QueryLike | None = None,
        filter: FilterLike | None = None,
        filter_lang: FilterLangLike | None = None,
        sortby: SortbyLike | None = None,
        fields: FieldsLike | None = None,
    ) -> CollectionSearch:
        """Query the ``/collections`` endpoint using the given parameters.

        This method returns an :class:`~pystac_client.CollectionSearch` instance. See
        that class's documentation for details on how to get the number of matches and
        iterate over results. The ``url``, `stac_io``, and ``client`` keywords are
        supplied by this Client instance.

        .. warning::

            This method is fully implemented if the API conforms to the
            `STAC API - Collection Search Extension
            <https://github.com/stac-api-extensions/collection-search>`__
            spec.

            If the API does not conform to the Collection Search Extension but
            conforms to `STAC API - Collections <https://api.stacspec.org/v1.0.0/collections/>`__
            (has a ``"rel"`` type of ``"data"`` in its root catalog), a limited
            client-side collection filtering process will be used. If the API does not
            meet either of these criteria, this method will raise a
            :exc:`NotImplementedError`.

            Client-side filtering will only use the ``bbox``, ``datetime``, and ``q``
            (free-text search) parameters.

        Args:
            max_collections : The maximum number of collections to return from the
                search, even if there are more matching results. This client to limit
                the total number of Collections returned from the :meth:`collections`,
                :meth:`collection_list`, and :meth:`collections_as_dicts methods`. The
                client will continue to request pages of collections until the number of
                max collections is reached. Setting this to ``None`` will allow
                iteration over a possibly very large number of results.
            limit: A recommendation to the service as to the number of items to return
                *per page* of results. Defaults to 100.
            bbox: A list, tuple, or iterator representing a bounding box of 2D
                or 3D coordinates. Results will be filtered
                to only those intersecting the bounding box.
            datetime: Either a single datetime or datetime range used to filter results.
                You may express a single datetime using a :class:`datetime.datetime`
                instance, a `RFC 3339-compliant <https://tools.ietf.org/html/rfc3339>`__
                timestamp, or a simple date string (see below). Instances of
                :class:`datetime.datetime` may be either
                timezone aware or unaware. Timezone aware instances will be converted to
                a UTC timestamp before being passed
                to the endpoint. Timezone unaware instances are assumed to represent UTC
                timestamps. You may represent a
                datetime range using a ``"/"`` separated string as described in the
                spec, or a list, tuple, or iterator
                of 2 timestamps or datetime instances. For open-ended ranges, use either
                ``".."`` (``'2020-01-01:00:00:00Z/..'``,
                ``['2020-01-01:00:00:00Z', '..']``) or a value of ``None``
                (``['2020-01-01:00:00:00Z', None]``).

                If using a simple date string, the datetime can be specified in
                ``YYYY-mm-dd`` format, optionally truncating
                to ``YYYY-mm`` or just ``YYYY``. Simple date strings will be expanded to
                include the entire time period, for example:

                - ``2017`` expands to ``2017-01-01T00:00:00Z/2017-12-31T23:59:59Z``
                - ``2017-06`` expands to ``2017-06-01T00:00:00Z/2017-06-30T23:59:59Z``
                - ``2017-06-10`` expands to
                  ``2017-06-10T00:00:00Z/2017-06-10T23:59:59Z``

                If used in a range, the end of the range expands to the end of that
                day/month/year, for example:

                - ``2017/2018`` expands to
                  ``2017-01-01T00:00:00Z/2018-12-31T23:59:59Z``
                - ``2017-06/2017-07`` expands to
                  ``2017-06-01T00:00:00Z/2017-07-31T23:59:59Z``
                - ``2017-06-10/2017-06-11`` expands to
                  ``2017-06-10T00:00:00Z/2017-06-11T23:59:59Z``

            q: Free-text search query. See the `STAC API - Free Text Extension
               Spec <https://github.com/stac-api-extensions/freetext-search>`__ for
               details.
            query: List or JSON of query parameters as per the STAC API `query`
                extension
            filter: JSON of query parameters as per the STAC API `filter` extension
            filter_lang: Language variant used in the filter body. If `filter` is a
                dictionary or not provided, defaults
                to 'cql2-json'. If `filter` is a string, defaults to `cql2-text`.
            sortby: A single field or list of fields to sort the response by
            fields: A list of fields to include in the response. Note this may
                result in invalid STAC objects, as they may not have required fields.
                Use `items_as_dicts` to avoid object unmarshalling errors.

        Returns:
            collection_search : A CollectionSearch instance that can be used to iterate
            through Collections.

        Raises:
            NotImplementedError: If the API does not conform to either the `STAC API -
                Collection Search spec <https://github.com/stac-api-extensions/collection-search>`__
                or the `STAC API - Collections spec <https://api.stacspec.org/v1.0.0/collections>`__.
        """

        if not (
            self.conforms_to(ConformanceClasses.COLLECTION_SEARCH)
            or self.conforms_to(ConformanceClasses.COLLECTIONS)
        ) and any([bbox, datetime, q, query, filter, sortby, fields]):
            raise DoesNotConformTo(
                "COLLECTION_SEARCH or COLLECTIONS",
                "there is no fallback option available for search.",
            )

        return CollectionSearch(
            url=self._collections_href(),
            client=self,
            max_collections=max_collections,
            limit=limit,
            bbox=bbox,
            datetime=datetime,
            q=q,
            query=query,
            filter=filter,
            filter_lang=filter_lang,
            sortby=sortby,
            fields=fields,
            modifier=self.modifier,
        )

    def get_search_link(self) -> pystac.Link | None:
        """Returns this client's search link.

        Searches for a link with rel="search" and either a GEOJSON or JSON media type.

        Returns:
            Optional[pystac.Link]: The search link, or None if there is not one found.
        """
        return next(
            (
                link
                for link in self.links
                if link.rel == "search"
                and (
                    link.media_type == pystac.MediaType.GEOJSON
                    or link.media_type == pystac.MediaType.JSON
                )
            ),
            None,
        )

    def _search_href(self) -> str:
        search_link = self.get_search_link()
        href = self._get_href("search", search_link, "search")
        return href

    def _collections_href(self, collection_id: str | None = None) -> str:
        data_link = self.get_single_link("data")
        href = self._get_href("data", data_link, "collections")
        if collection_id is not None:
            return urljoin(href, collection_id)
        return href

    def _get_collection_queryables_href(self, collection_id: str | None = None) -> str:
        href = self._collections_href(collection_id)
        return urljoin(href, QUERYABLES_ENDPOINT)


================================================
FILE: pystac_client/collection_client.py
================================================
from __future__ import annotations

import warnings
from collections.abc import Callable, Iterator
from typing import (
    TYPE_CHECKING,
    Any,
    Optional,
    cast,
)

import pystac
from pystac.layout import APILayoutStrategy, HrefLayoutStrategy

from pystac_client._utils import Modifiable, call_modifier
from pystac_client.conformance import ConformanceClasses
from pystac_client.exceptions import APIError
from pystac_client.item_search import ItemSearch
from pystac_client.mixins import QueryablesMixin
from pystac_client.stac_api_io import StacApiIO
from pystac_client.warnings import FallbackToPystac

if TYPE_CHECKING:
    from pystac.item import Item as Item_Type

    from pystac_client import Client


class CollectionClient(pystac.Collection, QueryablesMixin):
    modifier: Callable[[Modifiable], None]
    _stac_io: StacApiIO
    _fallback_strategy: HrefLayoutStrategy = APILayoutStrategy()

    def __init__(
        self,
        id: str,
        description: str,
        extent: pystac.Extent,
        title: str | None = None,
        stac_extensions: list[str] | None = None,
        href: str | None = None,
        extra_fields: dict[str, Any] | None = None,
        catalog_type: pystac.CatalogType | None = None,
        license: str = "proprietary",
        keywords: list[str] | None = None,
        providers: list[pystac.Provider] | None = None,
        summaries: pystac.Summaries | None = None,
        assets: dict[str, pystac.Asset] | None = None,
        strategy: HrefLayoutStrategy | None = None,
        *,
        modifier: Callable[[Modifiable], None] | None = None,
        **kwargs: dict[str, Any],
    ):
        super().__init__(
            id,
            description,
            extent,
            title,
            stac_extensions,
            href,
            extra_fields,
            catalog_type,
            license,
            keywords,
            providers,
            summaries,
            assets,
            strategy,
            **kwa
Download .txt
gitextract_yi101d5s/

├── .adr-dir
├── .codespellignore
├── .gitattributes
├── .github/
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── continuous-integration.yml
│       └── release.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yml
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── RELEASING.md
├── docs/
│   ├── _static/
│   │   └── custom.css
│   ├── api.rst
│   ├── conf.py
│   ├── contributing.rst
│   ├── design/
│   │   ├── 0001-record-architecture-decisions.md
│   │   ├── 0002-choose-stac-library.md
│   │   └── design_decisions.rst
│   ├── index.rst
│   ├── quickstart.rst
│   ├── tutorials/
│   │   ├── aoi-coverage.ipynb
│   │   ├── authentication.md
│   │   ├── cql2-filter.ipynb
│   │   ├── item-search-intersects.ipynb
│   │   ├── pystac-client-introduction.ipynb
│   │   └── stac-metadata-viz.ipynb
│   ├── tutorials.rst
│   └── usage.rst
├── pyproject.toml
├── pystac_client/
│   ├── __init__.py
│   ├── _utils.py
│   ├── cli.py
│   ├── client.py
│   ├── collection_client.py
│   ├── collection_search.py
│   ├── conformance.py
│   ├── errors.py
│   ├── exceptions.py
│   ├── free_text.py
│   ├── item_search.py
│   ├── mixins.py
│   ├── py.typed
│   ├── stac_api_io.py
│   ├── version.py
│   └── warnings.py
├── scripts/
│   ├── build-docs
│   ├── format
│   ├── lint
│   └── test
└── tests/
    ├── __init__.py
    ├── cassettes/
    │   ├── test_cli/
    │   │   ├── TestCLICollections.test_collection_search[inprocess].yaml
    │   │   ├── TestCLICollections.test_collections[inprocess].yaml
    │   │   ├── TestCLICollections.test_save[inprocess].yaml
    │   │   ├── TestCLISearch.test_altering_conforms_to[inprocess---clear-conforms-to].yaml
    │   │   ├── TestCLISearch.test_altering_conforms_to[inprocess---remove-conforms-to=ITEM_SEARCH].yaml
    │   │   ├── TestCLISearch.test_fields[inprocess].yaml
    │   │   ├── TestCLISearch.test_filter[inprocess].yaml
    │   │   ├── TestCLISearch.test_intersects[inprocess-netherlands_aoi.json].yaml
    │   │   ├── TestCLISearch.test_intersects[inprocess-sample-item.json].yaml
    │   │   ├── TestCLISearch.test_intersects_despite_warning[inprocess].yaml
    │   │   ├── TestCLISearch.test_item_search[inprocess].yaml
    │   │   ├── TestCLISearch.test_matched[inprocess].yaml
    │   │   ├── TestCLISearch.test_matched_not_available[inprocess].yaml
    │   │   ├── TestCLISearch.test_non_conformant_can_be_fixed[inprocess].yaml
    │   │   ├── TestCLISearch.test_non_conformant_can_be_ignored[inprocess---ignore=no-conforms-to].yaml
    │   │   ├── TestCLISearch.test_non_conformant_can_be_ignored[inprocess---ignore].yaml
    │   │   ├── TestCLISearch.test_non_conformant_raises_by_default[inprocess].yaml
    │   │   ├── TestCLISearch.test_non_conformant_raises_if_warning_set_to_error[inprocess---error=no-conforms-to].yaml
    │   │   ├── TestCLISearch.test_non_conformant_raises_if_warning_set_to_error[inprocess---error].yaml
    │   │   └── TestCLISearch.test_save[inprocess].yaml
    │   ├── test_client/
    │   │   ├── TestAPI.test_collections_fallback.yaml
    │   │   ├── TestAPI.test_environment_variable.yaml
    │   │   ├── TestAPI.test_from_file.yaml
    │   │   ├── TestAPI.test_instance.yaml
    │   │   ├── TestAPI.test_links.yaml
    │   │   ├── TestAPICollectionSearch.test_search.yaml
    │   │   ├── TestAPISearch.test_search_max_items_unlimited_default.yaml
    │   │   ├── TestConformsTo.test_changing_conforms_to_changes_behavior.yaml
    │   │   ├── TestQueryables.test_get_queryables.yaml
    │   │   ├── TestQueryables.test_get_queryables_collections.yaml
    │   │   ├── TestSigning.test_sign_with_return_warns.yaml
    │   │   ├── TestSigning.test_signing.yaml
    │   │   ├── test_collections_are_clients.yaml
    │   │   ├── test_fallback_strategy.yaml
    │   │   ├── test_get_collection_returns_none_if_not_found.yaml
    │   │   ├── test_get_items_without_ids.yaml
    │   │   ├── test_non_recursion_on_fallback.yaml
    │   │   └── test_query_string_in_collections_url.yaml
    │   ├── test_collection_client/
    │   │   ├── TestCollectionClient.test_get_item.yaml
    │   │   ├── TestCollectionClient.test_get_item_with_item_search.yaml
    │   │   ├── TestCollectionClient.test_get_items.yaml
    │   │   ├── TestCollectionClient.test_get_items_with_ids.yaml
    │   │   ├── TestCollectionClient.test_get_queryables.yaml
    │   │   └── TestCollectionClient.test_instance.yaml
    │   ├── test_collection_search/
    │   │   ├── TestCollectionSearch.test_bbox_results.yaml
    │   │   ├── TestCollectionSearch.test_client_side_bbox.yaml
    │   │   ├── TestCollectionSearch.test_client_side_datetime.yaml
    │   │   ├── TestCollectionSearch.test_client_side_q.yaml
    │   │   ├── TestCollectionSearch.test_datetime_results.yaml
    │   │   ├── TestCollectionSearch.test_enabled_but_client_side_q.yaml
    │   │   ├── TestCollectionSearch.test_matched.yaml
    │   │   ├── TestCollectionSearch.test_q_results.yaml
    │   │   ├── TestCollectionSearch.test_result_paging.yaml
    │   │   └── TestCollectionSearch.test_result_paging_max_collections.yaml
    │   ├── test_item_search/
    │   │   ├── TestItemSearch.test_datetime_results.yaml
    │   │   ├── TestItemSearch.test_deprecations[get_all_items-item_collection-False-True].yaml
    │   │   ├── TestItemSearch.test_deprecations[get_all_items_as_dict-item_collection_as_dict-False-False].yaml
    │   │   ├── TestItemSearch.test_deprecations[get_item_collections-pages-True-True].yaml
    │   │   ├── TestItemSearch.test_deprecations[get_items-items-True-True].yaml
    │   │   ├── TestItemSearch.test_deprecations[item_collections-pages-True-True].yaml
    │   │   ├── TestItemSearch.test_get_all_items.yaml
    │   │   ├── TestItemSearch.test_get_all_items_deprecated.yaml
    │   │   ├── TestItemSearch.test_ids_results.yaml
    │   │   ├── TestItemSearch.test_intersects_results.yaml
    │   │   ├── TestItemSearch.test_item_collection.yaml
    │   │   ├── TestItemSearch.test_items_as_dicts.yaml
    │   │   ├── TestItemSearch.test_result_paging.yaml
    │   │   ├── TestItemSearch.test_result_paging_max_items.yaml
    │   │   ├── TestItemSearch.test_results.yaml
    │   │   ├── TestItemSearchParams.test_collection_object.yaml
    │   │   ├── TestItemSearchParams.test_mixed_collection_object_and_string.yaml
    │   │   ├── TestItemSearchQuery.test_query_json_syntax.yaml
    │   │   ├── TestItemSearchQuery.test_query_shortcut_syntax.yaml
    │   │   ├── test_fields.yaml
    │   │   └── test_multiple_collections.yaml
    │   └── test_stac_api_io/
    │       ├── TestSTAC_IOOverride.test_request_input.yaml
    │       ├── TestSTAC_IOOverride.test_str_input.yaml
    │       ├── TestSTAC_IOOverride.test_timeout_smoke_test.yaml
    │       └── test_stac_io_in_pystac.yaml
    ├── data/
    │   ├── astraea_api.json
    │   ├── fedeo_clearinghouse.json
    │   ├── invalid-collection.json
    │   ├── netherlands_aoi.json
    │   ├── planetary-computer-aster-l1t-collection.json
    │   ├── planetary-computer-collection.json
    │   ├── planetary-computer-root.json
    │   ├── sample-item-collection.json
    │   ├── sample-item.json
    │   └── test-case-1/
    │       ├── catalog.json
    │       ├── country-1/
    │       │   ├── area-1-1/
    │       │   │   ├── area-1-1-imagery/
    │       │   │   │   └── area-1-1-imagery.json
    │       │   │   ├── area-1-1-labels/
    │       │   │   │   └── area-1-1-labels.json
    │       │   │   └── collection.json
    │       │   ├── area-1-2/
    │       │   │   ├── area-1-2-imagery/
    │       │   │   │   └── area-1-2-imagery.json
    │       │   │   ├── area-1-2-labels/
    │       │   │   │   └── area-1-2-labels.json
    │       │   │   └── collection.json
    │       │   └── catalog.json
    │       └── country-2/
    │           ├── area-2-1/
    │           │   ├── area-2-1-imagery/
    │           │   │   └── area-2-1-imagery.json
    │           │   ├── area-2-1-labels/
    │           │   │   └── area-2-1-labels.json
    │           │   └── collection.json
    │           ├── area-2-2/
    │           │   ├── area-2-2-imagery/
    │           │   │   └── area-2-2-imagery.json
    │           │   ├── area-2-2-labels/
    │           │   │   └── area-2-2-labels.json
    │           │   └── collection.json
    │           └── catalog.json
    ├── helpers.py
    ├── test_base_search.py
    ├── test_cli.py
    ├── test_client.py
    ├── test_collection_client.py
    ├── test_collection_search.py
    ├── test_conformance.py
    ├── test_free_text.py
    ├── test_item_search.py
    ├── test_stac_api_io.py
    └── test_warnings.py
Download .txt
SYMBOL INDEX (360 symbols across 24 files)

FILE: pystac_client/_utils.py
  function call_modifier (line 15) | def call_modifier(
  function urljoin (line 32) | def urljoin(href: str, name: str) -> str:

FILE: pystac_client/cli.py
  function search (line 39) | def search(
  function collections (line 91) | def collections(
  function add_warnings_behavior (line 141) | def add_warnings_behavior(parser: argparse.ArgumentParser) -> None:
  function set_warnings (line 167) | def set_warnings(error: list[str] | None, ignore: list[str] | None) -> N...
  function set_conforms_to (line 189) | def set_conforms_to(
  function parse_args (line 203) | def parse_args(args: list[str]) -> dict[str, Any]:
  function cli (line 400) | def cli() -> int:

FILE: pystac_client/client.py
  class Client (line 50) | class Client(pystac.Catalog, QueryablesMixin):
    method __init__ (line 68) | def __init__(
    method __repr__ (line 95) | def __repr__(self) -> str:
    method open (line 99) | def open(
    method from_file (line 196) | def from_file(  # type: ignore
    method has_conforms_to (line 231) | def has_conforms_to(self) -> bool:
    method get_conforms_to (line 235) | def get_conforms_to(self) -> list[str]:
    method set_conforms_to (line 243) | def set_conforms_to(self, conformance_uris: list[str]) -> None:
    method clear_conforms_to (line 251) | def clear_conforms_to(self) -> None:
    method add_conforms_to (line 259) | def add_conforms_to(self, name: str) -> None:
    method remove_conforms_to (line 270) | def remove_conforms_to(self, name: str) -> None:
    method conforms_to (line 286) | def conforms_to(self, conformance_class: ConformanceClasses | str) -> ...
    method from_dict (line 309) | def from_dict(
    method _supports_collections (line 332) | def _supports_collections(self) -> bool:
    method _warn_about_fallback (line 337) | def _warn_about_fallback(self, *args: str) -> None:
    method get_merged_queryables (line 342) | def get_merged_queryables(self, collections: list[str]) -> dict[str, A...
    method get_collection (line 385) | def get_collection(
    method get_collections (line 429) | def get_collections(self) -> Iterator[Collection]:
    method get_items (line 459) | def get_items(
    method get_all_items (line 490) | def get_all_items(self) -> Iterator["Item_Type"]:
    method search (line 501) | def search(
    method collection_search (line 643) | def collection_search(
    method get_search_link (line 779) | def get_search_link(self) -> pystac.Link | None:
    method _search_href (line 800) | def _search_href(self) -> str:
    method _collections_href (line 805) | def _collections_href(self, collection_id: str | None = None) -> str:
    method _get_collection_queryables_href (line 812) | def _get_collection_queryables_href(self, collection_id: str | None = ...

FILE: pystac_client/collection_client.py
  class CollectionClient (line 29) | class CollectionClient(pystac.Collection, QueryablesMixin):
    method __init__ (line 34) | def __init__(
    method from_dict (line 76) | def from_dict(
    method __repr__ (line 91) | def __repr__(self) -> str:
    method set_root (line 94) | def set_root(self, root: pystac.Catalog | Client | None) -> None:
    method get_root (line 104) | def get_root(self) -> Client:
    method conforms_to (line 114) | def conforms_to(self, conformance_class: ConformanceClasses | str) -> ...
    method get_items (line 118) | def get_items(self, *ids: str, recursive: bool = False) -> Iterator[It...
    method get_item (line 154) | def get_item(self, id: str, recursive: bool = False) -> Item_Type | None:
    method _items_href (line 208) | def _items_href(self) -> str:

FILE: pystac_client/collection_search.py
  function temporal_intervals_overlap (line 38) | def temporal_intervals_overlap(
  function bboxes_overlap (line 50) | def bboxes_overlap(bbox1: BBox, bbox2: BBox) -> bool:
  function _extent_matches (line 57) | def _extent_matches(
  function collection_matches (line 111) | def collection_matches(
  class CollectionSearch (line 148) | class CollectionSearch(BaseSearch):
    method __init__ (line 258) | def __init__(
    method _validate_client_side_args (line 334) | def _validate_client_side_args(self) -> None:
    method matched (line 347) | def matched(self) -> int | None:
    method collections (line 389) | def collections(self) -> Iterator[Collection]:
    method collections_as_dicts (line 403) | def collections_as_dicts(self) -> Iterator[dict[str, Any]]:
    method pages (line 415) | def pages(self) -> Iterator[list[Collection]]:
    method pages_as_dicts (line 432) | def pages_as_dicts(self) -> Iterator[dict[str, Any]]:
    method collection_list (line 500) | def collection_list(self) -> list[Collection]:
    method collection_list_as_dict (line 517) | def collection_list_as_dict(self) -> dict[str, Any]:

FILE: pystac_client/conformance.py
  class ConformanceClasses (line 7) | class ConformanceClasses(Enum):
    method get_by_name (line 30) | def get_by_name(cls, name: str) -> ConformanceClasses:
    method __str__ (line 38) | def __str__(self) -> str:
    method __repr__ (line 41) | def __repr__(self) -> str:
    method valid_uri (line 45) | def valid_uri(self) -> str:
    method pattern (line 49) | def pattern(self) -> re.Pattern[str]:

FILE: pystac_client/errors.py
  class ClientTypeError (line 1) | class ClientTypeError(Exception):
  class IgnoredResultWarning (line 7) | class IgnoredResultWarning(RuntimeWarning):

FILE: pystac_client/exceptions.py
  class APIError (line 4) | class APIError(Exception):
    method from_response (line 10) | def from_response(cls, response: Response) -> "APIError":
  class ParametersError (line 16) | class ParametersError(Exception):

FILE: pystac_client/free_text.py
  function parse_query_for_sqlite (line 12) | def parse_query_for_sqlite(q: str) -> str:
  function sqlite_text_search (line 37) | def sqlite_text_search(q: str, text_fields: dict[str, str]) -> bool:

FILE: pystac_client/item_search.py
  class GeoInterface (line 39) | class GeoInterface(Protocol):
    method __geo_interface__ (line 41) | def __geo_interface__(self) -> dict[str, Any]: ...
  function dict_merge (line 91) | def dict_merge(
  class BaseSearch (line 125) | class BaseSearch(ABC):
    method __init__ (line 128) | def __init__(
    method get_parameters (line 182) | def get_parameters(self) -> dict[str, Any]:
    method _clean_params_for_get_request (line 190) | def _clean_params_for_get_request(self) -> dict[str, Any]:
    method url_with_parameters (line 212) | def url_with_parameters(self) -> str:
    method _format_query (line 238) | def _format_query(self, value: QueryLike | None) -> dict[str, Any] | N...
    method _format_filter_lang (line 271) | def _format_filter_lang(
    method _format_filter (line 290) | def _format_filter(
    method _format_bbox (line 338) | def _format_bbox(value: BBoxLike | None) -> BBox | None:
    method _to_utc_isoformat (line 350) | def _to_utc_isoformat(dt: datetime_) -> str:
    method _to_isoformat_range (line 356) | def _to_isoformat_range(
    method _format_datetime (line 413) | def _format_datetime(self, value: DatetimeLike | None) -> Datetime | N...
    method _format_collections (line 446) | def _format_collections(value: CollectionsLike | None) -> Collections ...
    method _format_ids (line 465) | def _format_ids(value: IDsLike | None) -> IDs | None:
    method _format_sortby (line 479) | def _format_sortby(self, value: SortbyLike | None) -> Sortby | None:
    method _sortby_part_to_dict (line 500) | def _sortby_part_to_dict(part: str) -> dict[str, str]:
    method _sortby_dict_to_str (line 509) | def _sortby_dict_to_str(sortby: Sortby) -> str:
    method _format_fields (line 517) | def _format_fields(self, value: FieldsLike | None) -> Fields | None:
    method _fields_to_dict (line 538) | def _fields_to_dict(fields: list[str]) -> Fields:
    method _fields_dict_to_str (line 551) | def _fields_dict_to_str(fields: Fields) -> str:
    method _format_intersects (line 557) | def _format_intersects(value: IntersectsLike | None) -> Intersects | N...
  function __getattr__ (line 579) | def __getattr__(name: str) -> Any:
  class ItemSearch (line 588) | class ItemSearch(BaseSearch):
    method __init__ (line 704) | def __init__(
    method matched (line 753) | def matched(self) -> int | None:
    method items (line 778) | def items(self) -> Iterator[Item]:
    method items_as_dicts (line 789) | def items_as_dicts(self) -> Iterator[dict[str, Any]]:
    method pages (line 801) | def pages(self) -> Iterator[ItemCollection]:
    method pages_as_dicts (line 816) | def pages_as_dicts(self) -> Iterator[dict[str, Any]]:
    method item_collection (line 846) | def item_collection(self) -> ItemCollection:
    method item_collection_as_dict (line 862) | def item_collection_as_dict(self) -> dict[str, Any]:
    method get_item_collections (line 885) | def get_item_collections(self) -> Iterator[ItemCollection]:
    method item_collections (line 900) | def item_collections(self) -> Iterator[ItemCollection]:
    method get_items (line 916) | def get_items(self) -> Iterator[Item]:
    method get_all_items (line 931) | def get_all_items(self) -> ItemCollection:
    method get_all_items_as_dict (line 946) | def get_all_items_as_dict(self) -> dict[str, Any]:

FILE: pystac_client/mixins.py
  class StacAPIObject (line 16) | class StacAPIObject(pystac.STACObject):
    method conforms_to (line 19) | def conforms_to(self, conformance_class: str | ConformanceClasses) -> ...
  class BaseMixin (line 23) | class BaseMixin(StacAPIObject):
    method _get_href (line 24) | def _get_href(self, rel: str, link: pystac.Link | None, endpoint: str)...
  class QueryablesMixin (line 33) | class QueryablesMixin(BaseMixin):
    method get_queryables_from (line 36) | def get_queryables_from(self, url: str) -> dict[str, Any]:
    method get_queryables (line 60) | def get_queryables(self) -> dict[str, Any]:
    method _get_queryables_href (line 64) | def _get_queryables_href(self) -> str:

FILE: pystac_client/stac_api_io.py
  class StacApiIO (line 41) | class StacApiIO(DefaultStacIO):
    method __init__ (line 42) | def __init__(
    method update (line 101) | def update(
    method read_text (line 127) | def read_text(self, source: pystac.link.HREF, *args: Any, **kwargs: An...
    method request (line 173) | def request(
    method write_text_to_href (line 225) | def write_text_to_href(self, href: str, *args: Any, **kwargs: Any) -> ...
    method stac_object_from_dict (line 231) | def stac_object_from_dict(
    method get_pages (line 292) | def get_pages(
  function _is_url (line 325) | def _is_url(href: str) -> bool:

FILE: pystac_client/warnings.py
  class PystacClientWarning (line 6) | class PystacClientWarning(UserWarning):
  class NoConformsTo (line 12) | class NoConformsTo(PystacClientWarning):
    method __str__ (line 15) | def __str__(self) -> str:
  class DoesNotConformTo (line 19) | class DoesNotConformTo(PystacClientWarning):
    method __str__ (line 22) | def __str__(self) -> str:
  class MissingLink (line 26) | class MissingLink(PystacClientWarning):
    method __str__ (line 29) | def __str__(self) -> str:
  class FallbackToPystac (line 33) | class FallbackToPystac(PystacClientWarning):
    method __str__ (line 36) | def __str__(self) -> str:
  function strict (line 41) | def strict() -> Iterator[None]:
  function ignore (line 71) | def ignore() -> Iterator[None]:

FILE: tests/helpers.py
  function read_data_file (line 15) | def read_data_file(file_name: str, mode: str = "r", parse_json: bool = F...

FILE: tests/test_base_search.py
  class TestBaseSearchParams (line 31) | class TestBaseSearchParams:
    method sample_client (line 33) | def sample_client(self) -> Client:
    method test_tuple_bbox (line 37) | def test_tuple_bbox(self) -> None:
    method test_list_bbox (line 42) | def test_list_bbox(self) -> None:
    method test_string_bbox (line 47) | def test_string_bbox(self) -> None:
    method test_generator_bbox (line 52) | def test_generator_bbox(self) -> None:
    method test_url_with_parameters (line 60) | def test_url_with_parameters(self) -> None:
    method test_single_string_datetime (line 81) | def test_single_string_datetime(self) -> None:
    method test_range_string_datetime (line 86) | def test_range_string_datetime(self) -> None:
    method test_list_of_strings_datetime (line 96) | def test_list_of_strings_datetime(self) -> None:
    method test_open_range_string_datetime (line 106) | def test_open_range_string_datetime(self) -> None:
    method test_single_datetime_object (line 111) | def test_single_datetime_object(self) -> None:
    method test_list_of_datetimes (line 118) | def test_list_of_datetimes(self) -> None:
    method test_open_list_of_datetimes (line 129) | def test_open_list_of_datetimes(self) -> None:
    method test_localized_datetime_converted_to_utc (line 136) | def test_localized_datetime_converted_to_utc(self) -> None:
    method test_single_year (line 142) | def test_single_year(self) -> None:
    method test_range_of_years (line 149) | def test_range_of_years(self) -> None:
    method test_single_month (line 156) | def test_single_month(self) -> None:
    method test_range_of_months (line 163) | def test_range_of_months(self) -> None:
    method test_single_date (line 170) | def test_single_date(self) -> None:
    method test_range_of_dates (line 177) | def test_range_of_dates(self) -> None:
    method test_mixed_simple_date_strings (line 184) | def test_mixed_simple_date_strings(self) -> None:
    method test_time (line 191) | def test_time(self) -> None:
    method test_many_datetimes (line 200) | def test_many_datetimes(self) -> None:
    method test_three_datetimes (line 230) | def test_three_datetimes(self) -> None:
    method test_double_open_ended_interval (line 238) | def test_double_open_ended_interval(self) -> None:
    method test_datetime_list_of_one_none (line 242) | def test_datetime_list_of_one_none(self) -> None:
    method test_poorly_formed_datetimes (line 246) | def test_poorly_formed_datetimes(self) -> None:
    method test_single_collection_string (line 250) | def test_single_collection_string(self) -> None:
    method test_multiple_collection_string (line 255) | def test_multiple_collection_string(self) -> None:
    method test_list_of_collection_strings (line 260) | def test_list_of_collection_strings(self) -> None:
    method test_generator_of_collection_strings (line 265) | def test_generator_of_collection_strings(self) -> None:
    method test_single_id_string (line 273) | def test_single_id_string(self) -> None:
    method test_multiple_id_string (line 280) | def test_multiple_id_string(self) -> None:
    method test_list_of_id_strings (line 291) | def test_list_of_id_strings(self) -> None:
    method test_generator_of_id_string (line 305) | def test_generator_of_id_string(self) -> None:
    method test_intersects_dict (line 319) | def test_intersects_dict(self) -> None:
    method test_intersects_json_string (line 324) | def test_intersects_json_string(self) -> None:
    method test_intersects_non_geo_interface_object (line 329) | def test_intersects_non_geo_interface_object(self) -> None:
    method test_filter_lang_default_for_method_despite_filter_as_dict (line 333) | def test_filter_lang_default_for_method_despite_filter_as_dict(self) -...
    method test_filter_lang_default_for_method_despite_filter_as_str (line 337) | def test_filter_lang_default_for_method_despite_filter_as_str(self) ->...
    method test_filter_lang_cql2_text (line 341) | def test_filter_lang_cql2_text(self) -> None:
    method test_filter_lang_cql2_json (line 346) | def test_filter_lang_cql2_json(self) -> None:
    method test_filter_lang_without_filter (line 351) | def test_filter_lang_without_filter(self) -> None:
    method test_filter_conversion_to_cql2_json (line 356) | def test_filter_conversion_to_cql2_json(self) -> None:
    method test_filter_conversion_to_cql2_text (line 364) | def test_filter_conversion_to_cql2_text(self) -> None:
    method test_filter_conversion_does_not_happen_if_filter_lang_specified_json (line 373) | def test_filter_conversion_does_not_happen_if_filter_lang_specified_json(
    method test_filter_conversion_does_not_happen_if_filter_lang_specified_text (line 388) | def test_filter_conversion_does_not_happen_if_filter_lang_specified_text(
    method test_sortby (line 401) | def test_sortby(self) -> None:
    method test_fields (line 471) | def test_fields(self) -> None:

FILE: tests/test_cli.py
  class TestCLISearch (line 15) | class TestCLISearch:
    method test_item_search (line 17) | def test_item_search(self, script_runner: ScriptRunner) -> None:
    method test_filter (line 31) | def test_filter(self, script_runner: ScriptRunner) -> None:
    method test_intersects (line 47) | def test_intersects(self, script_runner: ScriptRunner, filename: str) ...
    method test_intersects_despite_warning (line 64) | def test_intersects_despite_warning(self, script_runner: ScriptRunner)...
    method test_headers (line 92) | def test_headers(self, headers: list[str], good_header_count: int) -> ...
    method test_no_arguments (line 105) | def test_no_arguments(self, script_runner: ScriptRunner) -> None:
    method test_non_conformant_raises_by_default (line 112) | def test_non_conformant_raises_by_default(
    method test_non_conformant_raises_if_warning_set_to_error (line 130) | def test_non_conformant_raises_if_warning_set_to_error(
    method test_non_conformant_can_be_fixed (line 148) | def test_non_conformant_can_be_fixed(self, script_runner: ScriptRunner...
    method test_non_conformant_can_be_ignored (line 163) | def test_non_conformant_can_be_ignored(
    method test_altering_conforms_to (line 183) | def test_altering_conforms_to(
    method test_matched_not_available (line 199) | def test_matched_not_available(self, script_runner: ScriptRunner) -> N...
    method test_matched (line 214) | def test_matched(self, script_runner: ScriptRunner) -> None:
    method test_fields (line 230) | def test_fields(self, script_runner: ScriptRunner) -> None:
    method test_save (line 247) | def test_save(self, script_runner: ScriptRunner) -> None:
  class TestCLICollections (line 272) | class TestCLICollections:
    method test_collections (line 274) | def test_collections(self, script_runner: ScriptRunner) -> None:
    method test_collection_search (line 285) | def test_collection_search(self, script_runner: ScriptRunner) -> None:
    method test_save (line 302) | def test_save(self, script_runner: ScriptRunner) -> None:

FILE: tests/test_client.py
  class TestAPI (line 32) | class TestAPI:
    method test_instance (line 34) | def test_instance(self) -> None:
    method test_links (line 43) | def test_links(self) -> None:
    method test_from_file (line 59) | def test_from_file(self) -> None:
    method test_invalid_url (line 64) | def test_invalid_url(self) -> None:
    method test_get_collections_with_conformance (line 68) | def test_get_collections_with_conformance(self, requests_mock: Mocker)...
    method test_get_collections_single_slash (line 98) | def test_get_collections_single_slash(self, requests_mock: Mocker) -> ...
    method test_keep_trailing_slash_on_root (line 118) | def test_keep_trailing_slash_on_root(self, requests_mock: Mocker) -> N...
    method test_fall_back_to_data_link_for_collections (line 127) | def test_fall_back_to_data_link_for_collections(
    method test_build_absolute_href_from_data_link (line 152) | def test_build_absolute_href_from_data_link(self, requests_mock: Mocke...
    method test_error_if_no_self_href_or_data_link (line 185) | def test_error_if_no_self_href_or_data_link(self, requests_mock: Mocke...
    method test_custom_request_parameters (line 197) | def test_custom_request_parameters(self, requests_mock: Mocker) -> None:
    method test_custom_query_params_get_collections_propagation (line 243) | def test_custom_query_params_get_collections_propagation(
    method test_custom_query_params_get_collection_propagation (line 307) | def test_custom_query_params_get_collection_propagation(
    method test_get_collections_without_conformance_fallsback_to_pystac (line 370) | def test_get_collections_without_conformance_fallsback_to_pystac(
    method test_opening_a_collection (line 415) | def test_opening_a_collection(self) -> None:
    method test_headers_with_custom_stac_io (line 420) | def test_headers_with_custom_stac_io(self, requests_mock: Mocker) -> N...
  class TestAPISearch (line 434) | class TestAPISearch:
    method api (line 436) | def api(self) -> Client:
    method test_search_conformance_error (line 439) | def test_search_conformance_error(self, api: Client) -> None:
    method test_no_search_link (line 447) | def test_no_search_link(self, api: Client) -> None:
    method test_no_conforms_to (line 458) | def test_no_conforms_to(self) -> None:
    method test_search (line 472) | def test_search(self, api: Client) -> None:
    method test_json_search_link (line 488) | def test_json_search_link(self, api: Client) -> None:
    method test_search_max_items_unlimited_default (line 497) | def test_search_max_items_unlimited_default(self, api: Client) -> None:
  class TestAPICollectionSearch (line 507) | class TestAPICollectionSearch:
    method api (line 509) | def api(self) -> Client:
    method test_search_conformance_error (line 512) | def test_search_conformance_error(self, api: Client) -> None:
    method test_search_conformance_warning (line 521) | def test_search_conformance_warning(self) -> None:
    method test_search (line 532) | def test_search(self, api: Client) -> None:
  class MySign (line 546) | class MySign:
    method __init__ (line 547) | def __init__(self) -> None:
    method __call__ (line 550) | def __call__(self, x: Modifiable) -> None:
  class TestSigning (line 554) | class TestSigning:
    method test_signing (line 556) | def test_signing(self) -> None:
    method test_sign_with_return_warns (line 594) | def test_sign_with_return_warns(self) -> None:
  class TestQueryables (line 611) | class TestQueryables:
    method test_get_queryables_collections (line 613) | def test_get_queryables_collections(self) -> None:
    method test_get_queryables_errors (line 628) | def test_get_queryables_errors(self, requests_mock: Mocker) -> None:
  class TestConformsTo (line 651) | class TestConformsTo:
    method test_ignore_conformance_is_deprecated_and_noop (line 652) | def test_ignore_conformance_is_deprecated_and_noop(self) -> None:
    method test_set_conforms_to_using_list_of_uris (line 663) | def test_set_conforms_to_using_list_of_uris(self) -> None:
    method test_add_and_remove_conforms_to_by_string (line 669) | def test_add_and_remove_conforms_to_by_string(self) -> None:
    method test_clear_all_conforms_to (line 678) | def test_clear_all_conforms_to(self) -> None:
    method test_empty_conforms_to (line 683) | def test_empty_conforms_to(self) -> None:
    method test_no_conforms_to_falls_back_to_pystac (line 691) | def test_no_conforms_to_falls_back_to_pystac(self) -> None:
    method test_changing_conforms_to_changes_behavior (line 700) | def test_changing_conforms_to_changes_behavior(self) -> None:
  function test_collections_are_clients (line 714) | def test_collections_are_clients() -> None:
  function test_get_items_without_ids (line 727) | def test_get_items_without_ids() -> None:
  function test_non_recursion_on_fallback (line 735) | def test_non_recursion_on_fallback() -> None:
  function test_fallback_strategy (line 743) | def test_fallback_strategy() -> None:
  function test_get_collection_fallback_strategy_for_static_catalogs (line 784) | def test_get_collection_fallback_strategy_for_static_catalogs() -> None:
  function test_get_collection_for_static_catalogs_returns_none_if_not_found (line 801) | def test_get_collection_for_static_catalogs_returns_none_if_not_found() ...
  function test_get_collection_raises_if_collection_id_is_empty (line 808) | def test_get_collection_raises_if_collection_id_is_empty() -> None:
  function test_get_collection_returns_none_if_not_found (line 818) | def test_get_collection_returns_none_if_not_found() -> None:
  function test_query_string_in_collections_url (line 825) | def test_query_string_in_collections_url() -> None:

FILE: tests/test_collection_client.py
  class TestCollectionClient (line 10) | class TestCollectionClient:
    method test_instance (line 12) | def test_instance(self) -> None:
    method test_get_items (line 20) | def test_get_items(self) -> None:
    method test_get_items_with_ids (line 29) | def test_get_items_with_ids(self) -> None:
    method test_get_item (line 43) | def test_get_item(self) -> None:
    method test_get_item_with_item_search (line 55) | def test_get_item_with_item_search(self) -> None:
    method test_get_queryables (line 81) | def test_get_queryables(self) -> None:

FILE: tests/test_collection_search.py
  class TestCollectionPerformance (line 32) | class TestCollectionPerformance:
    method single_href (line 34) | def single_href(self) -> str:
    method test_requests (line 38) | def test_requests(self, benchmark: BenchmarkFixture, single_href: str)...
    method test_single_collection (line 44) | def test_single_collection(
    method test_single_collection_search (line 51) | def test_single_collection_search(
  class TestCollectionSearch (line 62) | class TestCollectionSearch:
    method fedeo_api (line 64) | def fedeo_api(self) -> Client:
    method test_method (line 68) | def test_method(self) -> None:
    method test_method_params (line 73) | def test_method_params(self) -> None:
    method test_q_results (line 102) | def test_q_results(self) -> None:
    method test_datetime_results (line 120) | def test_datetime_results(self) -> None:
    method test_bbox_results (line 159) | def test_bbox_results(self) -> None:
    method test_result_paging (line 175) | def test_result_paging(self) -> None:
    method test_matched (line 192) | def test_matched(self) -> None:
    method test_enabled_but_client_side_q (line 205) | def test_enabled_but_client_side_q(self) -> None:
    method test_client_side_q (line 236) | def test_client_side_q(self) -> None:
    method test_client_side_bbox (line 265) | def test_client_side_bbox(self) -> None:
    method test_client_side_datetime (line 287) | def test_client_side_datetime(self) -> None:
    method test_client_side_extra_args (line 315) | def test_client_side_extra_args(self) -> None:
    method test_result_paging_max_collections (line 325) | def test_result_paging_max_collections(self) -> None:
  function test_bboxes_overlap (line 345) | def test_bboxes_overlap() -> None:
  function test_temporal_intervals_overlap (line 357) | def test_temporal_intervals_overlap() -> None:
  function test_invalid_collection (line 395) | def test_invalid_collection() -> None:

FILE: tests/test_conformance.py
  class TestConformanceClasses (line 6) | class TestConformanceClasses:
    method test_get_by_name_raises_for_invalid_names (line 7) | def test_get_by_name_raises_for_invalid_names(self) -> None:
    method test_get_by_name_valid (line 18) | def test_get_by_name_valid(self) -> None:
    method test_valid_uri_property (line 23) | def test_valid_uri_property(self) -> None:
    method test_pattern_property (line 29) | def test_pattern_property(self) -> None:

FILE: tests/test_free_text.py
  function test_sqlite_single_term (line 6) | def test_sqlite_single_term() -> None:
  function test_sqlite_special_characters (line 12) | def test_sqlite_special_characters() -> None:
  function test_sqlite_exact_phrase (line 26) | def test_sqlite_exact_phrase() -> None:
  function test_sqlite_and_terms_default (line 41) | def test_sqlite_and_terms_default() -> None:
  function test_sqlite_or_terms_explicit (line 57) | def test_sqlite_or_terms_explicit() -> None:
  function test_sqlite_or_terms_commas (line 64) | def test_sqlite_or_terms_commas() -> None:
  function test_sqlite_and_terms (line 74) | def test_sqlite_and_terms() -> None:
  function test_sqlite_parentheses_grouping (line 83) | def test_sqlite_parentheses_grouping() -> None:
  function test_sqlite_inclusions_exclusions (line 96) | def test_sqlite_inclusions_exclusions() -> None:
  function test_sqlite_partial_match (line 104) | def test_sqlite_partial_match() -> None:

FILE: tests/test_item_search.py
  class TestItemPerformance (line 35) | class TestItemPerformance:
    method single_href (line 37) | def single_href(self) -> str:
    method test_requests (line 44) | def test_requests(self, benchmark: BenchmarkFixture, single_href: str)...
    method test_single_item (line 50) | def test_single_item(self, benchmark: BenchmarkFixture, single_href: s...
    method test_single_item_search (line 55) | def test_single_item_search(
  class TestItemSearch (line 66) | class TestItemSearch:
    method astraea_api (line 68) | def astraea_api(self) -> Client:
    method test_method (line 72) | def test_method(self) -> None:
    method test_method_params (line 81) | def test_method_params(self) -> None:
    method test_results (line 103) | def test_results(self) -> None:
    method test_ids_results (line 115) | def test_ids_results(self) -> None:
    method test_datetime_results (line 131) | def test_datetime_results(self) -> None:
    method test_intersects_results (line 160) | def test_intersects_results(self) -> None:
    method test_get_with_query (line 193) | def test_get_with_query(self, requests_mock: Mocker) -> None:
    method test_result_paging (line 211) | def test_result_paging(self) -> None:
    method test_result_paging_max_items (line 228) | def test_result_paging_max_items(self) -> None:
    method test_item_collection (line 244) | def test_item_collection(self) -> None:
    method test_deprecations (line 267) | def test_deprecations(
    method test_items_as_dicts (line 297) | def test_items_as_dicts(self) -> None:
  class TestItemSearchQuery (line 308) | class TestItemSearchQuery:
    method test_query_shortcut_syntax (line 310) | def test_query_shortcut_syntax(self) -> None:
    method test_query_json_syntax (line 334) | def test_query_json_syntax(self) -> None:
  function test_query_json_syntax (line 360) | def test_query_json_syntax() -> None:
  function test_url_with_query_parameter (line 376) | def test_url_with_query_parameter() -> None:
  function test_multiple_collections (line 387) | def test_multiple_collections() -> None:
  function test_naive_datetime (line 398) | def test_naive_datetime() -> None:
  function test_fields (line 408) | def test_fields() -> None:
  function test_feature (line 422) | def test_feature() -> None:

FILE: tests/test_stac_api_io.py
  class TestSTAC_IOOverride (line 16) | class TestSTAC_IOOverride:
    method test_request_input (line 18) | def test_request_input(self) -> None:
    method test_str_input (line 24) | def test_str_input(self) -> None:
    method test_http_error (line 31) | def test_http_error(self) -> None:
    method test_local_file (line 39) | def test_local_file(self, tmp_path: Path) -> None:
    method test_conformance_deprecated (line 49) | def test_conformance_deprecated(self) -> None:
    method test_custom_headers (line 54) | def test_custom_headers(self, requests_mock: Mocker) -> None:
    method test_modifier (line 70) | def test_modifier(self, requests_mock: Mocker) -> None:
    method test_modifier_noreturn (line 93) | def test_modifier_noreturn(self, requests_mock: Mocker) -> None:
    method test_custom_query_params (line 116) | def test_custom_query_params(self, requests_mock: Mocker) -> None:
    method test_write (line 145) | def test_write(self, tmp_path: Path) -> None:
    method test_stop_on_empty_page (line 157) | def test_stop_on_empty_page(
    method test_stop_on_attributeless_page (line 200) | def test_stop_on_attributeless_page(
    method test_stop_on_first_empty_page (line 242) | def test_stop_on_first_empty_page(
    method test_timeout_smoke_test (line 265) | def test_timeout_smoke_test(self) -> None:
    method test_respect_env_for_certs (line 273) | def test_respect_env_for_certs(self, monkeypatch: MonkeyPatch, name: s...
  function test_stac_io_in_pystac (line 281) | def test_stac_io_in_pystac() -> None:

FILE: tests/test_warnings.py
  class TestWarningContextManagers (line 9) | class TestWarningContextManagers:
    method test_ignore_context_manager (line 10) | def test_ignore_context_manager(self) -> None:
    method test_strict_context_manager (line 18) | def test_strict_context_manager(self) -> None:
    method test_ignore_context_manager_cleanup (line 27) | def test_ignore_context_manager_cleanup(self) -> None:
Copy disabled (too large) Download .json
Condensed preview — 168 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (24,117K chars).
[
  {
    "path": ".adr-dir",
    "chars": 19,
    "preview": "docs/source/design\n"
  },
  {
    "path": ".codespellignore",
    "chars": 8,
    "preview": "filetest"
  },
  {
    "path": ".gitattributes",
    "chars": 50,
    "preview": "tests/cassettes/**/*.yaml linguist-generated=true\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 343,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: \"/\"\n    schedule:\n      interval: weekly\n  - pa"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 149,
    "preview": "**Related Issue(s):** \n\n- #\n\n\n**Description:**\n\n\n**PR Checklist:**\n\n- [ ] Code is formatted\n- [ ] Tests pass\n- [ ] Chang"
  },
  {
    "path": ".github/workflows/continuous-integration.yml",
    "chars": 3133,
    "preview": "name: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\nconcurrency:\n  group: ${{ github.ref }}\n  cancel-in-pr"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 753,
    "preview": "name: Release\n\non:\n  push:\n    tags:\n      - \"*\"\n\njobs:\n  release:\n    name: release\n    runs-on: ubuntu-latest\n    step"
  },
  {
    "path": ".gitignore",
    "chars": 10333,
    "preview": ".python-version\n**/*.private*\n\n# Ignore everything JetBrains\n.idea\n\n# Created by .ignore support plugin (hsz.mobi)\n### V"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 995,
    "preview": "# Configuration file for pre-commit (https://pre-commit.com/).\n# Please run `pre-commit run --all-files` when adding or "
  },
  {
    "path": ".readthedocs.yml",
    "chars": 472,
    "preview": "version: 2\n\nbuild:\n  os: ubuntu-24.04\n  tools:\n    python: \"3.12\"\n  jobs:\n    pre_create_environment:\n      - asdf plugi"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 25032,
    "preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
  },
  {
    "path": "LICENSE",
    "chars": 553,
    "preview": "Copyright 2021 Jon Duckworth\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file "
  },
  {
    "path": "MANIFEST.in",
    "chars": 23,
    "preview": "global-include *.typed\n"
  },
  {
    "path": "README.md",
    "chars": 1483,
    "preview": "# pystac-client\n\n[![CI](https://github.com/stac-utils/pystac-client/actions/workflows/continuous-integration.yml/badge.s"
  },
  {
    "path": "RELEASING.md",
    "chars": 1428,
    "preview": "# Releasing\n\n1. Determine the next version.\n   We follow [semantic versioning](https://semver.org/).\n2. Create a release"
  },
  {
    "path": "docs/_static/custom.css",
    "chars": 246,
    "preview": ".no-sidebar {\n    display: none;\n}\n\n.card-text a {\n    font-weight: 600;\n    color: rgb(255, 26, 26);\n}\n\n.card-header p "
  },
  {
    "path": "docs/api.rst",
    "chars": 2044,
    "preview": "API Reference\n=============\n\nThis section is autogenerated from in-line code documentation. It is mostly useful as a\nref"
  },
  {
    "path": "docs/conf.py",
    "chars": 4207,
    "preview": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common op"
  },
  {
    "path": "docs/contributing.rst",
    "chars": 7415,
    "preview": "Contributing\n============\n\nA list of issues and ongoing work is available on the PySTAC Client `issues page\n<https://git"
  },
  {
    "path": "docs/design/0001-record-architecture-decisions.md",
    "chars": 485,
    "preview": "# 1. Record architecture decisions\n\nDate: 2021-03-01\n\n## Status\n\nAccepted\n\n## Context\n\nWe need to record the architectur"
  },
  {
    "path": "docs/design/0002-choose-stac-library.md",
    "chars": 3103,
    "preview": "# 2. Choose STAC library\n\nDate: 2021-03-01\n\n## Status\n\nAccepted\n\n## Context\n\nWe would like to use an existing Python lib"
  },
  {
    "path": "docs/design/design_decisions.rst",
    "chars": 351,
    "preview": "Design Decisions\n================\n\n`Architectural Design Records (ADRs)\n<https://cognitect.com/blog/2011/11/15/documenti"
  },
  {
    "path": "docs/index.rst",
    "chars": 2372,
    "preview": "PySTAC Client Documentation\n===========================\n\nPySTAC Client is a Python package for working with `STAC <https"
  },
  {
    "path": "docs/quickstart.rst",
    "chars": 6821,
    "preview": "Quickstart\n----------\n\nPySTAC Client can be used as either a Command Line Interface (CLI) or a\nPython library.\n\nCLI\n~~~\n"
  },
  {
    "path": "docs/tutorials/aoi-coverage.ipynb",
    "chars": 4421,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"e06a27bf\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Calculating "
  },
  {
    "path": "docs/tutorials/authentication.md",
    "chars": 2999,
    "preview": "# Authentication\n\nWhile not integrated into this library directly, pystac-client provides a series of hooks that support"
  },
  {
    "path": "docs/tutorials/cql2-filter.ipynb",
    "chars": 10012,
    "preview": "{\n \"cells\": [\n  {\n   \"attachments\": {},\n   \"cell_type\": \"markdown\",\n   \"id\": \"e06a27bf\",\n   \"metadata\": {},\n   \"source\":"
  },
  {
    "path": "docs/tutorials/item-search-intersects.ipynb",
    "chars": 6788,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"e06a27bf\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Item Search "
  },
  {
    "path": "docs/tutorials/pystac-client-introduction.ipynb",
    "chars": 8306,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"e06a27bf\",\n   \"metadata\": {},\n   \"source\": [\n    \"# PySTAC-Clien"
  },
  {
    "path": "docs/tutorials/stac-metadata-viz.ipynb",
    "chars": 7499,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"ab31574b\",\n   \"metadata\": {},\n   \"source\": [\n    \"# STAC Metadat"
  },
  {
    "path": "docs/tutorials.rst",
    "chars": 1900,
    "preview": ".. _tutorials:\n\nTutorials\n#########\n\nPySTAC-Client Introduction\n--------------------------\n\n- :tutorial:`GitHub version "
  },
  {
    "path": "docs/usage.rst",
    "chars": 26183,
    "preview": "Usage\n#####\n\nPySTAC-Client (pystac-client) builds upon\n`PySTAC <https://github.com/stac-utils/pystac>`_ library to add s"
  },
  {
    "path": "pyproject.toml",
    "chars": 3496,
    "preview": "[project]\nname = \"pystac-client\"\ndescription = \"Python library for searching SpatioTemporal Asset Catalog (STAC) APIs.\"\n"
  },
  {
    "path": "pystac_client/__init__.py",
    "chars": 516,
    "preview": "__all__ = [\n    \"Client\",\n    \"CollectionClient\",\n    \"CollectionSearch\",\n    \"ConformanceClasses\",\n    \"ItemSearch\",\n  "
  },
  {
    "path": "pystac_client/_utils.py",
    "chars": 1152,
    "preview": "import urllib\nimport warnings\nfrom collections.abc import Callable\nfrom typing import Any, Union\n\nimport pystac\n\nfrom py"
  },
  {
    "path": "pystac_client/cli.py",
    "chars": 13923,
    "preview": "import argparse\nimport json\nimport logging\nimport os\nimport re\nimport sys\nimport warnings\nfrom typing import Any\n\nfrom p"
  },
  {
    "path": "pystac_client/client.py",
    "chars": 34036,
    "preview": "import re\nimport warnings\nfrom collections.abc import Callable, Iterator\nfrom functools import lru_cache\nfrom typing imp"
  },
  {
    "path": "pystac_client/collection_client.py",
    "chars": 7747,
    "preview": "from __future__ import annotations\n\nimport warnings\nfrom collections.abc import Callable, Iterator\nfrom typing import (\n"
  },
  {
    "path": "pystac_client/collection_search.py",
    "chars": 21574,
    "preview": "import warnings\nfrom collections.abc import Callable, Iterator\nfrom datetime import datetime, timezone\nfrom functools im"
  },
  {
    "path": "pystac_client/conformance.py",
    "chars": 1459,
    "preview": "from __future__ import annotations\n\nimport re\nfrom enum import Enum\n\n\nclass ConformanceClasses(Enum):\n    \"\"\"Enumeration"
  },
  {
    "path": "pystac_client/errors.py",
    "chars": 246,
    "preview": "class ClientTypeError(Exception):\n    \"\"\"Raised when trying to open a Client on a non-catalog STAC Object.\"\"\"\n\n    pass\n"
  },
  {
    "path": "pystac_client/exceptions.py",
    "chars": 417,
    "preview": "from requests import Response\n\n\nclass APIError(Exception):\n    \"\"\"Raised when unexpected server error.\"\"\"\n\n    status_co"
  },
  {
    "path": "pystac_client/free_text.py",
    "chars": 2374,
    "preview": "\"\"\"Client-side free-text search filtering as described in `OGC API - Features - Part 9:\nText Search <https://docs.ogc.or"
  },
  {
    "path": "pystac_client/item_search.py",
    "chars": 35607,
    "preview": "import json\nimport re\nimport warnings\nfrom abc import ABC\nfrom collections.abc import Callable, Iterable, Iterator, Mapp"
  },
  {
    "path": "pystac_client/mixins.py",
    "chars": 2214,
    "preview": "import warnings\nfrom typing import Any\n\nimport pystac\n\nfrom pystac_client._utils import urljoin\nfrom pystac_client.confo"
  },
  {
    "path": "pystac_client/py.typed",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "pystac_client/stac_api_io.py",
    "chars": 12691,
    "preview": "import json\nimport logging\nimport warnings\nfrom collections.abc import Callable, Iterator\nfrom copy import deepcopy\nfrom"
  },
  {
    "path": "pystac_client/version.py",
    "chars": 22,
    "preview": "__version__ = \"0.9.0\"\n"
  },
  {
    "path": "pystac_client/warnings.py",
    "chars": 2778,
    "preview": "import warnings\nfrom collections.abc import Iterator\nfrom contextlib import contextmanager\n\n\nclass PystacClientWarning(U"
  },
  {
    "path": "scripts/build-docs",
    "chars": 320,
    "preview": "#!/bin/bash\n\nset -e\n\nif [[ -n \"${CI}\" ]]; then\n    set -x\nfi\n\nfunction usage() {\n    echo -n \\\n        \"Usage: $(basenam"
  },
  {
    "path": "scripts/format",
    "chars": 314,
    "preview": "#!/bin/bash\n\nset -e\n\nif [[ -n \"${CI}\" ]]; then\n    set -x\nfi\n\nfunction usage() {\n    echo -n \\\n        \"Usage: $(basenam"
  },
  {
    "path": "scripts/lint",
    "chars": 435,
    "preview": "#!/bin/bash\n\nset -e\n\nif [[ -n \"${CI}\" ]]; then\n    set -x\nfi\n\nfunction usage() {\n    echo -n \\\n        \"Usage: $(basenam"
  },
  {
    "path": "scripts/test",
    "chars": 459,
    "preview": "#!/bin/bash\n\nset -e\n\nif [[ -n \"${CI}\" ]]; then\n    set -x\nfi\n\nfunction usage() {\n    echo -n \\\n        \"Usage: $(basenam"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/cassettes/test_cli/TestCLICollections.test_collection_search[inprocess].yaml",
    "chars": 100980,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLICollections.test_collections[inprocess].yaml",
    "chars": 100969,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLICollections.test_save[inprocess].yaml",
    "chars": 100969,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_altering_conforms_to[inprocess---clear-conforms-to].yaml",
    "chars": 4669,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_altering_conforms_to[inprocess---remove-conforms-to=ITEM_SEARCH].yaml",
    "chars": 4669,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_fields[inprocess].yaml",
    "chars": 20337,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_filter[inprocess].yaml",
    "chars": 340971,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_intersects[inprocess-netherlands_aoi.json].yaml",
    "chars": 3864579,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_intersects[inprocess-sample-item.json].yaml",
    "chars": 3870080,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_intersects_despite_warning[inprocess].yaml",
    "chars": 2546985,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_item_search[inprocess].yaml",
    "chars": 802007,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_matched[inprocess].yaml",
    "chars": 9508,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_matched_not_available[inprocess].yaml",
    "chars": 59186,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_non_conformant_can_be_fixed[inprocess].yaml",
    "chars": 14415,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_non_conformant_can_be_ignored[inprocess---ignore=no-conforms-to].yaml",
    "chars": 14415,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_non_conformant_can_be_ignored[inprocess---ignore].yaml",
    "chars": 14415,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_non_conformant_raises_by_default[inprocess].yaml",
    "chars": 2339,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_non_conformant_raises_if_warning_set_to_error[inprocess---error=no-conforms-to].yaml",
    "chars": 2339,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_non_conformant_raises_if_warning_set_to_error[inprocess---error].yaml",
    "chars": 2339,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_cli/TestCLISearch.test_save[inprocess].yaml",
    "chars": 35114,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/TestAPI.test_collections_fallback.yaml",
    "chars": 66997,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/TestAPI.test_environment_variable.yaml",
    "chars": 2255,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/TestAPI.test_from_file.yaml",
    "chars": 27179,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/TestAPI.test_instance.yaml",
    "chars": 27179,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/TestAPI.test_links.yaml",
    "chars": 1530073,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/TestAPICollectionSearch.test_search.yaml",
    "chars": 8265,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/TestAPISearch.test_search_max_items_unlimited_default.yaml",
    "chars": 473600,
    "preview": "interactions:\n- request:\n    body: '{\"bbox\": [-73.21, 43.99, -73.12, 45.05], \"datetime\": \"2014-01-01T00:00:00Z/2020-12-3"
  },
  {
    "path": "tests/cassettes/test_client/TestConformsTo.test_changing_conforms_to_changes_behavior.yaml",
    "chars": 30065,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/TestQueryables.test_get_queryables.yaml",
    "chars": 8574,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/TestQueryables.test_get_queryables_collections.yaml",
    "chars": 98174,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/TestSigning.test_sign_with_return_warns.yaml",
    "chars": 132157,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/test_collections_are_clients.yaml",
    "chars": 144507,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/test_fallback_strategy.yaml",
    "chars": 248628,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/test_get_collection_returns_none_if_not_found.yaml",
    "chars": 5964,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/test_get_items_without_ids.yaml",
    "chars": 61498,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/test_non_recursion_on_fallback.yaml",
    "chars": 54146,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_client/test_query_string_in_collections_url.yaml",
    "chars": 72673,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_client/TestCollectionClient.test_get_item.yaml",
    "chars": 86699,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_client/TestCollectionClient.test_get_item_with_item_search.yaml",
    "chars": 213990,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_client/TestCollectionClient.test_get_items.yaml",
    "chars": 192072,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_client/TestCollectionClient.test_get_items_with_ids.yaml",
    "chars": 123927,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_client/TestCollectionClient.test_get_queryables.yaml",
    "chars": 79592,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_client/TestCollectionClient.test_instance.yaml",
    "chars": 65351,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_search/TestCollectionSearch.test_bbox_results.yaml",
    "chars": 124200,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_search/TestCollectionSearch.test_client_side_bbox.yaml",
    "chars": 192703,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_search/TestCollectionSearch.test_client_side_datetime.yaml",
    "chars": 192801,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_search/TestCollectionSearch.test_client_side_q.yaml",
    "chars": 192665,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_search/TestCollectionSearch.test_datetime_results.yaml",
    "chars": 561985,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_search/TestCollectionSearch.test_enabled_but_client_side_q.yaml",
    "chars": 184072,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_search/TestCollectionSearch.test_matched.yaml",
    "chars": 96344,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_search/TestCollectionSearch.test_q_results.yaml",
    "chars": 22001,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_search/TestCollectionSearch.test_result_paging.yaml",
    "chars": 278136,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_collection_search/TestCollectionSearch.test_result_paging_max_collections.yaml",
    "chars": 258049,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_datetime_results.yaml",
    "chars": 1072501,
    "preview": "interactions:\n- request:\n    body: '{\"datetime\": \"2019-01-01T00:00:01Z/2019-01-01T00:00:10Z\", \"collections\":\n      [\"sen"
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_deprecations[get_all_items-item_collection-False-True].yaml",
    "chars": 62714,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"]}'\n    "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_deprecations[get_all_items_as_dict-item_collection_as_dict-False-False].yaml",
    "chars": 62714,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"]}'\n    "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_deprecations[get_item_collections-pages-True-True].yaml",
    "chars": 125403,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"]}'\n    "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_deprecations[get_items-items-True-True].yaml",
    "chars": 1207323,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"]}'\n    "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_deprecations[item_collections-pages-True-True].yaml",
    "chars": 125403,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"]}'\n    "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_get_all_items.yaml",
    "chars": 8807,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"]}'\n    "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_get_all_items_deprecated.yaml",
    "chars": 8600,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"]}'\n    "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_ids_results.yaml",
    "chars": 4692,
    "preview": "interactions:\n- request:\n    body: '{\"ids\": [\"S2B_MSIL2A_20210610T115639_N0212_R066_T33XXG_20210613T185024.SAFE\",\n      "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_intersects_results.yaml",
    "chars": 248373,
    "preview": "interactions:\n- request:\n    body: '{\"collections\": [\"naip\"], \"intersects\": {\"type\": \"Polygon\", \"coordinates\":\n      [[["
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_item_collection.yaml",
    "chars": 62714,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"]}'\n    "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_items_as_dicts.yaml",
    "chars": 62714,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"]}'\n    "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_result_paging.yaml",
    "chars": 62714,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"]}'\n    "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_result_paging_max_items.yaml",
    "chars": 95827,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"collections\": [\"naip\"]}'\n    headers:\n      Accept:\n      - '*/*'\n   "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearch.test_results.yaml",
    "chars": 63791,
    "preview": "interactions:\n- request:\n    body: '{\"limit\": 10, \"collections\": [\"naip\"]}'\n    headers:\n      Accept:\n      - '*/*'\n   "
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearchParams.test_collection_object.yaml",
    "chars": 36615,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Connection:\n      - close\n      Host:\n      - planetarycomput"
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearchParams.test_mixed_collection_object_and_string.yaml",
    "chars": 36631,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Connection:\n      - close\n      Host:\n      - planetarycomput"
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearchQuery.test_query_json_syntax.yaml",
    "chars": 248243,
    "preview": "interactions:\n- request:\n    body: '{\"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"], \"query\":\n      {\"g"
  },
  {
    "path": "tests/cassettes/test_item_search/TestItemSearchQuery.test_query_shortcut_syntax.yaml",
    "chars": 72941,
    "preview": "interactions:\n- request:\n    body: '{\"bbox\": [-73.21, 43.99, -73.12, 44.05], \"collections\": [\"naip\"], \"query\":\n      {\"g"
  },
  {
    "path": "tests/cassettes/test_item_search/test_fields.yaml",
    "chars": 4494,
    "preview": "interactions:\n- request:\n    body: '{\"collections\": [\"sentinel-2-c1-l2a\"], \"intersects\": {\"type\": \"Point\",\n      \"coordi"
  },
  {
    "path": "tests/cassettes/test_item_search/test_multiple_collections.yaml",
    "chars": 52259,
    "preview": "interactions:\n- request:\n    body: '{\"datetime\": \"2023-10-08T00:00:00Z/2023-10-08T23:59:59Z\", \"collections\":\n      [\"sen"
  },
  {
    "path": "tests/cassettes/test_stac_api_io/TestSTAC_IOOverride.test_request_input.yaml",
    "chars": 27179,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_stac_api_io/TestSTAC_IOOverride.test_str_input.yaml",
    "chars": 27179,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_stac_api_io/TestSTAC_IOOverride.test_timeout_smoke_test.yaml",
    "chars": 27179,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/cassettes/test_stac_api_io/test_stac_io_in_pystac.yaml",
    "chars": 27180,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip, de"
  },
  {
    "path": "tests/data/astraea_api.json",
    "chars": 16333,
    "preview": "{\n    \"conformsTo\": [\n        \"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core\",\n        \"http://www.opengis"
  },
  {
    "path": "tests/data/fedeo_clearinghouse.json",
    "chars": 5679,
    "preview": "{\n  \"extent\": {\n    \"spatial\": {\"bbox\": [[\n      -180,\n      -90,\n      180,\n      90\n    ]]},\n    \"temporal\": {\"interva"
  },
  {
    "path": "tests/data/invalid-collection.json",
    "chars": 1124,
    "preview": "{\n    \"id\": \"planet\",\n    \"title\": \"Planet\",\n    \"extent\": {},\n    \"license\": \"proprietary\",\n    \"description\": \"Planet\""
  },
  {
    "path": "tests/data/netherlands_aoi.json",
    "chars": 459,
    "preview": "{\n    \"type\": \"Polygon\",\n    \"coordinates\": [\n      [\n        [\n          6.42425537109375,\n          53.174765470134616"
  },
  {
    "path": "tests/data/planetary-computer-aster-l1t-collection.json",
    "chars": 11129,
    "preview": "{\n  \"id\": \"aster-l1t\",\n  \"type\": \"Collection\",\n  \"links\": [\n    {\n      \"rel\": \"items\",\n      \"type\": \"application/geo+j"
  },
  {
    "path": "tests/data/planetary-computer-collection.json",
    "chars": 39438,
    "preview": "{\n  \"id\": \"gap\",\n  \"type\": \"Collection\",\n  \"links\": [\n    {\n      \"rel\": \"items\",\n      \"type\": \"application/geo+json\",\n"
  },
  {
    "path": "tests/data/planetary-computer-root.json",
    "chars": 2648,
    "preview": "{\n    \"id\": \"microsoft-pc\",\n    \"description\": \"Searchable spatiotemporal metadata describing Earth science datasets hos"
  },
  {
    "path": "tests/data/sample-item-collection.json",
    "chars": 62870,
    "preview": "{\n    \"type\": \"FeatureCollection\",\n    \"stac_version\": \"1.0.0-rc.4\",\n    \"numberMatched\": 324132,\n    \"numberReturned\": "
  },
  {
    "path": "tests/data/sample-item.json",
    "chars": 1866,
    "preview": "{\n    \"type\": \"Feature\",\n    \"stac_version\": \"1.0.0\",\n    \"id\": \"CS3-20160503_132131_05\",\n    \"properties\": {\n      \"dat"
  },
  {
    "path": "tests/data/test-case-1/catalog.json",
    "chars": 460,
    "preview": "{\n  \"type\": \"Catalog\",\n  \"id\": \"test\",\n  \"stac_version\": \"1.1.0\",\n  \"description\": \"test catalog\",\n  \"links\": [\n    {\n  "
  },
  {
    "path": "tests/data/test-case-1/country-1/area-1-1/area-1-1-imagery/area-1-1-imagery.json",
    "chars": 1346,
    "preview": "{\n  \"type\": \"Feature\",\n  \"stac_version\": \"1.1.0\",\n  \"id\": \"area-1-1-imagery\",\n  \"properties\": {\n    \"datetime\": \"2019-10"
  },
  {
    "path": "tests/data/test-case-1/country-1/area-1-1/area-1-1-labels/area-1-1-labels.json",
    "chars": 1787,
    "preview": "{\n  \"type\": \"Feature\",\n  \"stac_version\": \"1.1.0\",\n  \"id\": \"area-1-1-labels\",\n  \"properties\": {\n    \"datetime\": \"2019-10-"
  },
  {
    "path": "tests/data/test-case-1/country-1/area-1-1/collection.json",
    "chars": 955,
    "preview": "{\n  \"type\": \"Collection\",\n  \"id\": \"area-1-1\",\n  \"stac_version\": \"1.1.0\",\n  \"description\": \"test collection country-1\",\n "
  },
  {
    "path": "tests/data/test-case-1/country-1/area-1-2/area-1-2-imagery/area-1-2-imagery.json",
    "chars": 1346,
    "preview": "{\n  \"type\": \"Feature\",\n  \"stac_version\": \"1.1.0\",\n  \"id\": \"area-1-2-imagery\",\n  \"properties\": {\n    \"datetime\": \"2019-10"
  },
  {
    "path": "tests/data/test-case-1/country-1/area-1-2/area-1-2-labels/area-1-2-labels.json",
    "chars": 1787,
    "preview": "{\n  \"type\": \"Feature\",\n  \"stac_version\": \"1.1.0\",\n  \"id\": \"area-1-2-labels\",\n  \"properties\": {\n    \"datetime\": \"2019-10-"
  },
  {
    "path": "tests/data/test-case-1/country-1/area-1-2/collection.json",
    "chars": 955,
    "preview": "{\n  \"type\": \"Collection\",\n  \"id\": \"area-1-2\",\n  \"stac_version\": \"1.1.0\",\n  \"description\": \"test collection country-1\",\n "
  },
  {
    "path": "tests/data/test-case-1/country-1/catalog.json",
    "chars": 582,
    "preview": "{\n  \"type\": \"Catalog\",\n  \"id\": \"country-1\",\n  \"stac_version\": \"1.1.0\",\n  \"description\": \"test catalog country-1\",\n  \"lin"
  },
  {
    "path": "tests/data/test-case-1/country-2/area-2-1/area-2-1-imagery/area-2-1-imagery.json",
    "chars": 1346,
    "preview": "{\n  \"type\": \"Feature\",\n  \"stac_version\": \"1.1.0\",\n  \"id\": \"area-2-1-imagery\",\n  \"properties\": {\n    \"datetime\": \"2019-10"
  },
  {
    "path": "tests/data/test-case-1/country-2/area-2-1/area-2-1-labels/area-2-1-labels.json",
    "chars": 1787,
    "preview": "{\n  \"type\": \"Feature\",\n  \"stac_version\": \"1.1.0\",\n  \"id\": \"area-2-1-labels\",\n  \"properties\": {\n    \"datetime\": \"2019-10-"
  },
  {
    "path": "tests/data/test-case-1/country-2/area-2-1/collection.json",
    "chars": 955,
    "preview": "{\n  \"type\": \"Collection\",\n  \"id\": \"area-2-1\",\n  \"stac_version\": \"1.1.0\",\n  \"description\": \"test collection country-2\",\n "
  },
  {
    "path": "tests/data/test-case-1/country-2/area-2-2/area-2-2-imagery/area-2-2-imagery.json",
    "chars": 1346,
    "preview": "{\n  \"type\": \"Feature\",\n  \"stac_version\": \"1.1.0\",\n  \"id\": \"area-2-2-imagery\",\n  \"properties\": {\n    \"datetime\": \"2019-10"
  },
  {
    "path": "tests/data/test-case-1/country-2/area-2-2/area-2-2-labels/area-2-2-labels.json",
    "chars": 1787,
    "preview": "{\n  \"type\": \"Feature\",\n  \"stac_version\": \"1.1.0\",\n  \"id\": \"area-2-2-labels\",\n  \"properties\": {\n    \"datetime\": \"2019-10-"
  },
  {
    "path": "tests/data/test-case-1/country-2/area-2-2/collection.json",
    "chars": 955,
    "preview": "{\n  \"type\": \"Collection\",\n  \"id\": \"area-2-2\",\n  \"stac_version\": \"1.1.0\",\n  \"description\": \"test collection country-2\",\n "
  },
  {
    "path": "tests/data/test-case-1/country-2/catalog.json",
    "chars": 582,
    "preview": "{\n  \"type\": \"Catalog\",\n  \"id\": \"country-2\",\n  \"stac_version\": \"1.1.0\",\n  \"description\": \"test catalog country-2\",\n  \"lin"
  },
  {
    "path": "tests/helpers.py",
    "chars": 631,
    "preview": "import json\nfrom pathlib import Path\nfrom typing import Any\n\nTEST_DATA = Path(__file__).parent / \"data\"\n\nSTAC_URLS = {\n "
  },
  {
    "path": "tests/test_base_search.py",
    "chars": 19636,
    "preview": "import json\nfrom collections.abc import Iterator\nfrom datetime import datetime\nfrom typing import Any\n\nimport pytest\nfro"
  },
  {
    "path": "tests/test_cli.py",
    "chars": 10249,
    "preview": "import json\nimport tempfile\n\nimport pytest\nfrom pytest_console_scripts import ScriptRunner\n\nimport pystac_client.cli\nfro"
  },
  {
    "path": "tests/test_client.py",
    "chars": 29920,
    "preview": "import json\nimport os.path\nimport warnings\nfrom datetime import datetime\nfrom tempfile import TemporaryDirectory\nfrom ty"
  },
  {
    "path": "tests/test_collection_client.py",
    "chars": 3366,
    "preview": "import pytest\n\nfrom pystac_client import CollectionClient\nfrom pystac_client.client import Client\nfrom pystac_client.war"
  },
  {
    "path": "tests/test_collection_search.py",
    "chars": 13751,
    "preview": "import json\nfrom datetime import datetime\nfrom math import ceil\nfrom typing import Any\n\nimport pystac\nimport pytest\nimpo"
  },
  {
    "path": "tests/test_conformance.py",
    "chars": 1409,
    "preview": "import pytest\n\nfrom pystac_client.conformance import ConformanceClasses\n\n\nclass TestConformanceClasses:\n    def test_get"
  },
  {
    "path": "tests/test_free_text.py",
    "chars": 4445,
    "preview": "from pystac_client.free_text import (\n    sqlite_text_search,\n)\n\n\ndef test_sqlite_single_term() -> None:\n    query = \"se"
  },
  {
    "path": "tests/test_item_search.py",
    "chars": 13902,
    "preview": "import operator\nimport urllib.parse\nfrom datetime import datetime, timedelta\nfrom typing import Any\n\nimport pystac\nimpor"
  },
  {
    "path": "tests/test_stac_api_io.py",
    "chars": 9919,
    "preview": "import typing\nfrom pathlib import Path\nfrom urllib.parse import parse_qs, urlsplit\n\nimport pystac\nimport pytest\nfrom pyt"
  },
  {
    "path": "tests/test_warnings.py",
    "chars": 1528,
    "preview": "import pytest\n\nfrom pystac_client import Client\nfrom pystac_client.warnings import DoesNotConformTo, ignore, strict\n\nfro"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the stac-utils/pystac-client GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 168 files (45.2 MB), approximately 5.5M tokens, and a symbol index with 360 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!