Skip to content

Commit dfddfb9

Browse files
committed
chore: progress with catalog manager
1 parent f5f9b89 commit dfddfb9

File tree

8 files changed

+202
-12
lines changed

8 files changed

+202
-12
lines changed

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ version = "0.1.0"
44
description = "Add your description here"
55
readme = "README.md"
66
requires-python = ">=3.9"
7-
dependencies = []
7+
dependencies = [
8+
"pystac-client==0.8.5",
9+
]
810

911
[tool.uv]
1012
dev-dependencies = [

ruff.toml

Lines changed: 0 additions & 2 deletions
This file was deleted.

superstac/.superstac.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
catalogs:
22
Earth Search:
33
url: https://earth-search.aws.element84.com/v1
4+
summary: 'Element 84 STAC catalog'
5+
is_private: False
46
Planetary Computer:
57
url: https://planetarycomputer.microsoft.com/api/stac/v1
8+
summary: 'Microsoft Planetary Computer STAC catalog'
9+
is_private: False

superstac/catalog.py

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,129 @@
1-
class CatalogManager: ...
1+
"""SuperSTAC Catalog Manager"""
2+
3+
from pathlib import Path
4+
import attr
5+
from typing import Any, Dict, Optional, Union
6+
7+
from superstac.enums import CatalogOutputFormat
8+
from superstac.exceptions import (
9+
CatalogConfigFileNotFound,
10+
InvalidCatalogSchemaError,
11+
InvalidCatalogYAMLError,
12+
)
13+
from superstac.models import CatalogEntry, AuthInfo
14+
import yaml
15+
16+
17+
@attr.s(auto_attribs=True)
18+
class CatalogManager:
19+
catalogs: Dict[str, CatalogEntry] = attr.Factory(dict)
20+
21+
def register_catalog(
22+
self,
23+
name: str,
24+
url: str,
25+
is_private: Optional[bool] = False,
26+
summary: Optional[str] = None,
27+
auth: Optional[AuthInfo] = None,
28+
) -> CatalogEntry:
29+
"""Register a single STAC catalog in state.
30+
31+
Args:
32+
name (str): The name of the catalog.
33+
url (str): A valid URL to the catalog.
34+
is_private (Optional[bool], optional): Indicates if the catalog requires authentication or not. Defaults to False.
35+
summary (Optional[str], optional): A short description of the catalog. Defaults to None.
36+
auth (Optional[AuthInfo], optional): Authentication parameters for the catalog. Defaults to None.
37+
38+
Raises:
39+
InvalidCatalogSchemaError: If an invalid parameter is encountered.
40+
41+
Returns:
42+
CatalogEntry: The registered STAC catalog.
43+
"""
44+
if is_private and auth is None:
45+
raise InvalidCatalogSchemaError(
46+
f"Authentication parameters is required for private catalogs. If this is a mistake, you can set 'is_private' to False or provide the {AuthInfo.__annotations__} parameters."
47+
)
48+
self.catalogs[name] = CatalogEntry(
49+
name=name,
50+
url=url,
51+
summary=summary,
52+
is_private=is_private,
53+
auth=AuthInfo(**auth.__dict__) if auth and not is_private else None,
54+
)
55+
return self.catalogs[name]
56+
57+
def get_available_catalogs(
58+
self, format: Union[str, CatalogOutputFormat] = CatalogOutputFormat.DICT
59+
) -> list[Union[dict[str, Any], str]]:
60+
"""Get the available STAC catalogs.
61+
62+
Raises:
63+
ValueError: When an invalid format is provided.
64+
65+
Returns:
66+
list[CatalogEntry]: The list of all available STAC catalogs.
67+
"""
68+
if isinstance(format, str):
69+
try:
70+
format = CatalogOutputFormat(format.lower())
71+
except ValueError:
72+
raise ValueError(f"Invalid format: {format}")
73+
74+
return [
75+
c.as_dict() if format == CatalogOutputFormat.DICT else c.as_json()
76+
for c in self.catalogs.values()
77+
if c.is_available
78+
]
79+
80+
def load_catalogs_from_config(
81+
self, file: Union[str, Path, None] = None
82+
) -> Dict[str, CatalogEntry]:
83+
if file is None:
84+
base_dir = Path(__file__).parent
85+
file = base_dir / ".superstac.yml"
86+
87+
path = Path(file).expanduser().resolve()
88+
89+
if not path.exists():
90+
raise CatalogConfigFileNotFound(f"Config file not found at {path}")
91+
92+
try:
93+
with open(path, "r") as f:
94+
data = yaml.safe_load(f) or {}
95+
except yaml.YAMLError as e:
96+
raise InvalidCatalogYAMLError(f"YAML parsing failed: {e}") from e
97+
except Exception as e:
98+
raise InvalidCatalogYAMLError(
99+
f"Unexpected error reading config: {e}"
100+
) from e
101+
102+
catalogs = data.get("catalogs")
103+
if not isinstance(catalogs, dict):
104+
raise InvalidCatalogSchemaError(
105+
f"Missing or invalid 'catalogs' section in config file: {path}"
106+
)
107+
108+
# Register each catalog
109+
for name, spec in catalogs.items():
110+
self.register_catalog(
111+
name=name,
112+
url=spec.get("url"),
113+
is_private=spec.get("is_private", False),
114+
summary=spec.get("summary"),
115+
auth=AuthInfo(**spec["auth"]) if "auth" in spec else None,
116+
)
117+
118+
return self.catalogs
119+
120+
121+
## TEST
122+
123+
124+
if __name__ == "__main__":
125+
# cm = CatalogManager()
126+
# cm.register_catalog(name="My Catalog", url="https://example.com/stac")
127+
# print(cm.load_catalogs_from_config())
128+
# print(cm.get_available_catalogs())
129+
...

superstac/enums.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ class AuthType(str, Enum):
55
BEARER = "bearer"
66
BASIC = "basic"
77
API_KEY = "apikey"
8+
9+
10+
class CatalogOutputFormat(Enum):
11+
JSON = "json"
12+
DICT = "dict"

superstac/exceptions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class InvalidCatalogSchemaError(Exception):
2+
"""Raised when an invalid catalog schema is provided."""
3+
4+
5+
class ParametersError(Exception):
6+
"""Raised when invalid parameters are used in a query."""
7+
8+
9+
class CatalogConfigFileNotFound(FileNotFoundError):
10+
"""Raised when the provided catalog config file is not found."""
11+
12+
13+
class InvalidCatalogYAMLError(Exception):
14+
"""Raised when invalid catalog yaml is passed or if does not conform to the schema."""

superstac/logger.py

Whitespace-only changes.

superstac/models.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,62 @@
1-
from dataclasses import dataclass, field, asdict
2-
from typing import Optional, List
1+
"""SuperSTAC Models"""
32

3+
from dataclasses import asdict, dataclass
4+
import json
5+
from typing import Optional
6+
from urllib.parse import urlparse
47

8+
from superstac.enums import AuthType
9+
10+
11+
# Todo - make auth type have a default ?
512
@dataclass
613
class AuthInfo:
7-
type: str
14+
"""AuthInfo"""
15+
16+
type: AuthType
817
token: Optional[str] = None
918
username: Optional[str] = None
1019
password: Optional[str] = None
1120
header_key: Optional[str] = None
1221

22+
def as_dict(self):
23+
return asdict(self)
24+
25+
def as_json(self):
26+
return json.dumps(self.as_dict(), indent=2)
27+
1328

1429
@dataclass
1530
class CatalogEntry:
31+
"""CatalogEntry"""
32+
1633
name: str
1734
url: str
35+
"""URL to the STAC catalog."""
36+
is_private: Optional[bool] = False
37+
"""Indicates whether the catalog is a private catalog or not."""
38+
summary: Optional[str] = None
39+
"""Short description of the catalog."""
1840
auth: Optional[AuthInfo] = None
19-
is_available: bool = False
20-
latency_ms: Optional[float] = None
21-
conforms_to: Optional[List[str]] = field(default_factory=list)
22-
collections: Optional[List[str]] = field(default_factory=list)
23-
extensions: Optional[List[str]] = field(default_factory=list)
41+
"""Authentication parameters."""
42+
is_available: Optional[bool] = True
43+
"""Defaults to True on instantiation. It will be updated based on the status code of the catalog. If 200 True and False if otherwise."""
44+
45+
def __post_init__(self):
46+
# Validate the URL
47+
parsed_url = urlparse(self.url)
48+
if not all([parsed_url.scheme, parsed_url.netloc]):
49+
raise ValueError(
50+
f"Invalid URL: '{self.url}' - Must have a scheme (e.g., http/https) and network location."
51+
)
52+
53+
if parsed_url.scheme not in ("http", "https"):
54+
raise ValueError(
55+
f"Invalid URL scheme: '{parsed_url.scheme}' for '{self.url}' - Only http and https are allowed."
56+
)
57+
58+
def as_dict(self):
59+
return asdict(self)
60+
61+
def as_json(self):
62+
return json.dumps(self.as_dict(), indent=2)

0 commit comments

Comments
 (0)