Metadata-Version: 2.4
Name: stac-fastapi-catalogs-extension
Version: 0.5.0
Summary: STAC FastAPI extension for multi-tenant catalogs
Author: Jonathan Healy
License: MIT License
        
        Copyright (c) 2026 StacLabs, Jonathan Healy
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/StacLabs/stac-fastapi-catalogs-extension
Project-URL: Repository, https://github.com/StacLabs/stac-fastapi-catalogs-extension
Project-URL: Issues, https://github.com/StacLabs/stac-fastapi-catalogs-extension/issues
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: attrs>=23.1.0
Requires-Dist: fastapi>=0.100.0
Requires-Dist: pydantic>=1.10
Requires-Dist: starlette>=0.27.0
Requires-Dist: stac-fastapi.api
Requires-Dist: stac-fastapi.types
Requires-Dist: stac-pydantic
Requires-Dist: typing-extensions>=4.0.0
Provides-Extra: dev
Requires-Dist: black>=22.10.0; extra == "dev"
Requires-Dist: flake8>=6.0.0; extra == "dev"
Requires-Dist: httpx>=0.28; extra == "dev"
Requires-Dist: isort>=5.12.0; extra == "dev"
Requires-Dist: mypy>=0.991; extra == "dev"
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
Requires-Dist: pydocstyle>=6.1.1; extra == "dev"
Requires-Dist: pytest>=7.0.0; extra == "dev"
Dynamic: license-file

<!-- markdownlint-disable MD033 MD041 -->


<p align="left">
  <img src="https://raw.githubusercontent.com/stac-utils/stac-fastapi-elasticsearch-opensearch/refs/heads/main/assets/sfeos.png" width=1000>
</p>

