Skip to content

Commit

Permalink
Merge pull request #7 from opencosmos/implement-collection-management
Browse files Browse the repository at this point in the history
Implement collection management
  • Loading branch information
TiagoOpenCosmos authored Feb 18, 2025
2 parents 73fa55e + 4e76dfd commit 4f58d87
Show file tree
Hide file tree
Showing 33 changed files with 771 additions and 246 deletions.
12 changes: 0 additions & 12 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install GEOS
run: sudo apt update && sudo apt install libgeos-dev
- name: Install uv
run: pip install uv
- name: Set up uv environment
Expand All @@ -33,8 +31,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install GEOS
run: sudo apt update && sudo apt install libgeos-dev
- name: Install uv
run: pip install uv
- name: Set up uv environment
Expand All @@ -51,8 +47,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install GEOS
run: sudo apt update && sudo apt install libgeos-dev
- name: Install uv
run: pip install uv
- name: Set up uv environment
Expand All @@ -69,8 +63,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install GEOS
run: sudo apt update && sudo apt install libgeos-dev
- name: Install uv
run: pip install uv
- name: Set up uv environment
Expand All @@ -88,8 +80,6 @@ jobs:
needs: [bandit, cognitive, lint, pydocstyle]
steps:
- uses: actions/checkout@v3
- name: Install GEOS
run: sudo apt update && sudo apt install libgeos-dev
- name: Install uv
run: pip install uv
- name: Set up uv environment
Expand All @@ -108,8 +98,6 @@ jobs:
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Install GEOS
run: sudo apt update && sudo apt install libgeos-dev
- name: Install uv
run: pip install uv
- name: Set up uv environment
Expand Down
77 changes: 75 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# DataCosmos SDK

## Overview

The **DataCosmos SDK** allows Open Cosmos' customers to interact with the **DataCosmos APIs** for seamless data management and retrieval. It provides authentication handling, HTTP request utilities, and a client for interacting with the **STAC API** (SpatioTemporal Asset Catalog).

## Installation

### Install via PyPI

The easiest way to install the SDK is via **pip**:

```sh
Expand All @@ -15,10 +17,11 @@ pip install datacosmos
## Getting Started

### Initializing the Client

The recommended way to initialize the SDK is by passing a `Config` object with authentication credentials:

```python
from datacosmos.client import DatacosmosClient
from datacosmos.datacosmos_client import DatacosmosClient
from datacosmos.config import Config

