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.
Files changed (66) hide show
  1. sunstone_data/__init__.py +36 -0
  2. sunstone_data/_version.py +24 -0
  3. sunstone_data/auth/__init__.py +43 -0
  4. sunstone_data/auth/client_creds.py +239 -0
  5. sunstone_data/auth/pkce.py +307 -0
  6. sunstone_data/auth/store.py +97 -0
  7. sunstone_data/catalog.py +175 -0
  8. sunstone_data/cli/__init__.py +5 -0
  9. sunstone_data/cli/_app.py +26 -0
  10. sunstone_data/cli/_errors.py +31 -0
  11. sunstone_data/cli/commands/__init__.py +22 -0
  12. sunstone_data/cli/commands/data_auth.py +120 -0
  13. sunstone_data/cli/commands/data_import.py +157 -0
  14. sunstone_data/cli/commands/data_info.py +166 -0
  15. sunstone_data/cli/commands/data_publish.py +69 -0
  16. sunstone_data/cli/commands/ls.py +49 -0
  17. sunstone_data/cli/commands/prov.py +58 -0
  18. sunstone_data/cli/commands/refs.py +142 -0
  19. sunstone_data/cli/commands/show.py +139 -0
  20. sunstone_data/cli/commands/snapshots.py +77 -0
  21. sunstone_data/cli/commands/sparql.py +68 -0
  22. sunstone_data/cli/main.py +30 -0
  23. sunstone_data/config.py +250 -0
  24. sunstone_data/discovery.py +317 -0
  25. sunstone_data/env_commands.py +97 -0
  26. sunstone_data/env_section.py +86 -0
  27. sunstone_data/graph/__init__.py +36 -0
  28. sunstone_data/graph/client.py +328 -0
  29. sunstone_data/graph/dataset.py +438 -0
  30. sunstone_data/graph/terms.py +34 -0
  31. sunstone_data/identifiers.py +155 -0
  32. sunstone_data/import_/__init__.py +28 -0
  33. sunstone_data/import_/fetcher.py +259 -0
  34. sunstone_data/import_/owner.py +130 -0
  35. sunstone_data/import_/parser.py +492 -0
  36. sunstone_data/import_/pipeline.py +275 -0
  37. sunstone_data/import_/storage.py +135 -0
  38. sunstone_data/metadata/__init__.py +41 -0
  39. sunstone_data/metadata/builder.py +276 -0
  40. sunstone_data/metadata/models.py +87 -0
  41. sunstone_data/metadata/provenance.py +171 -0
  42. sunstone_data/metadata/provenance_rdf.py +102 -0
  43. sunstone_data/plugin.py +62 -0
  44. sunstone_data/publish.py +134 -0
  45. sunstone_data/query.py +624 -0
  46. sunstone_data/resolve.py +178 -0
  47. sunstone_data/url.py +78 -0
  48. sunstone_data/utils/__init__.py +12 -0
  49. sunstone_data/utils/checksum.py +14 -0
  50. sunstone_data/utils/encoding.py +16 -0
  51. sunstone_data/utils/naming.py +38 -0
  52. sunstone_data/versioning/__init__.py +17 -0
  53. sunstone_data/versioning/schema_evolution.py +180 -0
  54. sunstone_data_platform-1.0.0.dist-info/METADATA +302 -0
  55. sunstone_data_platform-1.0.0.dist-info/RECORD +66 -0
  56. sunstone_data_platform-1.0.0.dist-info/WHEEL +4 -0
  57. sunstone_data_platform-1.0.0.dist-info/entry_points.txt +2 -0
  58. sunstone_pipeline/__init__.py +1 -0
  59. sunstone_pipeline/assets/__init__.py +11 -0
  60. sunstone_pipeline/assets/mirrors.py +123 -0
  61. sunstone_pipeline/assets/transforms.py +74 -0
  62. sunstone_pipeline/definitions.py +25 -0
  63. sunstone_pipeline/io_manager.py +56 -0
  64. sunstone_pipeline/resources/__init__.py +6 -0
  65. sunstone_pipeline/resources/iceberg.py +29 -0
  66. sunstone_pipeline/resources/refs.py +88 -0