**Jump to:** | [Table of Contents](#table-of-contents) |

  [![Downloads](https://static.pepy.tech/badge/stac-fastapi-catalogs-extension?color=blue)](https://pepy.tech/project/stac-fastapi-catalogs-extension)
   [![PyPI version](https://img.shields.io/pypi/v/stac-fastapi-catalogs-extension.svg?color=blue)](https://pypi.org/project/stac-fastapi-catalogs-extension/)
  [![STAC](https://img.shields.io/badge/STAC-1.1.0-blue.svg)](https://github.com/radiantearth/stac-spec/tree/v1.1.0)


# stac-fastapi multi-tenant virtual catalogs extension

STAC FastAPI extension for multi-tenant, recursive catalog hierarchies.

This package adds a dedicated /catalogs registry and scoped catalog routes so a
single STAC API deployment can serve multiple logical catalog trees. It is
designed for use cases where one API needs tenant-style isolation, curated
views, provider-specific trees, or thematic navigation across shared datasets.

The extension supports poly-hierarchy (multi-parenting), where a collection or
catalog can be linked under multiple catalog paths without duplicating data.
It also supports contextual navigation for scoped routes, preserving UI
breadcrumb behavior while still exposing alternative parents through related
links.

## Implementation status

| Project | Status | Notes |
| --- | --- | --- |
| [stac-fastapi-elasticsearch-opensearch (SFEOS)](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch) | Implemented | Active integration target for this extension |
| [stac-fastapi-pgstac](https://github.com/stac-utils/stac-fastapi-pgstac) | Implemented | Active integration target for this extension |
| [stac-fastapi-mongo](https://github.com/stac-utils/stac-fastapi-mongo) | Not implemented yet | Planned |

_Last verified: 2026-03-22_

## Table of contents

- [Implementation status](#implementation-status)
- [Specification reference](#specification-reference)
- [Conformance class guidance](#conformance-class-guidance)
- [What this package provides](#what-this-package-provides)
- [Supported projects](#supported-projects)
- [Install](#install)
- [Integrate in a STAC FastAPI deployment](#integrate-in-a-stac-fastapi-deployment)
- [Backend client requirements](#backend-client-requirements)
- [Notes for common deployment repos](#notes-for-common-deployment-repos)
- [Endpoints added by this extension](#endpoints-added-by-this-extension)

## Specification reference

This implementation is aligned with the Multi-Tenant Catalogs extension work in
StacLabs:

- https://github.com/StacLabs/multi-tenant-catalogs

Notable concepts adopted here include:

- Recursive /catalogs endpoint structure
- Optional transaction management plane
- Safety-first unlink semantics (organizational operations are non-destructive)
- Runtime link generation for parent/child/related relationships

## Conformance class guidance

When enabling this extension in your deployment, advertise conformance classes
according to your enabled capabilities:

- Required:
	- https://api.stacspec.org/v1.0.0/core
	- https://api.stacspec.org/v1.0.0-rc.2/multi-tenant-catalogs
- Recommended:
	- https://api.stacspec.org/v1.0.0-rc.2/children
- Optional (only if transaction endpoints are enabled):
	- https://api.stacspec.org/v1.0.0-rc.2/multi-tenant-catalogs/transaction
- Optional (only if scoped search endpoints are enabled):
	- https://api.stacspec.org/v1.0.0/item-search
	- https://api.stacspec.org/v1.0.0-rc.2/multi-tenant-catalogs/search

Operational guidance:

- Read-only/public APIs should not expose POST/PUT/DELETE catalog endpoints and
	should not advertise the transaction conformance class.
- If transactions are enabled, expose and document the management endpoints and
	include the transaction conformance class in conformance responses.
- **Important**: To preserve the poly-hierarchy DAG structure, updates to
	collections SHOULD be performed through scoped routes
	(`/catalogs/{catalogId}/collections/{collectionId}`) rather than the core STAC
	route (`/collections/{collectionId}`). The core route is flat and does not
	maintain parent-child relationships, so updates through that route may not
	properly preserve the hierarchical context.

### Multi-Tenant Security

For multi-tenant deployments where you want to prevent information leakage about
other tenants, use the `hide_alternate_parents` flag:

```python
CatalogsExtension(
    client=catalogs_client,
    hide_alternate_parents=True,  # Disables rel="related" links to alternative parents
)
```

When `hide_alternate_parents=True`, the API will only advertise the single
contextual parent through `rel="parent"` and will not expose alternative parents
via `rel="related"` links. This prevents clients from discovering the names and
structure of other tenants in the system.

## Supported projects

This extension is designed for STAC FastAPI deployment applications and is
currently supported in:

- SFEOS: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
- stac-fastapi-pgstac: https://github.com/stac-utils/stac-fastapi-pgstac

Planned (not yet implemented):

- stac-fastapi-mongo: https://github.com/stac-utils/stac-fastapi-mongo

It can also be integrated into custom STAC FastAPI deployments that implement
the AsyncBaseCatalogsClient contract.

## What this package provides

- Three STAC FastAPI extension classes:
  - `CatalogsExtension`: Read-only discovery endpoints for catalogs
  - `CatalogsTransactionExtension`: Write operations (POST, PUT, DELETE) for catalog management
  - `CatalogsSearchExtension`: Scoped search endpoints bounded to a catalog's descendant tree
- Request/response models for catalogs, children, and search APIs
- Abstract client contracts: `AsyncBaseCatalogsClient` and `AsyncCatalogsSearchClient`

This package wires routes and validation into your API. Your deployment app is
responsible for providing a concrete client implementation backed by your
database/search stack.

## Install

```bash
pip install stac-fastapi-catalogs-extension
```

## Integrate in a STAC FastAPI deployment

In your deployment app.py (for example in
stac-fastapi-elasticsearch-opensearch), instantiate StacApi with
`CatalogsExtension` and optionally `CatalogsTransactionExtension` and/or
`CatalogsSearchExtension`, passing an implementation of `AsyncBaseCatalogsClient`.

### Read-only deployment (discovery only)

```python
from stac_fastapi.api.app import StacApi
from stac_fastapi.types.config import ApiSettings

from stac_fastapi_catalogs_extension import (
    CatalogsExtension,
    CATALOGS_CORE_CONFORMANCE,
)
from my_project.catalogs_client import CatalogsClient
from my_project.core_client import CoreClient


settings = ApiSettings()

core_client = CoreClient(...)
catalogs_client = CatalogsClient(...)

api = StacApi(
    settings=settings,
    client=core_client,
    extensions=[
        CatalogsExtension(
            client=catalogs_client,
            conformance_classes=list(CATALOGS_CORE_CONFORMANCE),
            settings=settings.model_dump(),
        )
    ],
)

app = api.app
```

### With transaction support (read and write)

```python
from stac_fastapi.api.app import StacApi
from stac_fastapi.types.config import ApiSettings

from stac_fastapi_catalogs_extension import (
    CatalogsExtension,
    CatalogsTransactionExtension,
)
from my_project.catalogs_client import CatalogsClient
from my_project.core_client import CoreClient


settings = ApiSettings()

core_client = CoreClient(...)
catalogs_client = CatalogsClient(...)

api = StacApi(
    settings=settings,
    client=core_client,
    extensions=[
        CatalogsExtension(
            client=catalogs_client,
            settings=settings.model_dump(),
        ),
        CatalogsTransactionExtension(
            client=catalogs_client,
            settings=settings.model_dump(),
        ),
    ],
)

app = api.app
```

The transaction conformance class is automatically registered when
`CatalogsTransactionExtension` is included.

### With scoped search support

```python
from stac_fastapi.api.app import StacApi
from stac_fastapi.types.config import ApiSettings
from stac_fastapi.types.search import BaseSearchGetRequest, BaseSearchPostRequest

from stac_fastapi_catalogs_extension import (
    CatalogsExtension,
    CatalogsSearchExtension,
    CATALOGS_SEARCH_CONFORMANCE,
)
from my_project.catalogs_client import CatalogsClient
from my_project.core_client import CoreClient


settings = ApiSettings()

core_client = CoreClient(...)
catalogs_client = CatalogsClient(...)

# In a real deployment, these models are generated dynamically by 
# your search extensions (e.g., FilterExtension, SortExtension).
# We pass them to CatalogsSearchExtension so scoped searches inherit them!
get_request_model = BaseSearchGetRequest
post_request_model = BaseSearchPostRequest

api = StacApi(
    settings=settings,
    client=core_client,
    extensions=[
        CatalogsExtension(
            client=catalogs_client,
            settings=settings.model_dump(),
        ),
        CatalogsSearchExtension(
            client=catalogs_client,
            search_get_request_model=get_request_model,
            search_post_request_model=post_request_model,
            conformance_classes=list(CATALOGS_SEARCH_CONFORMANCE),
            settings=settings.model_dump(),
        ),
    ],
)

app = api.app
```

The search conformance classes are automatically registered when
`CatalogsSearchExtension` is included. This enables scoped search via:
- `GET /catalogs/{catalog_id}/search` - Query parameter-based search
- `POST /catalogs/{catalog_id}/search` - JSON body-based search

Both endpoints perform recursive tree traversal to search only items in
collections linked to the specified catalog and its descendants.

**Key architectural benefit**: By injecting the dynamic core search request models,
the scoped search endpoints automatically inherit any features added to the global
`/search` endpoint (CQL2 filtering, sorting, field projection, etc.), ensuring
scoped searches are always feature-complete!

### Inheriting Core Search Extensions (Filter, Sort, Fields)

If your core STAC API utilizes extensions that modify the global `/search` endpoint
(such as the `FilterExtension` for CQL2, `SortExtension`, or `FieldsExtension`),
the scoped search endpoints can automatically inherit these exact same capabilities.

By passing the dynamically generated request models from your core API setup into
the `CatalogsSearchExtension`, you guarantee that multi-tenant users have access
to the same advanced querying features inside their sub-catalogs:

```python
# Generate models using your core stac-fastapi extensions
get_request_model = create_get_request_model(core_extensions)
post_request_model = create_post_request_model(core_extensions)

CatalogsSearchExtension(
    search_get_request_model=get_request_model,
    search_post_request_model=post_request_model,
    # ...
)
```

This makes it crystal clear that the scoped search isn't a "second-class citizen"
endpoint—it is functionally identical to the main search!

## Backend client requirements

Your CatalogsClient should subclass AsyncBaseCatalogsClient and implement the
required async methods, including:

- get_catalogs
- get_catalog
- create_catalog
- update_catalog
- delete_catalog
- get_catalog_collections
- create_catalog_collection
- get_catalog_collection
- update_catalog_collection
- unlink_catalog_collection
- get_catalog_collection_items
- get_catalog_collection_item
- get_sub_catalogs
- create_sub_catalog
- unlink_sub_catalog
- get_catalog_children
- get_catalog_conformance
- get_catalog_queryables

If you are supporting Scoped Search, your client must also subclass
`AsyncCatalogsSearchClient` and implement:

- `get_all_descendant_collections`
- `catalog_search_get`
- `catalog_search_post`

## Notes for common deployment repos

### stac-fastapi-elasticsearch-opensearch style deployment

- Build a catalogs client that reads/writes catalog links and hierarchy metadata
  from your OpenSearch/Elasticsearch index strategy.
- Reuse your existing core client for global /collections and /search routes.
- Add CatalogsExtension to the extensions list in app.py as shown above.

### stac-fastapi-pgstac style deployment

- Build a catalogs client that maps these methods to SQL functions or pgstac
  tables/views that represent catalog hierarchy and scoped membership.
- Keep link generation contextual for scoped routes
  (/catalogs/{id}/collections/{collection_id}) so UI breadcrumb navigation stays
  correct.
- Add CatalogsExtension to the extensions list in app.py as shown above.

## Endpoints added by this extension

### CatalogsExtension (read-only discovery)

- GET /catalogs
- GET /catalogs/{catalog_id}
- GET /catalogs/{catalog_id}/collections
- GET /catalogs/{catalog_id}/collections/{collection_id}
- GET /catalogs/{catalog_id}/collections/{collection_id}/items
- GET /catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}
- GET /catalogs/{catalog_id}/catalogs
- GET /catalogs/{catalog_id}/children
- GET /catalogs/{catalog_id}/conformance
- GET /catalogs/{catalog_id}/queryables

### CatalogsTransactionExtension (write operations)

When CatalogsTransactionExtension is enabled, the following additional endpoints
are available for catalog and collection management:

- POST /catalogs
- PUT /catalogs/{catalog_id}
- DELETE /catalogs/{catalog_id}
- POST /catalogs/{catalog_id}/collections
- PUT /catalogs/{catalog_id}/collections/{collection_id}
- DELETE /catalogs/{catalog_id}/collections/{collection_id}
- POST /catalogs/{catalog_id}/catalogs
- DELETE /catalogs/{catalog_id}/catalogs/{sub_catalog_id}

### CatalogsSearchExtension (scoped search)

When CatalogsSearchExtension is enabled, the following additional endpoints
are available for searching items within a catalog's descendant tree:

- GET /catalogs/{catalog_id}/search
- POST /catalogs/{catalog_id}/search

These endpoints perform recursive tree traversal to search only items in
collections linked to the specified catalog and its descendants. They automatically
inherit any features from the global `/search` endpoint (CQL2 filtering, sorting,
field projection, etc.).

