sunstone-data-platform 1.0.0__tar.gz

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.
Files changed (66) hide show
  1. sunstone_data_platform-1.0.0/.gitignore +22 -0
  2. sunstone_data_platform-1.0.0/PKG-INFO +302 -0
  3. sunstone_data_platform-1.0.0/README.md +277 -0
  4. sunstone_data_platform-1.0.0/pyproject.toml +89 -0
  5. sunstone_data_platform-1.0.0/src/sunstone_data/__init__.py +36 -0
  6. sunstone_data_platform-1.0.0/src/sunstone_data/_version.py +24 -0
  7. sunstone_data_platform-1.0.0/src/sunstone_data/auth/__init__.py +43 -0
  8. sunstone_data_platform-1.0.0/src/sunstone_data/auth/client_creds.py +239 -0
  9. sunstone_data_platform-1.0.0/src/sunstone_data/auth/pkce.py +307 -0
  10. sunstone_data_platform-1.0.0/src/sunstone_data/auth/store.py +97 -0
  11. sunstone_data_platform-1.0.0/src/sunstone_data/catalog.py +175 -0
  12. sunstone_data_platform-1.0.0/src/sunstone_data/cli/__init__.py +5 -0
  13. sunstone_data_platform-1.0.0/src/sunstone_data/cli/_app.py +26 -0
  14. sunstone_data_platform-1.0.0/src/sunstone_data/cli/_errors.py +31 -0
  15. sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/__init__.py +22 -0
  16. sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/data_auth.py +120 -0
  17. sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/data_import.py +157 -0
  18. sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/data_info.py +166 -0
  19. sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/data_publish.py +69 -0
  20. sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/ls.py +49 -0
  21. sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/prov.py +58 -0
  22. sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/refs.py +142 -0
  23. sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/show.py +139 -0
  24. sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/snapshots.py +77 -0
  25. sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/sparql.py +68 -0
  26. sunstone_data_platform-1.0.0/src/sunstone_data/cli/main.py +30 -0
  27. sunstone_data_platform-1.0.0/src/sunstone_data/config.py +250 -0
  28. sunstone_data_platform-1.0.0/src/sunstone_data/discovery.py +317 -0
  29. sunstone_data_platform-1.0.0/src/sunstone_data/env_commands.py +97 -0
  30. sunstone_data_platform-1.0.0/src/sunstone_data/env_section.py +86 -0
  31. sunstone_data_platform-1.0.0/src/sunstone_data/graph/__init__.py +36 -0
  32. sunstone_data_platform-1.0.0/src/sunstone_data/graph/client.py +328 -0
  33. sunstone_data_platform-1.0.0/src/sunstone_data/graph/dataset.py +438 -0
  34. sunstone_data_platform-1.0.0/src/sunstone_data/graph/terms.py +34 -0
  35. sunstone_data_platform-1.0.0/src/sunstone_data/identifiers.py +155 -0
  36. sunstone_data_platform-1.0.0/src/sunstone_data/import_/__init__.py +28 -0
  37. sunstone_data_platform-1.0.0/src/sunstone_data/import_/fetcher.py +259 -0
  38. sunstone_data_platform-1.0.0/src/sunstone_data/import_/owner.py +130 -0
  39. sunstone_data_platform-1.0.0/src/sunstone_data/import_/parser.py +492 -0
  40. sunstone_data_platform-1.0.0/src/sunstone_data/import_/pipeline.py +275 -0
  41. sunstone_data_platform-1.0.0/src/sunstone_data/import_/storage.py +135 -0
  42. sunstone_data_platform-1.0.0/src/sunstone_data/metadata/__init__.py +41 -0
  43. sunstone_data_platform-1.0.0/src/sunstone_data/metadata/builder.py +276 -0
  44. sunstone_data_platform-1.0.0/src/sunstone_data/metadata/models.py +87 -0
  45. sunstone_data_platform-1.0.0/src/sunstone_data/metadata/provenance.py +171 -0
  46. sunstone_data_platform-1.0.0/src/sunstone_data/metadata/provenance_rdf.py +102 -0
  47. sunstone_data_platform-1.0.0/src/sunstone_data/plugin.py +62 -0
  48. sunstone_data_platform-1.0.0/src/sunstone_data/publish.py +134 -0
  49. sunstone_data_platform-1.0.0/src/sunstone_data/query.py +624 -0
  50. sunstone_data_platform-1.0.0/src/sunstone_data/resolve.py +178 -0
  51. sunstone_data_platform-1.0.0/src/sunstone_data/url.py +78 -0
  52. sunstone_data_platform-1.0.0/src/sunstone_data/utils/__init__.py +12 -0
  53. sunstone_data_platform-1.0.0/src/sunstone_data/utils/checksum.py +14 -0
  54. sunstone_data_platform-1.0.0/src/sunstone_data/utils/encoding.py +16 -0
  55. sunstone_data_platform-1.0.0/src/sunstone_data/utils/naming.py +38 -0
  56. sunstone_data_platform-1.0.0/src/sunstone_data/versioning/__init__.py +17 -0
  57. sunstone_data_platform-1.0.0/src/sunstone_data/versioning/schema_evolution.py +180 -0
  58. sunstone_data_platform-1.0.0/src/sunstone_pipeline/__init__.py +1 -0
  59. sunstone_data_platform-1.0.0/src/sunstone_pipeline/assets/__init__.py +11 -0
  60. sunstone_data_platform-1.0.0/src/sunstone_pipeline/assets/mirrors.py +123 -0
  61. sunstone_data_platform-1.0.0/src/sunstone_pipeline/assets/transforms.py +74 -0
  62. sunstone_data_platform-1.0.0/src/sunstone_pipeline/definitions.py +25 -0
  63. sunstone_data_platform-1.0.0/src/sunstone_pipeline/io_manager.py +56 -0
  64. sunstone_data_platform-1.0.0/src/sunstone_pipeline/resources/__init__.py +6 -0
  65. sunstone_data_platform-1.0.0/src/sunstone_pipeline/resources/iceberg.py +29 -0
  66. sunstone_data_platform-1.0.0/src/sunstone_pipeline/resources/refs.py +88 -0
