sunstone-data-platform 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sunstone_data/__init__.py +36 -0
- sunstone_data/_version.py +24 -0
- sunstone_data/auth/__init__.py +43 -0
- sunstone_data/auth/client_creds.py +239 -0
- sunstone_data/auth/pkce.py +307 -0
- sunstone_data/auth/store.py +97 -0
- sunstone_data/catalog.py +175 -0
- sunstone_data/cli/__init__.py +5 -0
- sunstone_data/cli/_app.py +26 -0
- sunstone_data/cli/_errors.py +31 -0
- sunstone_data/cli/commands/__init__.py +22 -0
- sunstone_data/cli/commands/data_auth.py +120 -0
- sunstone_data/cli/commands/data_import.py +157 -0
- sunstone_data/cli/commands/data_info.py +166 -0
- sunstone_data/cli/commands/data_publish.py +69 -0
- sunstone_data/cli/commands/ls.py +49 -0
- sunstone_data/cli/commands/prov.py +58 -0
- sunstone_data/cli/commands/refs.py +142 -0
- sunstone_data/cli/commands/show.py +139 -0
- sunstone_data/cli/commands/snapshots.py +77 -0
- sunstone_data/cli/commands/sparql.py +68 -0
- sunstone_data/cli/main.py +30 -0
- sunstone_data/config.py +250 -0
- sunstone_data/discovery.py +317 -0
- sunstone_data/env_commands.py +97 -0
- sunstone_data/env_section.py +86 -0
- sunstone_data/graph/__init__.py +36 -0
- sunstone_data/graph/client.py +328 -0
- sunstone_data/graph/dataset.py +438 -0
- sunstone_data/graph/terms.py +34 -0
- sunstone_data/identifiers.py +155 -0
- sunstone_data/import_/__init__.py +28 -0
- sunstone_data/import_/fetcher.py +259 -0
- sunstone_data/import_/owner.py +130 -0
- sunstone_data/import_/parser.py +492 -0
- sunstone_data/import_/pipeline.py +275 -0
- sunstone_data/import_/storage.py +135 -0
- sunstone_data/metadata/__init__.py +41 -0
- sunstone_data/metadata/builder.py +276 -0
- sunstone_data/metadata/models.py +87 -0
- sunstone_data/metadata/provenance.py +171 -0
- sunstone_data/metadata/provenance_rdf.py +102 -0
- sunstone_data/plugin.py +62 -0
- sunstone_data/publish.py +134 -0
- sunstone_data/query.py +624 -0
- sunstone_data/resolve.py +178 -0
- sunstone_data/url.py +78 -0
- sunstone_data/utils/__init__.py +12 -0
- sunstone_data/utils/checksum.py +14 -0
- sunstone_data/utils/encoding.py +16 -0
- sunstone_data/utils/naming.py +38 -0
- sunstone_data/versioning/__init__.py +17 -0
- sunstone_data/versioning/schema_evolution.py +180 -0
- sunstone_data_platform-1.0.0.dist-info/METADATA +302 -0
- sunstone_data_platform-1.0.0.dist-info/RECORD +66 -0
- sunstone_data_platform-1.0.0.dist-info/WHEEL +4 -0
- sunstone_data_platform-1.0.0.dist-info/entry_points.txt +2 -0
- sunstone_pipeline/__init__.py +1 -0
- sunstone_pipeline/assets/__init__.py +11 -0
- sunstone_pipeline/assets/mirrors.py +123 -0
- sunstone_pipeline/assets/transforms.py +74 -0
- sunstone_pipeline/definitions.py +25 -0
- sunstone_pipeline/io_manager.py +56 -0
- sunstone_pipeline/resources/__init__.py +6 -0
- sunstone_pipeline/resources/iceberg.py +29 -0
- sunstone_pipeline/resources/refs.py +88 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""``sunstone data history`` — the Iceberg snapshot log (spec 180 §8).
|
|
2
|
+
|
|
3
|
+
This reads Iceberg table metadata through the catalog, not the graph server.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from sunstone_data.cli._app import console, data_app
|
|
13
|
+
from sunstone_data.cli._errors import cli_errors
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@data_app.command("history")
|
|
17
|
+
def history_cmd(
|
|
18
|
+
table: str = typer.Argument(..., help="Table reference (namespace.table[@branch])"),
|
|
19
|
+
branch: str = typer.Option("main", "--branch", "-b", help="Catalog branch to read"),
|
|
20
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Show a table's Iceberg snapshot log, oldest snapshot first.
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
sunstone data history un.health.cases
|
|
26
|
+
sunstone data history un.health.cases@experiment --json
|
|
27
|
+
"""
|
|
28
|
+
with cli_errors(json_output):
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
|
|
31
|
+
from sunstone_data.catalog import get_catalog
|
|
32
|
+
from sunstone_data.query import TableRef
|
|
33
|
+
|
|
34
|
+
ref = TableRef.parse(table)
|
|
35
|
+
iceberg_table = get_catalog(ref.version_ref or branch).load_table(f"{ref.namespace}.{ref.table}")
|
|
36
|
+
|
|
37
|
+
rows = []
|
|
38
|
+
for entry in iceberg_table.history():
|
|
39
|
+
snapshot = iceberg_table.snapshot_by_id(entry.snapshot_id)
|
|
40
|
+
summary = getattr(snapshot, "summary", None)
|
|
41
|
+
operation = getattr(summary, "operation", None)
|
|
42
|
+
parent = getattr(snapshot, "parent_snapshot_id", None)
|
|
43
|
+
rows.append(
|
|
44
|
+
{
|
|
45
|
+
"snapshot_id": str(entry.snapshot_id),
|
|
46
|
+
"parent_snapshot_id": str(parent) if parent is not None else None,
|
|
47
|
+
"timestamp": datetime.fromtimestamp(entry.timestamp_ms / 1000, tz=timezone.utc).isoformat(),
|
|
48
|
+
"operation": getattr(operation, "value", operation),
|
|
49
|
+
"summary": dict(getattr(summary, "additional_properties", {}) or {}),
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if json_output:
|
|
54
|
+
print(json.dumps(rows, indent=2))
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
if not rows:
|
|
58
|
+
console.print("[yellow]No snapshots found[/yellow]")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
from rich.table import Table
|
|
62
|
+
|
|
63
|
+
out = Table(box=None, pad_edge=False)
|
|
64
|
+
out.add_column("Timestamp", style="cyan")
|
|
65
|
+
out.add_column("Snapshot")
|
|
66
|
+
out.add_column("Parent")
|
|
67
|
+
out.add_column("Operation", style="magenta")
|
|
68
|
+
out.add_column("Records added", justify="right")
|
|
69
|
+
for row in rows:
|
|
70
|
+
out.add_row(
|
|
71
|
+
row["timestamp"],
|
|
72
|
+
row["snapshot_id"],
|
|
73
|
+
row["parent_snapshot_id"] or "",
|
|
74
|
+
row["operation"] or "",
|
|
75
|
+
row["summary"].get("added-records", ""),
|
|
76
|
+
)
|
|
77
|
+
console.print(out)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""``sunstone data sparql`` — run a query through the graph-server proxy (spec 180 §8).
|
|
2
|
+
|
|
3
|
+
`execute_query` is also what ``sunstone data lineage`` runs its PROV query
|
|
4
|
+
with, so both commands render SPARQL results the same way.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from sunstone_data.cli._app import console, data_app
|
|
12
|
+
from sunstone_data.cli._errors import cli_errors
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def execute_query(query: str, json_output: bool, empty_message: str = "No results") -> None:
|
|
16
|
+
"""Run `query` through the proxy and write the answer to stdout.
|
|
17
|
+
|
|
18
|
+
With `json_output` the response body is passed through untouched. A
|
|
19
|
+
SPARQL results document is rendered as a table, an ASK answer as
|
|
20
|
+
``true``/``false``, and anything else (CONSTRUCT, DESCRIBE, an error
|
|
21
|
+
page) as its raw text.
|
|
22
|
+
"""
|
|
23
|
+
from sunstone_data.graph.client import get_graph_client
|
|
24
|
+
|
|
25
|
+
response = get_graph_client().sparql(query)
|
|
26
|
+
|
|
27
|
+
if json_output:
|
|
28
|
+
print(response.text)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
payload = response.json()
|
|
33
|
+
except ValueError:
|
|
34
|
+
print(response.text)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if "boolean" in payload:
|
|
38
|
+
console.print(str(payload["boolean"]).lower())
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
bindings = payload.get("results", {}).get("bindings", [])
|
|
42
|
+
if not bindings:
|
|
43
|
+
console.print(f"[yellow]{empty_message}[/yellow]")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
from rich.table import Table
|
|
47
|
+
|
|
48
|
+
variables = payload.get("head", {}).get("vars") or sorted({k for b in bindings for k in b})
|
|
49
|
+
table = Table(box=None, pad_edge=False)
|
|
50
|
+
for variable in variables:
|
|
51
|
+
table.add_column(variable, style="cyan", overflow="fold")
|
|
52
|
+
for binding in bindings:
|
|
53
|
+
table.add_row(*(binding.get(v, {}).get("value", "") for v in variables))
|
|
54
|
+
console.print(table)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@data_app.command("sparql")
|
|
58
|
+
def sparql_cmd(
|
|
59
|
+
query: str = typer.Argument(..., help="SPARQL query text"),
|
|
60
|
+
json_output: bool = typer.Option(False, "--json", help="Print the raw response body"),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Run a SPARQL query through the graph-server proxy.
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
sunstone data sparql 'SELECT * WHERE { GRAPH ?g { ?s ?p ?o } } LIMIT 10'
|
|
66
|
+
"""
|
|
67
|
+
with cli_errors(json_output):
|
|
68
|
+
execute_query(query, json_output)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Entry point for the ``sunstone`` CLI.
|
|
2
|
+
|
|
3
|
+
Kept intentionally thin so that ``sunstone`` / ``sunstone --help`` and the
|
|
4
|
+
sunstone-py-owned subcommands (``sunstone env`` etc.) don't pay the cost
|
|
5
|
+
of pulling pyiceberg, pandas, s3fs, and the rest of the data-platform
|
|
6
|
+
stack just to print a usage line.
|
|
7
|
+
|
|
8
|
+
Layout:
|
|
9
|
+
|
|
10
|
+
* :mod:`sunstone_data.cli._app` — defines ``app``, ``data_app``, ``console``
|
|
11
|
+
(Typer + Rich only, ~1ms to import).
|
|
12
|
+
* :mod:`sunstone_data.cli.commands` — one module per ``sunstone data ...``
|
|
13
|
+
subcommand. Each module registers its command at import time via
|
|
14
|
+
``@data_app.command(...)`` and lazy-imports its heavy dependencies
|
|
15
|
+
inside the function body.
|
|
16
|
+
|
|
17
|
+
This module re-exports ``app`` (the entry point) and ``data_app`` (used by
|
|
18
|
+
:mod:`sunstone_data.plugin`).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from sunstone_data.cli import commands # noqa: F401 (import side-effect: registers commands)
|
|
24
|
+
from sunstone_data.cli._app import app, console, data_app
|
|
25
|
+
|
|
26
|
+
__all__ = ["app", "console", "data_app"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
app()
|
sunstone_data/config.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Configuration loading for Sunstone Data Platform."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Settings:
|
|
19
|
+
"""Platform configuration settings."""
|
|
20
|
+
|
|
21
|
+
# Iceberg REST catalog
|
|
22
|
+
catalog_uri: str = "http://localhost:8181"
|
|
23
|
+
warehouse: str = "s3://iceberg/"
|
|
24
|
+
|
|
25
|
+
# graph-server base URL. Identity settings are discovered from it at
|
|
26
|
+
# ``/ui/config.json``; see sunstone_data.discovery.
|
|
27
|
+
graph_url: str = ""
|
|
28
|
+
|
|
29
|
+
# OAuth2 client-credentials (service) identity. Setting both selects the
|
|
30
|
+
# client-credentials flow over any cached human login; see
|
|
31
|
+
# sunstone_data.auth.client_creds.
|
|
32
|
+
client_id: str = ""
|
|
33
|
+
client_secret: str = ""
|
|
34
|
+
|
|
35
|
+
# S3-compatible storage (RustFS locally, Hetzner/R2 remote)
|
|
36
|
+
s3_endpoint: str = "http://localhost:9000"
|
|
37
|
+
s3_access_key: str = ""
|
|
38
|
+
s3_secret_key: str = ""
|
|
39
|
+
s3_region: str = "us-east-1" # "auto" for Cloudflare R2
|
|
40
|
+
|
|
41
|
+
# GCS storage
|
|
42
|
+
gcs_project_id: str = ""
|
|
43
|
+
gcs_endpoint: str = "" # empty = real GCS; set for fake-gcs-server
|
|
44
|
+
|
|
45
|
+
# Data-owner organization modeling (import + org tree)
|
|
46
|
+
org_roots: tuple[str, ...] = ()
|
|
47
|
+
owner_rules_path: str = ""
|
|
48
|
+
org_base_uri: str = "https://sunstone.institute/org"
|
|
49
|
+
|
|
50
|
+
# Base IRI the Iceberg REST catalog mints dataset IRIs under. Must match
|
|
51
|
+
# the catalog's own DATA_BASE_IRI (CatalogConfig::from_env in
|
|
52
|
+
# crates/iceberg-catalog/src/app.rs, same default), or named-version
|
|
53
|
+
# lookups address a graph the catalog never wrote.
|
|
54
|
+
data_base_iri: str = "https://data.sunstoneinstitute.ai"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def storage_backend(self) -> str:
|
|
58
|
+
"""Return 'gcs' for gs:// warehouses, 's3' otherwise."""
|
|
59
|
+
return "gcs" if self.warehouse.startswith("gs://") else "s3"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _parse_org_roots(raw: str) -> tuple[str, ...]:
|
|
63
|
+
"""Split a comma-separated ``SUNSTONE_ORG_ROOTS`` value into a tuple.
|
|
64
|
+
|
|
65
|
+
Empty / unset yields an empty tuple, which disables root-allowlist
|
|
66
|
+
enforcement (any owner root is accepted).
|
|
67
|
+
"""
|
|
68
|
+
return tuple(part.strip() for part in raw.split(",") if part.strip())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _r2_defaults() -> dict[str, str]:
|
|
72
|
+
"""Derive S3 client settings from ``R2_*`` env vars (docker ``r2`` profile).
|
|
73
|
+
|
|
74
|
+
The ``r2`` docker-compose profile is configured entirely from four
|
|
75
|
+
``R2_*`` variables (see ``.env.r2.example``). A PyIceberg/DuckDB client
|
|
76
|
+
reads Iceberg data files directly from object storage, so it needs the
|
|
77
|
+
same bucket, endpoint, and credentials Nessie was given. Rather than make
|
|
78
|
+
R2 users duplicate those four values into ``S3_*``/``ICEBERG_WAREHOUSE``,
|
|
79
|
+
derive them here. Explicit ``S3_*``/``ICEBERG_WAREHOUSE``/``CATALOG_URI``
|
|
80
|
+
env vars still take precedence over these derived defaults.
|
|
81
|
+
|
|
82
|
+
Returns an empty dict when ``R2_ACCOUNT_ID``/``R2_BUCKET`` are not set, so
|
|
83
|
+
non-R2 setups are unaffected.
|
|
84
|
+
"""
|
|
85
|
+
account_id = os.getenv("R2_ACCOUNT_ID", "")
|
|
86
|
+
bucket = os.getenv("R2_BUCKET", "")
|
|
87
|
+
if not (account_id and bucket):
|
|
88
|
+
return {}
|
|
89
|
+
return {
|
|
90
|
+
"catalog_uri": "http://localhost:19122",
|
|
91
|
+
"warehouse": f"s3://{bucket}/",
|
|
92
|
+
"s3_endpoint": f"https://{account_id}.r2.cloudflarestorage.com",
|
|
93
|
+
"s3_access_key": os.getenv("R2_ACCESS_KEY_ID", ""),
|
|
94
|
+
"s3_secret_key": os.getenv("R2_SECRET_ACCESS_KEY", ""),
|
|
95
|
+
"s3_region": "auto", # R2 ignores region but the SDK requires a value
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _try_resolve_environment() -> Any | None:
|
|
100
|
+
"""Resolve the active environment via sunstone-py.
|
|
101
|
+
|
|
102
|
+
Returns an ``Environment`` (sunstone-py 1.12+) if available, else None.
|
|
103
|
+
Never raises — missing sunstone-py or resolution errors are logged at
|
|
104
|
+
debug level and return None so the caller falls back to ``.env`` files.
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
from sunstone.env import resolve_environment
|
|
108
|
+
|
|
109
|
+
return resolve_environment()
|
|
110
|
+
except ImportError:
|
|
111
|
+
logger.debug("sunstone-py not installed, skipping environment resolution")
|
|
112
|
+
return None
|
|
113
|
+
except Exception:
|
|
114
|
+
logger.debug("Failed to resolve environment", exc_info=True)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def current_environment_name() -> str:
|
|
119
|
+
"""Name of the active sunstone-py environment, or ``"default"`` if none is active.
|
|
120
|
+
|
|
121
|
+
Per-environment caches (discovery, login sessions) are keyed by this name.
|
|
122
|
+
"""
|
|
123
|
+
env = _try_resolve_environment()
|
|
124
|
+
return getattr(env, "name", None) or "default"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _section_field(env: Any, attr: str) -> str | None:
|
|
128
|
+
"""Read ``attr`` from the data-platform section if present.
|
|
129
|
+
|
|
130
|
+
Returns ``None`` when either the section is missing (legacy config that
|
|
131
|
+
only declared top-level keys) or the field on the section is unset.
|
|
132
|
+
Callers should fall back to ``os.environ`` in that case.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
section = env.section("data-platform")
|
|
136
|
+
except KeyError:
|
|
137
|
+
return None
|
|
138
|
+
value = getattr(section, attr, None)
|
|
139
|
+
return value or None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@lru_cache(maxsize=1)
|
|
143
|
+
def get_settings() -> Settings:
|
|
144
|
+
"""Load settings, preferring sunstone-py's active environment over ``.env`` vars.
|
|
145
|
+
|
|
146
|
+
Resolution order (highest precedence wins):
|
|
147
|
+
1. Real ``os.environ`` entries set by the user / CI.
|
|
148
|
+
2. The active sunstone-py ``Environment``'s ``data-platform`` section.
|
|
149
|
+
3. ``.env`` file in the project root.
|
|
150
|
+
4. Built-in defaults.
|
|
151
|
+
|
|
152
|
+
Storage settings are not discovered. graph-server's ``/ui/config.json``
|
|
153
|
+
publishes identity settings only, so ``warehouse`` and the S3/GCS fields
|
|
154
|
+
resolve from the section, env vars, or built-in defaults.
|
|
155
|
+
|
|
156
|
+
Legacy configs that declared ``graph_url`` etc. at the *top level* of an
|
|
157
|
+
``[environments.<name>]`` block (rather than under
|
|
158
|
+
``[environments.<name>."data-platform"]``) are handled transparently:
|
|
159
|
+
sunstone-py's ``Environment.activate()`` flattens top-level scalars into
|
|
160
|
+
``os.environ`` (e.g. ``CATALOG_URL``, ``S3_ENDPOINT``) so the ``os.environ``
|
|
161
|
+
fallback covers them. Top-level CLI callbacks call ``activate_environment``
|
|
162
|
+
automatically; library callers that want the same behaviour should call
|
|
163
|
+
``sunstone.env.activate_environment()`` once before ``get_settings()``.
|
|
164
|
+
|
|
165
|
+
Settings are cached after first load. Call ``get_settings.cache_clear()``
|
|
166
|
+
to force a reload (e.g. in tests).
|
|
167
|
+
"""
|
|
168
|
+
env = _try_resolve_environment()
|
|
169
|
+
if env is not None:
|
|
170
|
+
# The data-platform section is the new home for typed fields. Each
|
|
171
|
+
# lookup falls back to the corresponding flattened env var so that
|
|
172
|
+
# both new section-style and legacy top-level configs work.
|
|
173
|
+
graph_url = _section_field(env, "graph_url") or os.environ.get("GRAPH_URL") or ""
|
|
174
|
+
client_id = _section_field(env, "client_id") or os.environ.get("SUNSTONE_CLIENT_ID") or ""
|
|
175
|
+
client_secret = _section_field(env, "client_secret") or os.environ.get("SUNSTONE_CLIENT_SECRET") or ""
|
|
176
|
+
section_s3_endpoint = _section_field(env, "s3_endpoint")
|
|
177
|
+
section_s3_access_key = _section_field(env, "s3_access_key")
|
|
178
|
+
section_s3_secret_key = _section_field(env, "s3_secret_key")
|
|
179
|
+
|
|
180
|
+
# The Iceberg REST catalog is a different service from graph_url, so
|
|
181
|
+
# it keeps its own env var rather than being derived from the graph
|
|
182
|
+
# URL. ``CATALOG_URL`` is a legacy flattened spelling, still read.
|
|
183
|
+
catalog_uri = os.environ.get("CATALOG_URI") or os.environ.get("CATALOG_URL") or "http://localhost:8181"
|
|
184
|
+
warehouse = os.getenv("ICEBERG_WAREHOUSE") or "s3://iceberg/"
|
|
185
|
+
gcs_project_id = os.getenv("GCS_PROJECT_ID") or ""
|
|
186
|
+
gcs_endpoint = os.getenv("GCS_ENDPOINT") or ""
|
|
187
|
+
s3_endpoint = section_s3_endpoint or os.environ.get("S3_ENDPOINT") or ""
|
|
188
|
+
s3_access_key = section_s3_access_key or os.environ.get("S3_ACCESS_KEY") or ""
|
|
189
|
+
s3_secret_key = section_s3_secret_key or os.environ.get("S3_SECRET_KEY") or ""
|
|
190
|
+
s3_region = os.environ.get("S3_REGION") or "us-east-1"
|
|
191
|
+
|
|
192
|
+
return Settings(
|
|
193
|
+
catalog_uri=catalog_uri,
|
|
194
|
+
warehouse=warehouse,
|
|
195
|
+
graph_url=graph_url,
|
|
196
|
+
client_id=client_id,
|
|
197
|
+
client_secret=client_secret,
|
|
198
|
+
s3_endpoint=s3_endpoint,
|
|
199
|
+
s3_access_key=s3_access_key,
|
|
200
|
+
s3_secret_key=s3_secret_key,
|
|
201
|
+
s3_region=s3_region,
|
|
202
|
+
gcs_project_id=gcs_project_id,
|
|
203
|
+
gcs_endpoint=gcs_endpoint,
|
|
204
|
+
org_roots=_parse_org_roots(os.getenv("SUNSTONE_ORG_ROOTS", "")),
|
|
205
|
+
owner_rules_path=os.getenv("SUNSTONE_OWNER_RULES_PATH", ""),
|
|
206
|
+
org_base_uri=os.getenv("SUNSTONE_ORG_BASE_URI", "https://sunstone.institute/org"),
|
|
207
|
+
data_base_iri=os.getenv("DATA_BASE_IRI", "https://data.sunstoneinstitute.ai"),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Fallback: load from .env file
|
|
211
|
+
project_root = Path(__file__).parent.parent.parent
|
|
212
|
+
root_env = project_root / ".env"
|
|
213
|
+
|
|
214
|
+
if root_env.exists():
|
|
215
|
+
load_dotenv(root_env)
|
|
216
|
+
|
|
217
|
+
# R2 profile: a present R2_* set seeds the warehouse/endpoint/creds the
|
|
218
|
+
# client needs. Explicit S3_*/ICEBERG_WAREHOUSE/CATALOG_URI vars override.
|
|
219
|
+
r2 = _r2_defaults()
|
|
220
|
+
|
|
221
|
+
catalog_uri = os.getenv("CATALOG_URI") or r2.get("catalog_uri") or "http://localhost:8181"
|
|
222
|
+
return Settings(
|
|
223
|
+
catalog_uri=catalog_uri,
|
|
224
|
+
warehouse=os.getenv("ICEBERG_WAREHOUSE") or r2.get("warehouse") or "s3://iceberg/",
|
|
225
|
+
graph_url=os.getenv("GRAPH_URL", ""),
|
|
226
|
+
client_id=os.getenv("SUNSTONE_CLIENT_ID", ""),
|
|
227
|
+
client_secret=os.getenv("SUNSTONE_CLIENT_SECRET", ""),
|
|
228
|
+
s3_endpoint=os.getenv("S3_ENDPOINT") or r2.get("s3_endpoint") or "http://localhost:9000",
|
|
229
|
+
s3_access_key=(
|
|
230
|
+
os.getenv("S3_ACCESS_KEY")
|
|
231
|
+
or r2.get("s3_access_key")
|
|
232
|
+
or os.getenv("OBJECT_STORE_ACCESS_KEY")
|
|
233
|
+
or os.getenv("AWS_ACCESS_KEY_ID")
|
|
234
|
+
or ""
|
|
235
|
+
),
|
|
236
|
+
s3_secret_key=(
|
|
237
|
+
os.getenv("S3_SECRET_KEY")
|
|
238
|
+
or r2.get("s3_secret_key")
|
|
239
|
+
or os.getenv("OBJECT_STORE_SECRET_KEY")
|
|
240
|
+
or os.getenv("AWS_SECRET_ACCESS_KEY")
|
|
241
|
+
or ""
|
|
242
|
+
),
|
|
243
|
+
s3_region=os.getenv("S3_REGION") or r2.get("s3_region") or "us-east-1",
|
|
244
|
+
gcs_project_id=os.getenv("GCS_PROJECT_ID", ""),
|
|
245
|
+
gcs_endpoint=os.getenv("GCS_ENDPOINT", ""),
|
|
246
|
+
org_roots=_parse_org_roots(os.getenv("SUNSTONE_ORG_ROOTS", "")),
|
|
247
|
+
owner_rules_path=os.getenv("SUNSTONE_OWNER_RULES_PATH", ""),
|
|
248
|
+
org_base_uri=os.getenv("SUNSTONE_ORG_BASE_URI", "https://sunstone.institute/org"),
|
|
249
|
+
data_base_iri=os.getenv("DATA_BASE_IRI", "https://data.sunstoneinstitute.ai"),
|
|
250
|
+
)
|