@@ -0,0 +1,317 @@
1
+ """Discovery and caching of data-platform identity configuration.
2
+
3
+ graph-server publishes the identity settings a client needs at
4
+ ``/ui/config.json``. That endpoint needs no auth, so a user configures one
5
+ setting — the graph-server base URL — and the rest is discovered. From the
6
+ ``issuer``, standard OIDC discovery supplies the remaining OAuth2 endpoints.
7
+
8
+ Clients fetch this once and cache it locally with a TTL.
9
+
10
+ Payload (served by ``crates/graph-server/src/ui.rs``)::
11
+
12
+ {
13
+ "issuer": "https://auth.example.com/realms/sunstone",
14
+ "clientId": "example-cli",
15
+ "sparqlUrl": "/sparql",
16
+ "branch": "main",
17
+ "limits": {...}
18
+ }
19
+
20
+ There is no schema version to check. Unknown keys are ignored and missing
21
+ keys become ``None``, so the server can add fields without breaking clients.
22
+ ``sparqlUrl`` is served as a relative path and is resolved here against the
23
+ base URL, so callers always get an absolute URL.
24
+
25
+ This does *not* describe object storage. Warehouse location and S3/GCS
26
+ credentials are configured explicitly (env vars, ``.env``, or the
27
+ ``data-platform`` section) — see :mod:`sunstone_data.config`.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import logging
34
+ import os
35
+ import tempfile
36
+ import time
37
+ import urllib.error
38
+ import urllib.request
39
+ from dataclasses import asdict, dataclass, fields
40
+ from pathlib import Path
41
+ from typing import Any
42
+ from urllib.parse import urljoin, urlsplit, urlunsplit
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ CONFIG_PATH = "/ui/config.json"
47
+ DEFAULT_TTL_SECONDS = 16 * 3600
48
+ FETCH_TIMEOUT_SECONDS = 10.0
49
+
50
+ #: TOML subtable name owned by the data-platform EnvSectionProvider.
51
+ SECTION_NAME = "data-platform"
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class DiscoveredConfig:
56
+ """Identity config published by graph-server, with TTL bookkeeping."""
57
+
58
+ issuer: str | None = None
59
+ """OIDC issuer URL. Standard OIDC discovery on it supplies the rest."""
60
+
61
+ client_id: str | None = None
62
+ """OAuth2 client id the CLI authenticates as."""
63
+
64
+ sparql_url: str | None = None
65
+ """Absolute SPARQL endpoint URL, resolved against the graph-server base."""
66
+
67
+ fetched_at: float = 0.0
68
+ expires_at: float = 0.0
69
+ source_url: str = ""
70
+
71
+
72
+ def discovery_url(graph_url: str) -> str:
73
+ """Build the config URL for a graph-server base URL."""
74
+ parts = urlsplit(graph_url)
75
+ if not parts.scheme or not parts.netloc:
76
+ raise ValueError(f"Invalid graph-server URL: {graph_url!r}")
77
+ return urlunsplit((parts.scheme, parts.netloc, CONFIG_PATH, "", ""))
78
+
79
+
80
+ def _cache_dir() -> Path:
81
+ base = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
82
+ return Path(base) / "sunstone" / "data-platform"
83
+
84
+
85
+ def cache_path(env_name: str) -> Path:
86
+ """Per-env cache file path under XDG_CACHE_HOME (or ~/.cache)."""
87
+ return _cache_dir() / f"{env_name}.json"
88
+
89
+
90
+ def _ttl_seconds() -> int:
91
+ raw = os.environ.get("SUNSTONE_DATA_DISCOVERY_TTL_SECONDS")
92
+ if raw is None:
93
+ return DEFAULT_TTL_SECONDS
94
+ try:
95
+ return max(0, int(raw))
96
+ except ValueError:
97
+ logger.warning("Invalid SUNSTONE_DATA_DISCOVERY_TTL_SECONDS=%r; using default", raw)
98
+ return DEFAULT_TTL_SECONDS
99
+
100
+
101
+ def _parse_payload(raw: bytes, source_url: str) -> dict:
102
+ try:
103
+ payload = json.loads(raw)
104
+ except json.JSONDecodeError as e:
105
+ raise ValueError(f"Discovery response from {source_url} is not valid JSON: {e}") from e
106
+ if not isinstance(payload, dict):
107
+ raise ValueError(f"Discovery response from {source_url} is not a JSON object")
108
+ return payload
109
+
110
+
111
+ def fetch_discovery(graph_url: str, *, timeout: float = FETCH_TIMEOUT_SECONDS) -> DiscoveredConfig:
112
+ """Fetch and parse graph-server's config payload. Raises on any failure."""
113
+ url = discovery_url(graph_url)
114
+ request = urllib.request.Request(url, headers={"Accept": "application/json"})
115
+ try:
116
+ with urllib.request.urlopen(request, timeout=timeout) as resp:
117
+ raw = resp.read()
118
+ except urllib.error.HTTPError as e:
119
+ raise RuntimeError(f"Discovery HTTP {e.code} from {url}: {e.reason}") from e
120
+ except urllib.error.URLError as e:
121
+ raise RuntimeError(f"Discovery request to {url} failed: {e.reason}") from e
122
+ except TimeoutError as e:
123
+ raise RuntimeError(f"Discovery request to {url} timed out after {timeout}s") from e
124
+
125
+ payload = _parse_payload(raw, url)
126
+ sparql = payload.get("sparqlUrl") or None
127
+ now = time.time()
128
+ return DiscoveredConfig(
129
+ issuer=payload.get("issuer") or None,
130
+ client_id=payload.get("clientId") or None,
131
+ # Served relative (``/sparql``); urljoin leaves an absolute one alone.
132
+ sparql_url=urljoin(url, sparql) if sparql else None,
133
+ fetched_at=now,
134
+ expires_at=now + _ttl_seconds(),
135
+ source_url=url,
136
+ )
137
+
138
+
139
+ def _write_cache_atomic(path: Path, config: DiscoveredConfig) -> None:
140
+ path.parent.mkdir(parents=True, exist_ok=True)
141
+ fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
142
+ try:
143
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
144
+ json.dump(asdict(config), f, indent=2)
145
+ os.replace(tmp_name, path)
146
+ except Exception:
147
+ Path(tmp_name).unlink(missing_ok=True)
148
+ raise
149
+
150
+
151
+ def write_cache(env_name: str, config: DiscoveredConfig) -> Path:
152
+ """Persist a discovered config to the per-env cache file."""
153
+ path = cache_path(env_name)
154
+ _write_cache_atomic(path, config)
155
+ return path
156
+
157
+
158
+ def read_cache(env_name: str, *, ignore_expiry: bool = False) -> DiscoveredConfig | None:
159
+ """Load a cached discovered config. Returns None if missing or expired."""
160
+ path = cache_path(env_name)
161
+ try:
162
+ with open(path, encoding="utf-8") as f:
163
+ data = json.load(f)
164
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
165
+ return None
166
+ if not isinstance(data, dict):
167
+ return None
168
+ field_names = {f.name for f in fields(DiscoveredConfig)}
169
+ kwargs = {k: v for k, v in data.items() if k in field_names}
170
+ try:
171
+ config = DiscoveredConfig(**kwargs)
172
+ except TypeError:
173
+ return None
174
+ if not ignore_expiry and config.expires_at and time.time() >= config.expires_at:
175
+ return None
176
+ return config
177
+
178
+
179
+ def _discovery_disabled() -> bool:
180
+ return os.environ.get("SUNSTONE_DATA_DISABLE_DISCOVERY", "").strip().lower() in ("1", "true", "yes")
181
+
182
+
183
+ def get_discovered(
184
+ env_name: str,
185
+ graph_url: str,
186
+ *,
187
+ force_refresh: bool = False,
188
+ ) -> DiscoveredConfig | None:
189
+ """Return cached discovered config, fetching if absent, stale, or forced.
190
+
191
+ Behavior:
192
+ - ``SUNSTONE_DATA_DISABLE_DISCOVERY=1``: short-circuits to ``None`` even
193
+ when forced (use to skip discovery in tests or offline runs).
194
+ - Cache hit (fresh) and not forced: return cached.
195
+ - Forced or cache miss/stale: fetch. On success, cache and return.
196
+ - Forced + fetch fails: raise.
197
+ - Unforced + fetch fails: log warning, return stale cache if any
198
+ (with ignore_expiry), else None.
199
+ """
200
+ if _discovery_disabled():
201
+ return None
202
+
203
+ if not force_refresh:
204
+ cached = read_cache(env_name)
205
+ if cached is not None:
206
+ return cached
207
+
208
+ try:
209
+ config = fetch_discovery(graph_url)
210
+ except (RuntimeError, ValueError):
211
+ if force_refresh:
212
+ raise
213
+ stale = read_cache(env_name, ignore_expiry=True)
214
+ if stale is not None:
215
+ logger.warning("Discovery fetch failed; using stale cache for env %r", env_name)
216
+ return stale
217
+ logger.warning("Discovery fetch failed and no cache available for env %r", env_name)
218
+ return None
219
+
220
+ write_cache(env_name, config)
221
+ return config
222
+
223
+
224
+ def clear_cache(env_name: str | None = None) -> None:
225
+ """Remove cache files. With no arg, removes all."""
226
+ if env_name is None:
227
+ directory = _cache_dir()
228
+ if directory.is_dir():
229
+ for p in directory.glob("*.json"):
230
+ p.unlink(missing_ok=True)
231
+ else:
232
+ cache_path(env_name).unlink(missing_ok=True)
233
+
234
+
235
+ @dataclass(frozen=True)
236
+ class InitResult:
237
+ """Outcome of a single env in :func:`init_environments`."""
238
+
239
+ name: str
240
+ graph_url: str
241
+ refreshed: bool # True when discovery fetch succeeded
242
+ refresh_error: str | None # str on fetch failure, None otherwise
243
+
244
+
245
+ def _graph_url(env_def: dict[str, Any]) -> str | None:
246
+ """Return an env definition's graph_url.
247
+
248
+ sunstone-py 1.12+ keeps it in the ``data-platform`` subtable. Top-level
249
+ keys are still accepted for configs written by older versions or by hand.
250
+ """
251
+ section = env_def.get(SECTION_NAME) if isinstance(env_def.get(SECTION_NAME), dict) else {}
252
+ return section.get("graph_url") or env_def.get("graph_url")
253
+
254
+
255
+ def init_environments(
256
+ *,
257
+ user_config: Path | None = None,
258
+ system_config: Path | None = None,
259
+ project_config: Path | None = None,
260
+ ) -> list[InitResult]:
261
+ """Cache discovery for every configured environment that has a graph_url.
262
+
263
+ The package ships no environment URLs. Managed machines get them from the
264
+ system config (``/etc/sunstone/data_platform.toml``, delivered by Fleet
265
+ from the fleet-config repo); others declare them in user or project
266
+ config, or set ``GRAPH_URL``.
267
+ """
268
+ from sunstone.env import list_environments
269
+
270
+ envs = list_environments(
271
+ system_config=system_config,
272
+ user_config=user_config,
273
+ project_config=project_config,
274
+ )
275
+ results: list[InitResult] = []
276
+ for name, env_def in envs.items():
277
+ graph_url = _graph_url(env_def)
278
+ if not graph_url:
279
+ continue
280
+ refreshed = False
281
+ refresh_error: str | None = None
282
+ try:
283
+ if get_discovered(name, graph_url, force_refresh=True) is not None:
284
+ refreshed = True
285
+ except (RuntimeError, ValueError) as e:
286
+ refresh_error = str(e)
287
+ results.append(InitResult(name=name, graph_url=graph_url, refreshed=refreshed, refresh_error=refresh_error))
288
+ return results
289
+
290
+
291
+ def refresh_environment(
292
+ name: str,
293
+ *,
294
+ user_config: Path | None = None,
295
+ system_config: Path | None = None,
296
+ project_config: Path | None = None,
297
+ ) -> DiscoveredConfig:
298
+ """Force a discovery fetch and cache write for the named env.
299
+
300
+ Raises if the env is not defined, lacks a graph_url, or the fetch fails.
301
+ """
302
+ from sunstone.env import list_environments
303
+
304
+ envs = list_environments(
305
+ system_config=system_config,
306
+ user_config=user_config,
307
+ project_config=project_config,
308
+ )
309
+ if name not in envs:
310
+ raise ValueError(f"Environment '{name}' is not defined in any config file")
311
+ graph_url = _graph_url(envs[name])
312
+ if not graph_url:
313
+ raise ValueError(f"Environment '{name}' has no graph_url")
314
+ config = get_discovered(name, graph_url, force_refresh=True)
315
+ if config is None:
316
+ raise RuntimeError(f"Discovery for '{name}' returned no config")
317
+ return config
@@ -0,0 +1,97 @@
1
+ """Register data-platform-specific commands on sunstone-py's ``env`` CLI group.
2
+
3
+ Importing this module attaches ``init`` and ``refresh`` subcommands to
4
+ ``sunstone.cli.env_app``. The base sunstone-py library knows nothing about
5
+ catalogs or discovery; those concerns live here in sunstone-data.
6
+
7
+ The plugin module (:mod:`sunstone_data.plugin`) imports this at module load
8
+ so registration happens during entry-point discovery, before sunstone-py's
9
+ top-level CLI dispatches.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ _registered = False
19
+
20
+
21
+ def _register() -> None:
22
+ global _registered
23
+ if _registered:
24
+ return
25
+ # Set the flag before any work so we never re-enter, even if imports raise.
26
+ _registered = True
27
+ try:
28
+ import typer
29
+ from sunstone.cli import env_app
30
+ except Exception:
31
+ # sunstone-py CLI is not available in this process (e.g. library-only
32
+ # usage). Nothing to register; this is fine.
33
+ logger.debug("sunstone.cli.env_app not available; skipping env command registration", exc_info=True)
34
+ return
35
+
36
+ from sunstone_data.discovery import init_environments, refresh_environment
37
+
38
+ @env_app.command("init")
39
+ def env_init() -> None: # noqa: D401 — Typer command docstring becomes --help text
40
+ """Cache discovery for every configured environment.
41
+
42
+ Environments come from config files, not from this package. Managed
43
+ machines get them from /etc/sunstone/data_platform.toml.
44
+ """
45
+ try:
46
+ results = init_environments()
47
+ except (OSError, RuntimeError) as e:
48
+ typer.echo(f"Error: {e}", err=True)
49
+ raise typer.Exit(1)
50
+
51
+ if not results:
52
+ typer.echo(
53
+ "No environment with a graph_url is configured. Managed machines get one from "
54
+ "/etc/sunstone/data_platform.toml; otherwise add one with 'sunstone env add'.",
55
+ err=True,
56
+ )
57
+ raise typer.Exit(1)
58
+
59
+ any_refresh_failed = False
60
+ for r in results:
61
+ if r.refreshed:
62
+ typer.echo(f"Discovery cached for '{r.name}' ({r.graph_url})")
63
+ else:
64
+ any_refresh_failed = True
65
+ detail = f": {r.refresh_error}" if r.refresh_error else ""
66
+ typer.echo(f"Warning: discovery fetch failed for '{r.name}'{detail}", err=True)
67
+
68
+ if any_refresh_failed:
69
+ typer.echo(
70
+ "Re-run with 'sunstone env refresh <name>' once graph-server is reachable.",
71
+ err=True,
72
+ )
73
+
74
+ @env_app.command("refresh")
75
+ def env_refresh(
76
+ name: str = typer.Argument(..., help="Environment name to refresh"),
77
+ ) -> None:
78
+ """Force-fetch graph-server's config payload and update the cache."""
79
+ try:
80
+ config = refresh_environment(name)
81
+ except (OSError, RuntimeError, ValueError) as e:
82
+ typer.echo(f"Error: {e}", err=True)
83
+ raise typer.Exit(1)
84
+ typer.echo(f"Refreshed discovery for '{name}' from {config.source_url}")
85
+ if config.issuer:
86
+ typer.echo(f" issuer: {config.issuer}")
87
+ if config.client_id:
88
+ typer.echo(f" client id: {config.client_id}")
89
+ if config.sparql_url:
90
+ typer.echo(f" sparql: {config.sparql_url}")
91
+
92
+
93
+ # Register at module import so the env commands attach during entry-point
94
+ # discovery, before the top-level CLI dispatches the user's command. The
95
+ # idempotency guard above makes any later call (e.g. from
96
+ # ``SunstoneDataPlugin.cli_groups``) a safe no-op.
97
+ _register()
@@ -0,0 +1,86 @@
1
+ """Typed environment section schema for the data platform.
2
+
3
+ sunstone-py 1.12+ replaces the typed ``DataEnvironment`` with a generic
4
+ ``Environment(name, source, vars, sections)``. Plugins own typed slices of
5
+ the config via the ``EnvSectionProvider`` protocol: each plugin claims a
6
+ TOML subtable name and returns a callable that validates the subtable.
7
+
8
+ This module defines the ``data-platform`` section. A user config that
9
+ declares::
10
+
11
+ [environments.dev."data-platform"]
12
+ graph_url = "https://graph.example.com"
13
+ s3_endpoint = "http://localhost:9000"
14
+ s3_access_key = "rustfsadmin"
15
+ s3_secret_key = "rustfsadmin12345"
16
+
17
+ is resolved by sunstone-py into ``env.section("data-platform")``
18
+ returning a :class:`DataPlatformSection`.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from typing import Callable
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class DataPlatformSection:
29
+ """Typed data-platform config slice from the active environment.
30
+
31
+ All fields are optional; consumers should treat unset (``None``)
32
+ fields as "fall back to ``os.environ`` or a built-in default".
33
+
34
+ Extra unknown keys in the TOML subtable raise ``TypeError`` at
35
+ construction time, which sunstone-py wraps into a ``ValueError``
36
+ indicating which environment / section caused the failure.
37
+ """
38
+
39
+ graph_url: str | None = None
40
+ """graph-server base URL — the one setting a user has to configure.
41
+
42
+ ``GET {graph_url}/ui/config.json`` supplies the identity settings
43
+ (``issuer``, ``clientId``, ``sparqlUrl``); see
44
+ :mod:`sunstone_data.discovery`.
45
+ """
46
+
47
+ client_id: str | None = None
48
+ """OAuth2 client id for the client-credentials (service) auth flow.
49
+
50
+ Pair with ``client_secret``. Setting both makes the client authenticate
51
+ as a service (spec §4 Pattern B) instead of using a cached human login,
52
+ so CI is never silently authenticated as whoever last logged in.
53
+ """
54
+
55
+ client_secret: str | None = None
56
+ """OAuth2 client secret. Pair with ``client_id``."""
57
+
58
+ s3_endpoint: str | None = None
59
+ """S3-compatible endpoint URL (e.g. RustFS).
60
+
61
+ Object storage is not discoverable — graph-server publishes nothing
62
+ about it — but PyIceberg and DuckDB read data files straight from the
63
+ bucket, so these stay user-configurable.
64
+ """
65
+
66
+ s3_access_key: str | None = None
67
+ """S3 access key id. Pair with ``s3_secret_key``."""
68
+
69
+ s3_secret_key: str | None = None
70
+ """S3 secret access key. Pair with ``s3_access_key``."""
71
+
72
+
73
+ class DataPlatformSectionProvider:
74
+ """``EnvSectionProvider`` implementation for the data-platform plugin.
75
+
76
+ Registered via the ``sunstone.plugins`` entry point as part of
77
+ :class:`sunstone_data.plugin.SunstoneDataPlugin`.
78
+ """
79
+
80
+ SECTION_NAME = "data-platform"
81
+
82
+ def env_section_name(self) -> str:
83
+ return self.SECTION_NAME
84
+
85
+ def env_section_model(self) -> Callable[..., DataPlatformSection]:
86
+ return DataPlatformSection
@@ -0,0 +1,36 @@
1
+ """RDF graph vocabulary and canonical serialization.
2
+
3
+ `terms` holds the namespace IRIs for every RDF vocabulary this project uses.
4
+ `dataset` is the only place that turns a set of triples into bytes: sorted
5
+ N-Triples, with deterministic fragment IRIs standing in for blank nodes.
6
+ """
7
+
8
+ from sunstone_data.graph.dataset import (
9
+ IRI,
10
+ DatasetDescription,
11
+ DatasetVersion,
12
+ Graph,
13
+ Literal,
14
+ Triple,
15
+ build_graph,
16
+ description_to_graph,
17
+ fragment_iri,
18
+ graph_to_description,
19
+ parse_ntriples,
20
+ serialize,
21
+ )
22
+
23
+ __all__ = [
24
+ "IRI",
25
+ "DatasetDescription",
26
+ "DatasetVersion",
27
+ "Graph",
28
+ "Literal",
29
+ "Triple",
30
+ "build_graph",
31
+ "description_to_graph",
32
+ "fragment_iri",
33
+ "graph_to_description",
34
+ "parse_ntriples",
35
+ "serialize",
36
+ ]