Skip to content

Commit dc30164

Browse files
committed
feat: setup logging + unique id
1 parent dfddfb9 commit dc30164

File tree

5 files changed

+128
-10
lines changed

5 files changed

+128
-10
lines changed

superstac/_logging.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""SuperSTAC logging module"""
2+
3+
import logging
4+
5+
6+
PACKAGE_NAME = "superstac"
7+
LOGGING_FORMAT = f"{PACKAGE_NAME}:%(asctime)s - %(name)s - %(levelname)s - %(message)s"
8+
9+
logger = logging.getLogger(PACKAGE_NAME)
10+
11+
12+
# Set the default logging level to INFO.
13+
logger.setLevel(logging.INFO)
14+
15+
16+
console_handler = logging.StreamHandler()
17+
18+
console_formatter = logging.Formatter(LOGGING_FORMAT)
19+
console_handler.setFormatter(console_formatter)
20+
21+
logger.addHandler(console_handler)
22+
23+
24+
def add_file_logging(file_path=f"{PACKAGE_NAME}.log"):
25+
"""
26+
Add file logging to the logger.
27+
28+
Args:
29+
file_path (str): Path to the log file. Defaults to `superstac.log`.
30+
31+
Example:
32+
from superstac import add_file_logging
33+
# Configure custom file log
34+
add_file_logging("my_log.log")
35+
"""
36+
file_handler = logging.FileHandler(file_path)
37+
file_formatter = logging.Formatter(LOGGING_FORMAT)
38+
file_handler.setFormatter(file_formatter)
39+
logger.addHandler(file_handler)
40+
41+
42+
def configure_logging(level=logging.WARNING):
43+
"""
44+
Configure the logging level for the package.
45+
46+
Args:
47+
level (int): Logging level (e.g., logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR)
48+
49+
Example:
50+
# Set logging level to INFO
51+
from superstac import configure_logging
52+
import logging
53+
# Configure logging to INFO level
54+
configure_logging(logging.INFO)
55+
"""
56+
logger.setLevel(level)
57+
for handler in logger.handlers:
58+
handler.setLevel(level)

superstac/catalog.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import attr
55
from typing import Any, Dict, Optional, Union
66

7+
from superstac._logging import logger
78
from superstac.enums import CatalogOutputFormat
89
from superstac.exceptions import (
910
CatalogConfigFileNotFound,
@@ -16,6 +17,7 @@
1617

1718
@attr.s(auto_attribs=True)
1819
class CatalogManager:
20+
logger.info("Initialized superstac")
1921
catalogs: Dict[str, CatalogEntry] = attr.Factory(dict)
2022

2123
def register_catalog(
@@ -41,10 +43,19 @@ def register_catalog(
4143
Returns:
4244
CatalogEntry: The registered STAC catalog.
4345
"""
46+
logger.info(f"Registering catalog: {name}")
47+
logger.debug(
48+
f"Params - url: {url}, is_private: {is_private}, summary: {summary}, auth: {auth}"
49+
)
4450
if is_private and auth is None:
51+
logger.error(
52+
f"Private catalog '{name}' requires authentication but none was provided."
53+
)
4554
raise InvalidCatalogSchemaError(
4655
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."
4756
)
57+
58+
logger.info(f"Catalog '{name}' registered successfully.")
4859
self.catalogs[name] = CatalogEntry(
4960
name=name,
5061
url=url,
@@ -65,56 +76,86 @@ def get_available_catalogs(
6576
Returns:
6677
list[CatalogEntry]: The list of all available STAC catalogs.
6778
"""
79+
logger.info("Retrieving available catalogs.")
6880
if isinstance(format, str):
6981
try:
7082
format = CatalogOutputFormat(format.lower())
7183
except ValueError:
84+
logger.error(f"Invalid output format: {format}")
7285
raise ValueError(f"Invalid format: {format}")
7386

74-
return [
87+
available = [
7588
c.as_dict() if format == CatalogOutputFormat.DICT else c.as_json()
7689
for c in self.catalogs.values()
7790
if c.is_available
7891
]
7992

93+
logger.info(f"{len(available)} catalogs available in format '{format.value}'.")
94+
return available
95+
8096
def load_catalogs_from_config(
8197
self, file: Union[str, Path, None] = None
8298
) -> Dict[str, CatalogEntry]:
99+
"""Load catalogs from configuration file.
100+
101+
Args:
102+
file (Union[str, Path, None], optional): Path to the configuration file. Defaults to None.
103+
104+
Raises:
105+
CatalogConfigFileNotFound: Raised when the catalog config file is not founds.
106+
InvalidCatalogYAMLError: Raised when the yaml file is invalid.
107+
InvalidCatalogSchemaError: Raised when there is a schema error in the provided config file.
108+
109+
Returns:
110+
Dict[str, CatalogEntry]: The registered catalogs.
111+
"""
112+
logger.info("Loading catalogs from configuration file.")
83113
if file is None:
84114
base_dir = Path(__file__).parent
85115
file = base_dir / ".superstac.yml"
86116

87117
path = Path(file).expanduser().resolve()
118+
logger.debug(f"Resolved config path: {path}")
88119

89120
if not path.exists():
121+
logger.error(f"Config file not found at path: {path}")
90122
raise CatalogConfigFileNotFound(f"Config file not found at {path}")
91123

92124
try:
93125
with open(path, "r") as f:
94126
data = yaml.safe_load(f) or {}
127+
logger.info(f"Successfully loaded YAML config from: {path}")
95128
except yaml.YAMLError as e:
129+
logger.exception("YAML parsing failed.")
96130
raise InvalidCatalogYAMLError(f"YAML parsing failed: {e}") from e
97131
except Exception as e:
132+
logger.exception("Unexpected error while reading config.")
98133
raise InvalidCatalogYAMLError(
99134
f"Unexpected error reading config: {e}"
100135
) from e
101136

102137
catalogs = data.get("catalogs")
103138
if not isinstance(catalogs, dict):
139+
logger.error(
140+
f"Missing or invalid 'catalogs' section in config file: {path}"
141+
)
104142
raise InvalidCatalogSchemaError(
105143
f"Missing or invalid 'catalogs' section in config file: {path}"
106144
)
107145

108-
# Register each catalog
146+
logger.info(f"Found {len(catalogs)} catalogs to register.")
109147
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-
148+
try:
149+
self.register_catalog(
150+
name=name,
151+
url=spec.get("url"),
152+
is_private=spec.get("is_private", False),
153+
summary=spec.get("summary"),
154+
auth=AuthInfo(**spec["auth"]) if "auth" in spec else None,
155+
)
156+
except Exception as e:
157+
logger.warning(f"Failed to register catalog '{name}': {e}")
158+
logger.info("All catalogs loaded and registered.")
118159
return self.catalogs
119160

120161

superstac/logger.py

Whitespace-only changes.

superstac/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from urllib.parse import urlparse
77

88
from superstac.enums import AuthType
9+
from superstac.utils import compute_catalog_id
910

1011

1112
# Todo - make auth type have a default ?
@@ -32,6 +33,8 @@ class CatalogEntry:
3233

3334
name: str
3435
url: str
36+
id: Optional[str] = None
37+
"""Internal unique ID for the catalog. Will be autogenerated post init."""
3538
"""URL to the STAC catalog."""
3639
is_private: Optional[bool] = False
3740
"""Indicates whether the catalog is a private catalog or not."""
@@ -54,6 +57,7 @@ def __post_init__(self):
5457
raise ValueError(
5558
f"Invalid URL scheme: '{parsed_url.scheme}' for '{self.url}' - Only http and https are allowed."
5659
)
60+
self.id = compute_catalog_id(self.name, self.url)
5761

5862
def as_dict(self):
5963
return asdict(self)

superstac/utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import uuid
2+
3+
4+
def compute_catalog_id(name: str, url: str) -> str:
5+
"""Compute a unique catalog id.
6+
7+
Args:
8+
name (str): The name of the catalog.
9+
url (str): The url of the catalog.
10+
11+
Returns:
12+
str: A unique uuid for the catalog.
13+
"""
14+
uid = uuid.uuid4().hex[:10]
15+
return f"{name.lower().replace(' ', '_')}_{uid}"

0 commit comments

Comments
 (0)