config = Config(
Expand All @@ -33,11 +36,13 @@ client = DatacosmosClient(config=config)
```

Alternatively, the SDK can load configuration automatically from:

- A YAML file (`config/config.yaml`)
- Environment variables

### STAC Client
The **STACClient** enables interaction with the STAC API, allowing for searching, retrieving, creating, updating, and deleting STAC items.

The STACClient enables interaction with the STAC API, allowing for searching, retrieving, creating, updating, and deleting STAC items and collections.

#### Initialize STACClient

Expand All @@ -49,7 +54,65 @@ stac_client = STACClient(client)

### STACClient Methods

#### 1. **Fetch a Collection**

```python
from datacosmos.stac.stac_client import STACClient
from datacosmos.datacosmos_client import DatacosmosClient

datacosmos_client = DatacosmosClient()
stac_client = STACClient(datacosmos_client)

collection = stac_client.fetch_collection("test-collection")
```

#### 2. **Fetch All Collections**

```python
collections = list(stac_client.fetch_all_collections())
```

#### 3. **Create a Collection**

```python
from pystac import Collection

new_collection = Collection(
id="test-collection",
title="Test Collection",
description="This is a test collection",
license="proprietary",
extent={
"spatial": {"bbox": [[-180, -90, 180, 90]]},
"temporal": {"interval": [["2023-01-01T00:00:00Z", None]]},
},
)

stac_client.create_collection(new_collection)
```

#### 4. **Update a Collection**

```python
from datacosmos.stac.collection.models.collection_update import CollectionUpdate

update_data = CollectionUpdate(
title="Updated Collection Title version 2",
description="Updated description version 2",
)

stac_client.update_collection("test-collection", update_data)
```

#### 5. **Delete a Collection**

```python
collection_id = "test-collection"
stac_client.delete_collection(collection_id)
```

#### 1. **Search Items**

```python
from datacosmos.stac.models.search_parameters import SearchParameters

Expand All @@ -58,16 +121,19 @@ items = list(stac_client.search_items(parameters=parameters))
```

#### 2. **Fetch a Single Item**

```python
item = stac_client.fetch_item(item_id="example-item", collection_id="example-collection")
```

#### 3. **Fetch All Items in a Collection**

```python
items = stac_client.fetch_collection_items(collection_id="example-collection")
```

#### 4. **Create a New STAC Item**

```python
from pystac import Item, Asset
from datetime import datetime
Expand Down Expand Up @@ -95,6 +161,7 @@ stac_client.create_item(collection_id="example-collection", item=stac_item)
```

#### 5. **Update an Existing STAC Item**

```python
from datacosmos.stac.models.item_update import ItemUpdate
from pystac import Asset, Link
Expand Down Expand Up @@ -124,22 +191,27 @@ stac_client.update_item(item_id="new-item", collection_id="example-collection",
```

#### 6. **Delete an Item**

```python
stac_client.delete_item(item_id="new-item", collection_id="example-collection")
```

## Configuration Options

- **Recommended:** Instantiate `DatacosmosClient` with a `Config` object.
- Alternatively, use **YAML files** (`config/config.yaml`).
- Or, use **environment variables**.

## Contributing

If you would like to contribute:

1. Fork the repository.
2. Create a feature branch.
3. Submit a pull request.

### Development Setup

If you are developing the SDK, you can use `uv` for dependency management:

```sh
Expand All @@ -151,6 +223,7 @@ source .venv/bin/activate
```

Before making changes, ensure that:

- The code is formatted using **Black** and **isort**.
- Static analysis and linting are performed using **ruff** and **pydocstyle**.
- Security checks are performed using **bandit**.
Expand Down
File renamed without changes.
4 changes: 4 additions & 0 deletions datacosmos/stac/collection/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""STAC package for interacting with collections from the STAC API, providing query and fetch functionalities.
It enables interaction with collections from the STAC using an authenticated Datacosmos client.
"""
149 changes: 149 additions & 0 deletions datacosmos/stac/collection/collection_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""Handles operations related to STAC collections."""

from typing import Generator, Optional

from pystac import Collection, Extent, SpatialExtent, TemporalExtent
from pystac.utils import str_to_datetime

from datacosmos.datacosmos_client import DatacosmosClient
from datacosmos.stac.collection.models.collection_update import CollectionUpdate
from datacosmos.utils.http_response import check_api_response


class CollectionClient:
"""Handles operations related to STAC collections."""

def __init__(self, client: DatacosmosClient):
"""Initialize the CollectionClient with a DatacosmosClient."""
self.client = client
self.base_url = client.config.stac.as_domain_url()

def fetch_collection(self, collection_id: str) -> Collection:
"""Fetch details of an existing STAC collection."""
url = self.base_url.with_suffix(f"/collections/{collection_id}")
response = self.client.get(url)
check_api_response(response)
return Collection.from_dict(response.json())

def create_collection(self, collection: Collection) -> None:
"""Create a new STAC collection.
Args:
collection (Collection): The STAC collection to create.
Raises:
InvalidRequest: If the collection data is malformed.
"""
if isinstance(collection.extent, dict):
spatial_data = collection.extent.get("spatial", {}).get("bbox", [[]])
temporal_data = collection.extent.get("temporal", {}).get("interval", [[]])

# Convert string timestamps to datetime objects
parsed_temporal = []
for interval in temporal_data:
start = str_to_datetime(interval[0]) if interval[0] else None
end = (
str_to_datetime(interval[1])
if len(interval) > 1 and interval[1]
else None
)
parsed_temporal.append([start, end])

collection.extent = Extent(
spatial=SpatialExtent(spatial_data),
temporal=TemporalExtent(parsed_temporal),
)

url = self.base_url.with_suffix("/collections")
response = self.client.post(url, json=collection.to_dict())
check_api_response(response)

def update_collection(
self, collection_id: str, update_data: CollectionUpdate
) -> None:
"""Update an existing STAC collection."""
url = self.base_url.with_suffix(f"/collections/{collection_id}")
response = self.client.patch(
url, json=update_data.model_dump(by_alias=True, exclude_none=True)
)
check_api_response(response)

def delete_collection(self, collection_id: str) -> None:
"""Delete a STAC collection by its ID."""
url = self.base_url.with_suffix(f"/collections/{collection_id}")
response = self.client.delete(url)
check_api_response(response)

def fetch_all_collections(self) -> Generator[Collection, None, None]:
"""Fetch all STAC collections with pagination support."""
url = self.base_url.with_suffix("/collections")
params = {"limit": 10}

while True:
data = self._fetch_collections_page(url, params)
yield from self._parse_collections(data)

next_cursor = self._get_next_pagination_cursor(data)
if not next_cursor:
break

params["cursor"] = next_cursor

def _fetch_collections_page(self, url: str, params: dict) -> dict:
"""Fetch a single page of collections from the API."""
response = self.client.get(url, params=params)
check_api_response(response)

data = response.json()

if isinstance(data, list):
return {"collections": data}

return data

def _parse_collections(self, data: dict) -> Generator[Collection, None, None]:
"""Convert API response data to STAC Collection objects, ensuring required fields exist."""
return (
Collection.from_dict(
{
**collection,
"type": collection.get("type", "Collection"),
"id": collection.get("id", ""),
"stac_version": collection.get("stac_version", "1.0.0"),
"extent": collection.get(
"extent",
{"spatial": {"bbox": []}, "temporal": {"interval": []}},
),
"links": collection.get("links", []) or [],
"properties": collection.get("properties", {}),
}
)
for collection in data.get("collections", [])
if collection.get("type") == "Collection"
)

def _get_next_pagination_cursor(self, data: dict) -> Optional[str]:
"""Extract the next pagination token from the response."""
next_href = self._get_next_link(data)
return self._extract_pagination_token(next_href) if next_href else None

def _get_next_link(self, data: dict) -> Optional[str]:
"""Extract the next page link from the response."""
next_link = next(
(link for link in data.get("links", []) if link.get("rel") == "next"), None
)
return next_link.get("href", "") if next_link else None

def _extract_pagination_token(self, next_href: str) -> Optional[str]:
"""Extract the pagination token from the next link URL.
Args:
next_href (str): The next page URL.
Returns:
Optional[str]: The extracted token, or None if parsing fails.
"""
try:
return next_href.split("?")[1].split("=")[-1]
except (IndexError, AttributeError):
raise InvalidRequest(f"Failed to parse pagination token from {next_href}")
1 change: 1 addition & 0 deletions datacosmos/stac/collection/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Models for the Collection Client."""
Loading

0 comments on commit 4f58d87

Please sign in to comment.