@@ -0,0 +1,22 @@
1
+ __marimo__
2
+ __pycache__
3
+ *.egg-info/
4
+ .env
5
+
6
+ # Generated by hatch-vcs at build time
7
+ src/sunstone_data/_version.py
8
+
9
+ # Agent/planning/private config (not for public tree)
10
+ .agents/
11
+ .planning/
12
+ old-stuff/
13
+
14
+ # Security prompts
15
+ security-prompts/
16
+ .DS_Store
17
+ .claude/*.local.*
18
+ .worktrees
19
+ /TODO.md
20
+ /research-stack
21
+ /target/
22
+ CLAUDE.local.md
@@ -0,0 +1,302 @@
1
+ Metadata-Version: 2.5
2
+ Name: sunstone-data-platform
3
+ Version: 1.0.0
4
+ Summary: Sunstone Data Platform - Data lakehouse with semantic metadata
5
+ Author-email: Sunstone Institute <stig@sunstone.institute>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: charset-normalizer>=3.4.0
9
+ Requires-Dist: duckdb>=1.5.3
10
+ Requires-Dist: fastexcel>=0.20.2
11
+ Requires-Dist: httpx>=0.27.0
12
+ Requires-Dist: pyiceberg[duckdb,gcsfs,s3fs]>=0.10.0
13
+ Requires-Dist: python-dateutil>=2.9.0.post0
14
+ Requires-Dist: python-dotenv>=1.2.2
15
+ Requires-Dist: python-slugify>=8.0.0
16
+ Requires-Dist: rich>=14.0.0
17
+ Requires-Dist: sunstone-py>=1.15.0
18
+ Requires-Dist: typer>=0.27.1
19
+ Provides-Extra: pipelines
20
+ Requires-Dist: dagster-docker>=0.29.0; extra == 'pipelines'
21
+ Requires-Dist: dagster-postgres>=0.29.0; extra == 'pipelines'
22
+ Requires-Dist: dagster-webserver>=1.13.0; extra == 'pipelines'
23
+ Requires-Dist: dagster>=1.13.9; extra == 'pipelines'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Sunstone Data Platform
27
+
28
+ A data lakehouse for importing, versioning, and querying external datasets with full provenance tracking. Built on Apache Iceberg, iceberg-catalog, RustFS, and DuckDB.
29
+
30
+ Data scientists can import CSV/Excel files from local paths or URLs, query them via DuckDB with git-like version references, and track lineage through notebook sessions. All provenance is captured using W3C PROV-DM so metadata can later materialize as a knowledge graph.
31
+
32
+ ## Architecture
33
+
34
+ ```
35
+ ┌──────────────┐
36
+ │ sunstone │
37
+ │ CLI (Typer) │
38
+ └──────┬───────┘
39
+ │
40
+ ┌────────────────┼────────────────┐
41
+ │ │ │
42
+ ┌────────▼───────┐ ┌─────▼──────┐ ┌───────▼────────┐
43
+ │ Import Pipeline │ │ Query │ │ Versioning │
44
+ │ fetch → parse → │ │ (DuckDB) │ │ branches/tags/ │
45
+ │ store → metadata│ │ │ │ history/diff │
46
+ └────────┬────────┘ └─────┬──────┘ └───────┬────────┘
47
+ │ │ │
48
+ └────────────────┼────────────────┘
49
+ │
50
+ ┌──────────▼──────────┐
51
+ │ iceberg-catalog │
52
+ │ (Iceberg REST API) │
53
+ └──────────┬──────────┘
54
+ │
55
+ ┌──────────▼──────────┐
56
+ │ RustFS (S3) │
57
+ │ Iceberg tables + │
58
+ │ original files │
59
+ └─────────────────────┘
60
+ ```
61
+
62
+ **Stack**: Python 3.11+ | PyIceberg | DuckDB | iceberg-catalog | RustFS | PostgreSQL 16
63
+
64
+ ## Endpoints
65
+
66
+ - `/catalog` — DCAT JSON-LD view of the lakehouse, served by graph-server views
67
+ (see [the views design](docs/specs/2026-08-02-graph-server-views-design.md))
68
+ - `/ui/` — the graph explorer, a 3D view of the data graph, served by
69
+ graph-server (see [docs/graph-explorer.md](docs/graph-explorer.md))
70
+
71
+ ## Local Development Setup
72
+
73
+ ### Prerequisites
74
+
75
+ - Python 3.11+
76
+ - [uv](https://docs.astral.sh/uv/) package manager
77
+ - Docker and Docker Compose
78
+
79
+ ### 1. Clone and install
80
+
81
+ ```bash
82
+ git clone <repo-url>
83
+ cd data-platform
84
+ uv sync
85
+ ```
86
+
87
+ ### 2. Configure environment
88
+
89
+ ```bash
90
+ cp .env.example .env
91
+ # Edit .env if you want to change default credentials
92
+ ```
93
+
94
+ ### 3. Start the infrastructure
95
+
96
+ ```bash
97
+ docker compose --profile s3 up -d
98
+ ```
99
+
100
+ This starts the graph stack (Postgres, `graph-migrate`, `oxigraph`,
101
+ `graph-server`, `materializer` — see
102
+ [docs/architecture.md](docs/architecture.md#infrastructure)) plus:
103
+ - **iceberg-catalog** on port 8181 (Iceberg REST catalog, S3-backed)
104
+ - **object-store** (RustFS) on port 9000 (S3-compatible storage, console on 9001)
105
+
106
+ For GCS testing, use `docker compose --profile gcs up -d` instead, which
107
+ starts `fake-gcs-server` — note there's no GCS-backed iceberg-catalog wired
108
+ up yet, so this profile provides the emulator but not a working GCS
109
+ catalog.
110
+
111
+ ### Cloudflare R2 backend
112
+
113
+ The `r2` profile hasn't moved to the graph stack yet — it still runs Nessie
114
+ directly, against a real Cloudflare R2 bucket (no local object store):
115
+
116
+ 1. Create an R2 bucket in the Cloudflare dashboard.
117
+ 2. Create an R2 API token (R2 > Manage R2 API Tokens) with **Object Read & Write** scoped to that bucket. Copy the Access Key ID and Secret Access Key.
118
+ 3. Copy `.env.r2.example` values into your `.env` and fill in `R2_ACCOUNT_ID`, `R2_BUCKET`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`.
119
+ 4. If your Postgres volume already exists from an earlier `s3`/`gcs` run, create the R2 version-store database once (the init script in `docker/init-databases.sql` only runs on a *fresh* `iceberg-db` volume):
120
+
121
+ ```bash
122
+ docker compose up -d postgres
123
+ docker compose exec postgres \
124
+ psql -U iceberg -d iceberg \
125
+ -c "CREATE DATABASE nessie_r2; GRANT ALL PRIVILEGES ON DATABASE nessie_r2 TO iceberg;"
126
+ ```
127
+
128
+ (A fresh stack — or `docker compose down -v && docker compose --profile r2 up -d` — creates it automatically. The reset destroys all local data.)
129
+ 5. Start the R2 profile:
130
+
131
+ ```bash
132
+ docker compose --profile r2 up -d
133
+ ```
134
+
135
+ Nessie listens on port 19122. Point your client at `http://localhost:19122`.
136
+
137
+ R2-specific Nessie settings the profile sets for you:
138
+
139
+ - `region=auto` (R2 ignores region but the SDK requires a value).
140
+ - `endpoint=https://<R2_ACCOUNT_ID>.r2.cloudflarestorage.com` (account-style host).
141
+ - `path-style-access=true` so requests are `<endpoint>/<bucket>/<key>` — R2 accepts both styles but path-style avoids virtual-host DNS issues with some older clients.
142
+
143
+ The local PyIceberg/DuckDB client reads Iceberg data files straight from R2, so it needs the same bucket and credentials. It derives those from the same four `R2_*` vars automatically — warehouse (`s3://<R2_BUCKET>/`), endpoint, access keys, `region=auto`, and a default `CATALOG_URI` of `http://localhost:19122`. No separate `S3_*`/`ICEBERG_WAREHOUSE` settings are required; if you set them explicitly they take precedence.
144
+
145
+ ### 4. Verify the stack
146
+
147
+ ```bash
148
+ uv run scripts/smoke_test.py
149
+ ```
150
+
151
+ ### 5. Set up your environment
152
+
153
+ The `sunstone` CLI comes from sunstone-py. data-platform registers as a plugin and adds the `data` subcommand group:
154
+
155
+ ```bash
156
+ # Add your local Docker stack as an environment
157
+ sunstone env add local --catalog-url http://localhost:19120 --s3-endpoint http://localhost:9000
158
+ sunstone env use local
159
+ ```
160
+
161
+ ### 6. Try it out
162
+
163
+ ```bash
164
+ # Import a local CSV
165
+ sunstone data import path/to/data.csv
166
+
167
+ # Import from a URL
168
+ sunstone data import https://example.com/dataset.csv
169
+
170
+ # List datasets
171
+ sunstone data info
172
+
173
+ # View dataset metadata
174
+ sunstone data info namespace.table_name
175
+
176
+ # Query history
177
+ sunstone data history namespace.table_name
178
+
179
+ # Compare versions
180
+ sunstone data diff namespace.table_name @v1 @v2
181
+ ```
182
+
183
+ ## Running Tests
184
+
185
+ ```bash
186
+ # Unit tests (fast, no Docker required)
187
+ uv run pytest tests/unit/
188
+
189
+ # Integration tests (requires Docker stack running)
190
+ uv run pytest tests/integration/
191
+
192
+ # All tests
193
+ uv run pytest
194
+
195
+ # Specific test file with verbose output
196
+ uv run pytest tests/unit/test_query.py -v
197
+ ```
198
+
199
+ ## CLI Commands
200
+
201
+ | Command | Description |
202
+ |---------|-------------|
203
+ | `sunstone data import <source>` | Import CSV/Excel from path or URL |
204
+ | `sunstone data info [table]` | View metadata for one or all datasets |
205
+ | `sunstone data history <table>` | Show snapshot history (git-log style) |
206
+ | `sunstone data tag <table> <tag>` | Tag a snapshot version |
207
+ | `sunstone data untag <table> <tag>` | Remove a tag |
208
+ | `sunstone data branches` | List branches and tags |
209
+ | `sunstone data diff <table> <from> [to]` | Compare two versions |
210
+ | `sunstone data lineage <table>` | Show provenance and lineage |
211
+ | `sunstone data original <table>` | Download the original imported source file |
212
+
213
+ All commands support `--branch` for working on version branches and `--json` for machine-readable output. Run `sunstone data <command> --help` for full options.
214
+
215
+ ## Key Concepts
216
+
217
+ - **Table references** use git-like syntax: `namespace.table@branch`, `@v1.0`, `@2026-01-25`, `@snapshot_id`
218
+ - **Deduplication**: Imports are checksum-based (SHA256). Re-importing the same file is a no-op.
219
+ - **Schema evolution**: Safe type promotions (e.g. int32 to int64) are allowed automatically. Unsafe changes require `--force-schema`.
220
+ - **Provenance**: Every import captures source metadata, schema info, and W3C PROV-DM provenance as Iceberg table properties.
221
+ - **Original files**: Source files are stored content-addressable in S3 alongside the Iceberg data.
222
+
223
+ ## sunstone: URLs
224
+
225
+ Datasets in the lakehouse can be referenced as URLs anywhere sunstone-py accepts a data source:
226
+
227
+ ```python
228
+ import sunstone
229
+
230
+ df = sunstone.read("sunstone:namespace/table@v1.0")
231
+ ```
232
+
233
+ Supports the full ref syntax: `@branch`, `@tag`, `@2026-01-25`, `@snapshot_id`.
234
+
235
+ ## Project Structure
236
+
237
+ ```
238
+ src/sunstone_data/
239
+ ├── plugin.py # sunstone-py plugin (CLIProvider + URLHandler)
240
+ ├── url.py # sunstone: URL parser
241
+ ├── config.py # Settings from DataEnvironment or .env
242
+ ├── catalog.py # PyIceberg RestCatalog connection to the Iceberg REST catalog
243
+ ├── query.py # DuckDB QuerySession with time-travel support
244
+ ├── lineage.py # Lineage in Iceberg table properties
245
+ ├── cli/ # Typer CLI commands + Rich formatters
246
+ ├── import_/ # Pipeline: fetch → parse → store → metadata
247
+ ├── metadata/ # Frozen dataclasses, MetadataBuilder, W3C PROV-DM
248
+ ├── versioning/ # Version branches/tags, refs, schema evolution, diff
249
+ └── utils/ # Checksums, encoding detection, naming
250
+ ```
251
+
252
+ ## Documentation
253
+
254
+ - [Getting Started](docs/getting-started.md) - Detailed setup instructions
255
+ - [Architecture](docs/architecture.md) - System design and data flow
256
+ - [CLI Reference](docs/cli-reference.md) - Complete command documentation
257
+ - [Import Pipeline](docs/import-pipeline.md) - How imports work
258
+ - [Development Guide](docs/development.md) - Contributing and testing
259
+
260
+ ## Resetting the Stack
261
+
262
+ To wipe all data and start fresh:
263
+
264
+ ```bash
265
+ docker compose --profile s3 --profile gcs down -v
266
+ docker compose --profile s3 up -d
267
+ ```
268
+
269
+ ## License
270
+
271
+ MIT
272
+
273
+ ## Graph server authentication
274
+
275
+ `graph-server` requires a Keycloak bearer token on every route except
276
+ `/healthz`/`/readyz`. Access is role-based: the client roles in your token
277
+ determine which environment you can use. See
278
+ `docs/specs/2026-07-05-graph-server-auth-design.md`.
279
+
280
+ No hostnames or client ids ship in this package. Each environment's
281
+ `graph_url` comes from config: Sunstone-managed machines get
282
+ `/etc/sunstone/data_platform.toml` from the `fleet-config` repo, and other
283
+ machines set `GRAPH_URL` or add an environment with `sunstone env add`.
284
+ graph-server publishes the OIDC issuer and public client id at
285
+ `$GRAPH_URL/ui/config.json`; the CLI reads them from there.
286
+
287
+ ### Humans (CLI, local agents)
288
+
289
+ ```bash
290
+ sunstone env init # cache discovery for configured environments
291
+ sunstone data login # browser sign-in (authorization code + PKCE)
292
+ ```
293
+
294
+ A browser window opens for Keycloak SSO; the refresh token renews silently
295
+ until the SSO session expires.
296
+
297
+ ### Services (CI, automation)
298
+
299
+ Set `SUNSTONE_CLIENT_ID` and `SUNSTONE_CLIENT_SECRET` (or `client_id` and
300
+ `client_secret` in the environment's `data-platform` section). The client then
301
+ uses the OAuth2 client-credentials flow against the discovered issuer instead
302
+ of a cached human login.
@@ -0,0 +1,277 @@
1
+ # Sunstone Data Platform
2
+
3
+ A data lakehouse for importing, versioning, and querying external datasets with full provenance tracking. Built on Apache Iceberg, iceberg-catalog, RustFS, and DuckDB.
4
+
5
+ Data scientists can import CSV/Excel files from local paths or URLs, query them via DuckDB with git-like version references, and track lineage through notebook sessions. All provenance is captured using W3C PROV-DM so metadata can later materialize as a knowledge graph.
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ ┌──────────────┐
11
+ │ sunstone │
12
+ │ CLI (Typer) │
13
+ └──────┬───────┘
14
+ │
15
+ ┌────────────────┼────────────────┐
16
+ │ │ │
17
+ ┌────────▼───────┐ ┌─────▼──────┐ ┌───────▼────────┐
18
+ │ Import Pipeline │ │ Query │ │ Versioning │
19
+ │ fetch → parse → │ │ (DuckDB) │ │ branches/tags/ │
20
+ │ store → metadata│ │ │ │ history/diff │
21
+ └────────┬────────┘ └─────┬──────┘ └───────┬────────┘
22
+ │ │ │
23
+ └────────────────┼────────────────┘
24
+ │
25
+ ┌──────────▼──────────┐
26
+ │ iceberg-catalog │
27
+ │ (Iceberg REST API) │
28
+ └──────────┬──────────┘
29
+ │
30
+ ┌──────────▼──────────┐
31
+ │ RustFS (S3) │
32
+ │ Iceberg tables + │
33
+ │ original files │
34
+ └─────────────────────┘
35
+ ```
36
+
37
+ **Stack**: Python 3.11+ | PyIceberg | DuckDB | iceberg-catalog | RustFS | PostgreSQL 16
38
+
39
+ ## Endpoints
40
+
41
+ - `/catalog` — DCAT JSON-LD view of the lakehouse, served by graph-server views
42
+ (see [the views design](docs/specs/2026-08-02-graph-server-views-design.md))
43
+ - `/ui/` — the graph explorer, a 3D view of the data graph, served by
44
+ graph-server (see [docs/graph-explorer.md](docs/graph-explorer.md))
45
+
46
+ ## Local Development Setup
47
+
48
+ ### Prerequisites
49
+
50
+ - Python 3.11+
51
+ - [uv](https://docs.astral.sh/uv/) package manager
52
+ - Docker and Docker Compose
53
+
54
+ ### 1. Clone and install
55
+
56
+ ```bash
57
+ git clone <repo-url>
58
+ cd data-platform
59
+ uv sync
60
+ ```
61
+
62
+ ### 2. Configure environment
63
+
64
+ ```bash
65
+ cp .env.example .env
66
+ # Edit .env if you want to change default credentials
67
+ ```
68
+
69
+ ### 3. Start the infrastructure
70
+
71
+ ```bash
72
+ docker compose --profile s3 up -d
73
+ ```
74
+
75
+ This starts the graph stack (Postgres, `graph-migrate`, `oxigraph`,
76
+ `graph-server`, `materializer` — see
77
+ [docs/architecture.md](docs/architecture.md#infrastructure)) plus:
78
+ - **iceberg-catalog** on port 8181 (Iceberg REST catalog, S3-backed)
79
+ - **object-store** (RustFS) on port 9000 (S3-compatible storage, console on 9001)
80
+
81
+ For GCS testing, use `docker compose --profile gcs up -d` instead, which
82
+ starts `fake-gcs-server` — note there's no GCS-backed iceberg-catalog wired
83
+ up yet, so this profile provides the emulator but not a working GCS
84
+ catalog.
85
+
86
+ ### Cloudflare R2 backend
87
+
88
+ The `r2` profile hasn't moved to the graph stack yet — it still runs Nessie
89
+ directly, against a real Cloudflare R2 bucket (no local object store):
90
+
91
+ 1. Create an R2 bucket in the Cloudflare dashboard.
92
+ 2. Create an R2 API token (R2 > Manage R2 API Tokens) with **Object Read & Write** scoped to that bucket. Copy the Access Key ID and Secret Access Key.
93
+ 3. Copy `.env.r2.example` values into your `.env` and fill in `R2_ACCOUNT_ID`, `R2_BUCKET`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`.
94
+ 4. If your Postgres volume already exists from an earlier `s3`/`gcs` run, create the R2 version-store database once (the init script in `docker/init-databases.sql` only runs on a *fresh* `iceberg-db` volume):
95
+
96
+ ```bash
97
+ docker compose up -d postgres
98
+ docker compose exec postgres \
99
+ psql -U iceberg -d iceberg \
100
+ -c "CREATE DATABASE nessie_r2; GRANT ALL PRIVILEGES ON DATABASE nessie_r2 TO iceberg;"
101
+ ```
102
+
103
+ (A fresh stack — or `docker compose down -v && docker compose --profile r2 up -d` — creates it automatically. The reset destroys all local data.)
104
+ 5. Start the R2 profile:
105
+
106
+ ```bash
107
+ docker compose --profile r2 up -d
108
+ ```
109
+
110
+ Nessie listens on port 19122. Point your client at `http://localhost:19122`.
111
+
112
+ R2-specific Nessie settings the profile sets for you:
113
+
114
+ - `region=auto` (R2 ignores region but the SDK requires a value).
115
+ - `endpoint=https://<R2_ACCOUNT_ID>.r2.cloudflarestorage.com` (account-style host).
116
+ - `path-style-access=true` so requests are `<endpoint>/<bucket>/<key>` — R2 accepts both styles but path-style avoids virtual-host DNS issues with some older clients.
117
+
118
+ The local PyIceberg/DuckDB client reads Iceberg data files straight from R2, so it needs the same bucket and credentials. It derives those from the same four `R2_*` vars automatically — warehouse (`s3://<R2_BUCKET>/`), endpoint, access keys, `region=auto`, and a default `CATALOG_URI` of `http://localhost:19122`. No separate `S3_*`/`ICEBERG_WAREHOUSE` settings are required; if you set them explicitly they take precedence.
119
+
120
+ ### 4. Verify the stack
121
+
122
+ ```bash
123
+ uv run scripts/smoke_test.py
124
+ ```
125
+
126
+ ### 5. Set up your environment
127
+
128
+ The `sunstone` CLI comes from sunstone-py. data-platform registers as a plugin and adds the `data` subcommand group:
129
+
130
+ ```bash
131
+ # Add your local Docker stack as an environment
132
+ sunstone env add local --catalog-url http://localhost:19120 --s3-endpoint http://localhost:9000
133
+ sunstone env use local
134
+ ```
135
+
136
+ ### 6. Try it out
137
+
138
+ ```bash
139
+ # Import a local CSV
140
+ sunstone data import path/to/data.csv
141
+
142
+ # Import from a URL
143
+ sunstone data import https://example.com/dataset.csv
144
+
145
+ # List datasets
146
+ sunstone data info
147
+
148
+ # View dataset metadata
149
+ sunstone data info namespace.table_name
150
+
151
+ # Query history
152
+ sunstone data history namespace.table_name
153
+
154
+ # Compare versions
155
+ sunstone data diff namespace.table_name @v1 @v2
156
+ ```
157
+
158
+ ## Running Tests
159
+
160
+ ```bash
161
+ # Unit tests (fast, no Docker required)
162
+ uv run pytest tests/unit/
163
+
164
+ # Integration tests (requires Docker stack running)
165
+ uv run pytest tests/integration/
166
+
167
+ # All tests
168
+ uv run pytest
169
+
170
+ # Specific test file with verbose output
171
+ uv run pytest tests/unit/test_query.py -v
172
+ ```
173
+
174
+ ## CLI Commands
175
+
176
+ | Command | Description |
177
+ |---------|-------------|
178
+ | `sunstone data import <source>` | Import CSV/Excel from path or URL |
179
+ | `sunstone data info [table]` | View metadata for one or all datasets |
180
+ | `sunstone data history <table>` | Show snapshot history (git-log style) |
181
+ | `sunstone data tag <table> <tag>` | Tag a snapshot version |
182
+ | `sunstone data untag <table> <tag>` | Remove a tag |
183
+ | `sunstone data branches` | List branches and tags |
184
+ | `sunstone data diff <table> <from> [to]` | Compare two versions |
185
+ | `sunstone data lineage <table>` | Show provenance and lineage |
186
+ | `sunstone data original <table>` | Download the original imported source file |
187
+
188
+ All commands support `--branch` for working on version branches and `--json` for machine-readable output. Run `sunstone data <command> --help` for full options.
189
+
190
+ ## Key Concepts
191
+
192
+ - **Table references** use git-like syntax: `namespace.table@branch`, `@v1.0`, `@2026-01-25`, `@snapshot_id`
193
+ - **Deduplication**: Imports are checksum-based (SHA256). Re-importing the same file is a no-op.
194
+ - **Schema evolution**: Safe type promotions (e.g. int32 to int64) are allowed automatically. Unsafe changes require `--force-schema`.
195
+ - **Provenance**: Every import captures source metadata, schema info, and W3C PROV-DM provenance as Iceberg table properties.
196
+ - **Original files**: Source files are stored content-addressable in S3 alongside the Iceberg data.
197
+
198
+ ## sunstone: URLs
199
+
200
+ Datasets in the lakehouse can be referenced as URLs anywhere sunstone-py accepts a data source:
201
+
202
+ ```python
203
+ import sunstone
204
+
205
+ df = sunstone.read("sunstone:namespace/table@v1.0")
206
+ ```
207
+
208
+ Supports the full ref syntax: `@branch`, `@tag`, `@2026-01-25`, `@snapshot_id`.
209
+
210
+ ## Project Structure
211
+
212
+ ```
213
+ src/sunstone_data/
214
+ ├── plugin.py # sunstone-py plugin (CLIProvider + URLHandler)
215
+ ├── url.py # sunstone: URL parser
216
+ ├── config.py # Settings from DataEnvironment or .env
217
+ ├── catalog.py # PyIceberg RestCatalog connection to the Iceberg REST catalog
218
+ ├── query.py # DuckDB QuerySession with time-travel support
219
+ ├── lineage.py # Lineage in Iceberg table properties
220
+ ├── cli/ # Typer CLI commands + Rich formatters
221
+ ├── import_/ # Pipeline: fetch → parse → store → metadata
222
+ ├── metadata/ # Frozen dataclasses, MetadataBuilder, W3C PROV-DM
223
+ ├── versioning/ # Version branches/tags, refs, schema evolution, diff
224
+ └── utils/ # Checksums, encoding detection, naming
225
+ ```
226
+
227
+ ## Documentation
228
+
229
+ - [Getting Started](docs/getting-started.md) - Detailed setup instructions
230
+ - [Architecture](docs/architecture.md) - System design and data flow
231
+ - [CLI Reference](docs/cli-reference.md) - Complete command documentation
232
+ - [Import Pipeline](docs/import-pipeline.md) - How imports work
233
+ - [Development Guide](docs/development.md) - Contributing and testing
234
+
235
+ ## Resetting the Stack
236
+
237
+ To wipe all data and start fresh:
238
+
239
+ ```bash
240
+ docker compose --profile s3 --profile gcs down -v
241
+ docker compose --profile s3 up -d
242
+ ```
243
+
244
+ ## License
245
+
246
+ MIT
247
+
248
+ ## Graph server authentication
249
+
250
+ `graph-server` requires a Keycloak bearer token on every route except
251
+ `/healthz`/`/readyz`. Access is role-based: the client roles in your token
252
+ determine which environment you can use. See
253
+ `docs/specs/2026-07-05-graph-server-auth-design.md`.
254
+
255
+ No hostnames or client ids ship in this package. Each environment's
256
+ `graph_url` comes from config: Sunstone-managed machines get
257
+ `/etc/sunstone/data_platform.toml` from the `fleet-config` repo, and other
258
+ machines set `GRAPH_URL` or add an environment with `sunstone env add`.
259
+ graph-server publishes the OIDC issuer and public client id at
260
+ `$GRAPH_URL/ui/config.json`; the CLI reads them from there.
261
+
262
+ ### Humans (CLI, local agents)
263
+
264
+ ```bash
265
+ sunstone env init # cache discovery for configured environments
266
+ sunstone data login # browser sign-in (authorization code + PKCE)
267
+ ```
268
+
269
+ A browser window opens for Keycloak SSO; the refresh token renews silently
270
+ until the SSO session expires.
271
+
272
+ ### Services (CI, automation)
273
+
274
+ Set `SUNSTONE_CLIENT_ID` and `SUNSTONE_CLIENT_SECRET` (or `client_id` and
275
+ `client_secret` in the environment's `data-platform` section). The client then
276
+ uses the OAuth2 client-credentials flow against the discovered issuer instead
277
+ of a cached human login.
@@ -0,0 +1,89 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sunstone-data-platform"
7
+ dynamic = ["version"]
8
+ description = "Sunstone Data Platform - Data lakehouse with semantic metadata"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Sunstone Institute", email = "stig@sunstone.institute" }
14
+ ]
15
+ dependencies = [
16
+ "sunstone-py>=1.15.0",
17
+ "pyiceberg[s3fs,gcsfs,duckdb]>=0.10.0",
18
+ "duckdb>=1.5.3",
19
+ "python-dotenv>=1.2.2",
20
+ "typer>=0.27.1",
21
+ "rich>=14.0.0",
22
+ "charset-normalizer>=3.4.0",
23
+ "httpx>=0.27.0",
24
+ "fastexcel>=0.20.2",
25
+ "python-slugify>=8.0.0",
26
+ "python-dateutil>=2.9.0.post0",
27
+ ]
28
+
29
+ [project.entry-points."sunstone.plugins"]
30
+ data_platform = "sunstone_data.plugin:SunstoneDataPlugin"
31
+
32
+ [project.optional-dependencies]
33
+ pipelines = [
34
+ "dagster>=1.13.9",
35
+ "dagster-webserver>=1.13.0",
36
+ "dagster-docker>=0.29.0",
37
+ "dagster-postgres>=0.29.0",
38
+ ]
39
+
40
+ [tool.hatch.version]
41
+ source = "vcs"
42
+
43
+ [tool.hatch.build.hooks.vcs]
44
+ version-file = "src/sunstone_data/_version.py"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/sunstone_data", "src/sunstone_pipeline"]
48
+
49
+ # The sdist is published to PyPI. Limit it to what the wheel is built from,
50
+ # so deploy manifests, workflows and runbooks stay out of it.
51
+ [tool.hatch.build.targets.sdist]
52
+ include = ["/src/sunstone_data", "/src/sunstone_pipeline", "/README.md"]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+ markers = [
57
+ "integration: marks tests as integration tests (require Docker stack)",
58
+ ]
59
+ filterwarnings = [
60
+ "ignore::pyparsing.exceptions.PyparsingDeprecationWarning",
61
+ "ignore::pydantic.warnings.PydanticDeprecatedSince20",
62
+ "ignore::pydantic.warnings.PydanticDeprecatedSince212",
63
+ # gcsfs imports moved google.cloud.storage._experimental modules — fixed upstream,
64
+ # check if this filter can be removed after upgrading gcsfs past 2025.3.2
65
+ "ignore:google\\.cloud\\.storage\\._experimental:DeprecationWarning:gcsfs",
66
+ ]
67
+
68
+ [tool.ruff]
69
+ line-length = 120
70
+
71
+ [tool.ruff.lint]
72
+ select = ["E", "F", "I"]
73
+ ignore = ["E501"]
74
+
75
+ [dependency-groups]
76
+ dev = [
77
+ "ruff>=0.15.17",
78
+ "frictionless>=5.19.0",
79
+ "gitpython>=3.1.58",
80
+ "openpyxl>=3.1.5",
81
+ "pandas>=3.0.5",
82
+ "pre-commit>=4.6.0",
83
+ "pytest>=9.0.3",
84
+ "pytest-asyncio>=0.24.0",
85
+ "pytest-cov>=4.0.0",
86
+ "pyyaml>=6.0.3",
87
+ "ruamel-yaml>=0.19.1",
88
+ "tomlkit>=0.15.1",
89
+ ]