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.
- sunstone_data_platform-1.0.0/.gitignore +22 -0
- sunstone_data_platform-1.0.0/PKG-INFO +302 -0
- sunstone_data_platform-1.0.0/README.md +277 -0
- sunstone_data_platform-1.0.0/pyproject.toml +89 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/__init__.py +36 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/_version.py +24 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/auth/__init__.py +43 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/auth/client_creds.py +239 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/auth/pkce.py +307 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/auth/store.py +97 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/catalog.py +175 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/__init__.py +5 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/_app.py +26 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/_errors.py +31 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/__init__.py +22 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/data_auth.py +120 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/data_import.py +157 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/data_info.py +166 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/data_publish.py +69 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/ls.py +49 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/prov.py +58 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/refs.py +142 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/show.py +139 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/snapshots.py +77 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/commands/sparql.py +68 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/cli/main.py +30 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/config.py +250 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/discovery.py +317 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/env_commands.py +97 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/env_section.py +86 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/graph/__init__.py +36 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/graph/client.py +328 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/graph/dataset.py +438 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/graph/terms.py +34 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/identifiers.py +155 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/import_/__init__.py +28 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/import_/fetcher.py +259 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/import_/owner.py +130 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/import_/parser.py +492 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/import_/pipeline.py +275 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/import_/storage.py +135 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/metadata/__init__.py +41 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/metadata/builder.py +276 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/metadata/models.py +87 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/metadata/provenance.py +171 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/metadata/provenance_rdf.py +102 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/plugin.py +62 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/publish.py +134 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/query.py +624 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/resolve.py +178 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/url.py +78 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/utils/__init__.py +12 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/utils/checksum.py +14 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/utils/encoding.py +16 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/utils/naming.py +38 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/versioning/__init__.py +17 -0
- sunstone_data_platform-1.0.0/src/sunstone_data/versioning/schema_evolution.py +180 -0
- sunstone_data_platform-1.0.0/src/sunstone_pipeline/__init__.py +1 -0
- sunstone_data_platform-1.0.0/src/sunstone_pipeline/assets/__init__.py +11 -0
- sunstone_data_platform-1.0.0/src/sunstone_pipeline/assets/mirrors.py +123 -0
- sunstone_data_platform-1.0.0/src/sunstone_pipeline/assets/transforms.py +74 -0
- sunstone_data_platform-1.0.0/src/sunstone_pipeline/definitions.py +25 -0
- sunstone_data_platform-1.0.0/src/sunstone_pipeline/io_manager.py +56 -0
- sunstone_data_platform-1.0.0/src/sunstone_pipeline/resources/__init__.py +6 -0
- sunstone_data_platform-1.0.0/src/sunstone_pipeline/resources/iceberg.py +29 -0
- 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
|
+
]
|