sunstone-data-platform 1.3.0__tar.gz → 1.4.1__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 (73) hide show
  1. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/PKG-INFO +7 -6
  2. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/README.md +4 -4
  3. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/pyproject.toml +5 -1
  4. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/_version.py +2 -2
  5. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/auth/stepup.py +4 -2
  6. sunstone_data_platform-1.4.1/src/sunstone_data/branch.py +66 -0
  7. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/commands/__init__.py +1 -0
  8. sunstone_data_platform-1.4.1/src/sunstone_data/cli/commands/conceptmap.py +81 -0
  9. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/commands/data_import.py +11 -3
  10. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/commands/data_publish.py +10 -1
  11. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/commands/refs.py +48 -1
  12. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/commands/show.py +9 -2
  13. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/commands/snapshots.py +10 -2
  14. sunstone_data_platform-1.4.1/src/sunstone_data/conceptmap.py +233 -0
  15. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/config.py +15 -0
  16. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/graph/client.py +157 -8
  17. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/plugin.py +20 -3
  18. sunstone_data_platform-1.4.1/src/sunstone_data/push.py +134 -0
  19. sunstone_data_platform-1.4.1/src/sunstone_data/queries/conceptmap-mappings.rq +41 -0
  20. sunstone_data_platform-1.4.1/src/sunstone_data/sssom.py +267 -0
  21. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_pipeline/assets/mirrors.py +1 -1
  22. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/.gitignore +0 -0
  23. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/__init__.py +0 -0
  24. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/auth/__init__.py +0 -0
  25. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/auth/client_creds.py +0 -0
  26. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/auth/pkce.py +0 -0
  27. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/auth/store.py +0 -0
  28. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/catalog.py +0 -0
  29. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/__init__.py +0 -0
  30. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/_app.py +0 -0
  31. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/_errors.py +0 -0
  32. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/commands/data_auth.py +0 -0
  33. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/commands/data_info.py +0 -0
  34. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/commands/ls.py +0 -0
  35. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/commands/prov.py +0 -0
  36. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/commands/sparql.py +0 -0
  37. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/cli/main.py +0 -0
  38. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/discovery.py +0 -0
  39. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/env_commands.py +0 -0
  40. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/env_section.py +0 -0
  41. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/graph/__init__.py +0 -0
  42. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/graph/dataset.py +0 -0
  43. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/graph/terms.py +0 -0
  44. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/identifiers.py +0 -0
  45. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/import_/__init__.py +0 -0
  46. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/import_/fetcher.py +0 -0
  47. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/import_/owner.py +0 -0
  48. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/import_/parser.py +0 -0
  49. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/import_/pipeline.py +0 -0
  50. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/import_/storage.py +0 -0
  51. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/metadata/__init__.py +0 -0
  52. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/metadata/builder.py +0 -0
  53. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/metadata/models.py +0 -0
  54. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/metadata/provenance.py +0 -0
  55. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/metadata/provenance_rdf.py +0 -0
  56. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/publish.py +0 -0
  57. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/query.py +0 -0
  58. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/resolve.py +0 -0
  59. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/url.py +0 -0
  60. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/utils/__init__.py +0 -0
  61. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/utils/checksum.py +0 -0
  62. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/utils/encoding.py +0 -0
  63. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/utils/naming.py +0 -0
  64. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/versioning/__init__.py +0 -0
  65. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_data/versioning/schema_evolution.py +0 -0
  66. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_pipeline/__init__.py +0 -0
  67. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_pipeline/assets/__init__.py +0 -0
  68. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_pipeline/assets/transforms.py +0 -0
  69. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_pipeline/definitions.py +0 -0
  70. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_pipeline/io_manager.py +0 -0
  71. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_pipeline/resources/__init__.py +0 -0
  72. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_pipeline/resources/iceberg.py +0 -0
  73. {sunstone_data_platform-1.3.0 → sunstone_data_platform-1.4.1}/src/sunstone_pipeline/resources/refs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sunstone-data-platform
3
- Version: 1.3.0
3
+ Version: 1.4.1
4
4
  Summary: Sunstone Data Platform - Data lakehouse with semantic metadata
5
5
  Author-email: Sunstone Institute <stig@sunstone.institute>
6
6
  License-Expression: MIT
@@ -15,8 +15,9 @@ Requires-Dist: pyiceberg[duckdb,gcsfs,s3fs]>=0.10.0
15
15
  Requires-Dist: python-dateutil>=2.9.0.post0
16
16
  Requires-Dist: python-dotenv>=1.2.2
17
17
  Requires-Dist: python-slugify>=8.0.0
18
+ Requires-Dist: rdflib>=7.6
18
19
  Requires-Dist: rich>=14.0.0
19
- Requires-Dist: sunstone-py>=1.15.0
20
+ Requires-Dist: sunstone-py>=1.19.0
20
21
  Requires-Dist: typer>=0.27.1
21
22
  Provides-Extra: pipelines
22
23
  Requires-Dist: dagster-docker>=0.29.0; extra == 'pipelines'
@@ -62,7 +63,7 @@ full data flow.
62
63
  ## Endpoints
63
64
 
64
65
  - `/catalog` — DCAT JSON-LD view of the lakehouse, served by graph-server views
65
- (see [the views design](docs/specs/2026-08-02-graph-server-views-design.md))
66
+ (see [DP-SPEC-13](https://worklode.dev.sunstoneinstitute.ai/DP-SPEC-13))
66
67
  - `/ui/` — the data explorer, a 3D view of the data graph, served by
67
68
  graph-server (see [docs/data-explorer.md](docs/data-explorer.md))
68
69
 
@@ -264,8 +265,8 @@ src/sunstone_data/
264
265
  ## Documentation
265
266
 
266
267
  - [docs/README.md](docs/README.md) - Index of all documentation
267
- - [docs/architecture.md](docs/architecture.md) - System overview and data flow
268
- - [docs/specs2/](docs/specs2/) - Current design, one file per subsystem. The Python client and CLI are in [05-python-client.md](docs/specs2/05-python-client.md).
268
+ - [docs/architecture.md](docs/architecture.md) - System overview, data flow, and an index of the specs in Worklode. The Python client and CLI are in [DP-SPEC-1](https://worklode.dev.sunstoneinstitute.ai/DP-SPEC-1).
269
+ - [docs/releasing.md](docs/releasing.md) - How to release the server and the client
269
270
 
270
271
  ## Resetting the Stack
271
272
 
@@ -285,7 +286,7 @@ MIT
285
286
  `graph-server` requires a Keycloak bearer token on every route except
286
287
  `/healthz`/`/readyz`. Access is role-based: the client roles in your token
287
288
  determine which environment you can use. See
288
- `docs/specs/2026-07-05-graph-server-auth-design.md`.
289
+ [DP-SPEC-14](https://worklode.dev.sunstoneinstitute.ai/DP-SPEC-14).
289
290
 
290
291
  No hostnames or client ids ship in this package. Each environment's
291
292
  `base_iri` comes from config: Sunstone-managed machines get
@@ -35,7 +35,7 @@ full data flow.
35
35
  ## Endpoints
36
36
 
37
37
  - `/catalog` — DCAT JSON-LD view of the lakehouse, served by graph-server views
38
- (see [the views design](docs/specs/2026-08-02-graph-server-views-design.md))
38
+ (see [DP-SPEC-13](https://worklode.dev.sunstoneinstitute.ai/DP-SPEC-13))
39
39
  - `/ui/` — the data explorer, a 3D view of the data graph, served by
40
40
  graph-server (see [docs/data-explorer.md](docs/data-explorer.md))
41
41
 
@@ -237,8 +237,8 @@ src/sunstone_data/
237
237
  ## Documentation
238
238
 
239
239
  - [docs/README.md](docs/README.md) - Index of all documentation
240
- - [docs/architecture.md](docs/architecture.md) - System overview and data flow
241
- - [docs/specs2/](docs/specs2/) - Current design, one file per subsystem. The Python client and CLI are in [05-python-client.md](docs/specs2/05-python-client.md).
240
+ - [docs/architecture.md](docs/architecture.md) - System overview, data flow, and an index of the specs in Worklode. The Python client and CLI are in [DP-SPEC-1](https://worklode.dev.sunstoneinstitute.ai/DP-SPEC-1).
241
+ - [docs/releasing.md](docs/releasing.md) - How to release the server and the client
242
242
 
243
243
  ## Resetting the Stack
244
244
 
@@ -258,7 +258,7 @@ MIT
258
258
  `graph-server` requires a Keycloak bearer token on every route except
259
259
  `/healthz`/`/readyz`. Access is role-based: the client roles in your token
260
260
  determine which environment you can use. See
261
- `docs/specs/2026-07-05-graph-server-auth-design.md`.
261
+ [DP-SPEC-14](https://worklode.dev.sunstoneinstitute.ai/DP-SPEC-14).
262
262
 
263
263
  No hostnames or client ids ship in this package. Each environment's
264
264
  `base_iri` comes from config: Sunstone-managed machines get
@@ -13,7 +13,7 @@ authors = [
13
13
  { name = "Sunstone Institute", email = "stig@sunstone.institute" }
14
14
  ]
15
15
  dependencies = [
16
- "sunstone-py>=1.15.0",
16
+ "sunstone-py>=1.19.0",
17
17
  "pyiceberg[s3fs,gcsfs,duckdb]>=0.10.0",
18
18
  "duckdb>=1.5.3",
19
19
  "python-dotenv>=1.2.2",
@@ -26,6 +26,7 @@ dependencies = [
26
26
  "python-dateutil>=2.9.0.post0",
27
27
  "publicsuffixlist>=1.0.2.20261003",
28
28
  "keyring>=25",
29
+ "rdflib>=7.6",
29
30
  ]
30
31
 
31
32
  [project.entry-points."sunstone.plugins"]
@@ -41,6 +42,9 @@ pipelines = [
41
42
 
42
43
  [tool.hatch.version]
43
44
  source = "vcs"
45
+ # Only `v*` tags are client versions. Without --match, setuptools-scm takes the
46
+ # nearest tag of any kind and strips a `server-` prefix (DP-REQ-391).
47
+ raw-options = { git_describe_command = "git describe --dirty --tags --long --match v[0-9]*" }
44
48
 
45
49
  [tool.hatch.build.hooks.vcs]
46
50
  version-file = "src/sunstone_data/_version.py"
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.3.0'
22
- __version_tuple__ = version_tuple = (1, 3, 0)
21
+ __version__ = version = '1.4.1'
22
+ __version_tuple__ = version_tuple = (1, 4, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -83,10 +83,12 @@ def step_up(
83
83
  """
84
84
  settings = get_settings()
85
85
  if os.environ.get("SUNSTONE_ACCESS_TOKEN", "").strip() or (settings.client_id and settings.client_secret):
86
- raise StepUpRequired("writes to main need a passkey sign-in; a service credential cannot step up")
86
+ raise StepUpRequired("writes to a protected ref need a passkey sign-in; a service credential cannot step up")
87
87
  session = store.read_session(env_name)
88
88
  if session is None:
89
- raise StepUpRequired(f"writes to main need a passkey sign-in; run `sunstone auth login` for {env_name!r}")
89
+ raise StepUpRequired(
90
+ f"writes to a protected ref need a passkey sign-in; run `sunstone auth login` for {env_name!r}"
91
+ )
90
92
  issuer = session.issuer or discovered_issuer(env_name, settings.graph_url)
91
93
  authorization_endpoint = discover_authorization_endpoint(issuer, client)
92
94
  token_endpoint = discover_token_endpoint(issuer, client)
@@ -0,0 +1,66 @@
1
+ """Default data branch: the current git branch (or CI ref), normalized (DP-SPEC-12 section 12)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import subprocess
8
+ from collections.abc import Mapping
9
+ from pathlib import Path
10
+
11
+ _MAX_LENGTH = 128
12
+
13
+
14
+ class NoDataBranchError(ValueError):
15
+ """No data branch could be derived; the caller must pass ``--branch``."""
16
+
17
+
18
+ def normalize_branch_name(name: str) -> str:
19
+ """Turn a git branch name into a valid data branch name.
20
+
21
+ In order: characters outside ``[A-Za-z0-9_.-]`` become ``-``, runs of
22
+ ``-`` collapse to one, a name not starting with a letter or ``_`` gets a
23
+ ``_`` prefix, and the result is cut to 128 characters.
24
+ """
25
+ name = re.sub(r"[^A-Za-z0-9_.-]", "-", name)
26
+ name = re.sub(r"-{2,}", "-", name)
27
+ if not re.match(r"[A-Za-z_]", name):
28
+ name = "_" + name
29
+ return name[:_MAX_LENGTH]
30
+
31
+
32
+ def _git_branch(cwd: Path | None) -> str | None:
33
+ """The checked-out git branch, or None (not a checkout, git missing, detached HEAD)."""
34
+ try:
35
+ result = subprocess.run(
36
+ ["git", "symbolic-ref", "--short", "-q", "HEAD"],
37
+ cwd=cwd,
38
+ capture_output=True,
39
+ text=True,
40
+ timeout=10,
41
+ check=False,
42
+ )
43
+ except (OSError, subprocess.SubprocessError):
44
+ return None
45
+ name = result.stdout.strip()
46
+ return name if result.returncode == 0 and name else None
47
+
48
+
49
+ def current_data_branch(cwd: Path | None = None, env: Mapping[str, str] | None = None) -> str:
50
+ """Return the data branch for ``cwd``, normalized.
51
+
52
+ Order: the checked-out git branch (also on a branch with no commits yet),
53
+ then ``GITHUB_HEAD_REF``, then ``GITHUB_REF_NAME``. CI checks out pull
54
+ requests on a detached HEAD, which is why the variables come second.
55
+
56
+ Raises:
57
+ NoDataBranchError: None of these gives a branch. Never falls back to ``main``.
58
+ """
59
+ env = os.environ if env is None else env
60
+ name = _git_branch(cwd) or env.get("GITHUB_HEAD_REF") or env.get("GITHUB_REF_NAME")
61
+ if not name:
62
+ raise NoDataBranchError(
63
+ "Cannot tell which data branch to use: not on a git branch and GITHUB_HEAD_REF / "
64
+ "GITHUB_REF_NAME are unset. Pass --branch."
65
+ )
66
+ return normalize_branch_name(name)
@@ -9,6 +9,7 @@ point can register the whole CLI surface in well under 100ms.
9
9
  """
10
10
 
11
11
  from sunstone_data.cli.commands import ( # noqa: F401 (import side-effect: registers commands)
12
+ conceptmap,
12
13
  data_auth,
13
14
  data_import,
14
15
  data_info,
@@ -0,0 +1,81 @@
1
+ """``sunstone data conceptmap create|update|sssom``: push a map-concepts scope directory, read its mappings (DP-REQ-323)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from dataclasses import asdict
8
+ from pathlib import Path
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
+ conceptmap_app = typer.Typer(help="Push concept maps from map-concepts scope directories", no_args_is_help=True)
16
+ data_app.add_typer(conceptmap_app, name="conceptmap")
17
+
18
+ _SCOPE_DIR = typer.Argument(
19
+ ..., help="Scope directory written by map-concepts (reads scheme/*.ttl and scheme/mappings.sssom.tsv)"
20
+ )
21
+ # Concept maps are published content, so the default is main, not the git
22
+ # branch of the directory the command runs in (DP-REQ-30).
23
+ _BRANCH = typer.Option("main", "--branch", "-b", help="Branch to write")
24
+ _BRANCH_READ = typer.Option("main", "--branch", "-b", help="Branch to read")
25
+ _JSON = typer.Option(False, "--json", help="Output as JSON")
26
+
27
+
28
+ def _report(result, json_output: bool) -> None:
29
+ from urllib.parse import quote
30
+
31
+ from sunstone_data.config import get_settings
32
+
33
+ link = f"{get_settings().graph_url}/ui/#visualise?graph={quote(result.graph_iri, safe='')}"
34
+ if json_output:
35
+ print(json.dumps({**asdict(result), "url": link}))
36
+ return
37
+ console.print(
38
+ f"Concept map [cyan]{result.conceptmap}[/cyan]: {result.graph_iri} on {result.branch}, "
39
+ f"{result.triples} triples (ETag {result.etag})"
40
+ )
41
+ console.print(link)
42
+
43
+
44
+ @conceptmap_app.command("create")
45
+ def create_cmd(scope_dir: Path = _SCOPE_DIR, branch: str = _BRANCH, json_output: bool = _JSON) -> None:
46
+ """Allocate a new concept map and push the scope directory to it."""
47
+ with cli_errors(json_output):
48
+ from sunstone_data import conceptmap
49
+
50
+ _report(conceptmap.create(scope_dir, branch), json_output)
51
+
52
+
53
+ @conceptmap_app.command("update")
54
+ def update_cmd(
55
+ n: int = typer.Argument(..., help="Concept map id, the <n> in /conceptmap/<n>"),
56
+ scope_dir: Path = _SCOPE_DIR,
57
+ branch: str = _BRANCH,
58
+ json_output: bool = _JSON,
59
+ ) -> None:
60
+ """Replace an existing concept map with the scope directory."""
61
+ with cli_errors(json_output):
62
+ from sunstone_data import conceptmap
63
+
64
+ _report(conceptmap.update(n, scope_dir, branch), json_output)
65
+
66
+
67
+ @conceptmap_app.command("sssom")
68
+ def sssom_cmd(
69
+ n: int = typer.Argument(..., help="Concept map id, the <n> in /conceptmap/<n>"),
70
+ branch: str = _BRANCH_READ,
71
+ output: Path | None = typer.Option(None, "--output", "-o", help="File to write (default: stdout)"),
72
+ ) -> None:
73
+ """Write a concept map's mappings as an SSSOM TSV file."""
74
+ with cli_errors(False):
75
+ from sunstone_data import conceptmap
76
+
77
+ text = conceptmap.sssom(n, branch)
78
+ if output is None:
79
+ sys.stdout.write(text)
80
+ else:
81
+ output.write_text(text, encoding="utf-8")
@@ -49,8 +49,7 @@ def _format_result(result, as_json: bool = False, branch: str = "main") -> None:
49
49
  console.print(f" Snapshot: {result.snapshot_id}")
50
50
  console.print(f" Dataset: [cyan]{iri}[/cyan]")
51
51
  console.print(f" Activity: [cyan]{activity}[/cyan]")
52
- if branch != "main":
53
- console.print(f" Branch: [cyan]{branch}[/cyan]")
52
+ console.print(f" Branch: [cyan]{branch}[/cyan]")
54
53
 
55
54
 
56
55
  @data_app.command("import")
@@ -71,7 +70,12 @@ def import_cmd(
71
70
  help="Publisher organization path, e.g. un/who. Overrides the source-URL rules file. Does not set the namespace.",
72
71
  ),
73
72
  tag: list[str] | None = typer.Option(None, "--tag", "-t", help="Add tags (key:value)"),
74
- branch: str = typer.Option("main", "--branch", "-b", help="Branch to import to"),
73
+ branch: str | None = typer.Option(
74
+ None,
75
+ "--branch",
76
+ "-b",
77
+ help="Branch to import to (default: the normalized git branch, else the CI ref; fails if neither)",
78
+ ),
75
79
  force_schema: bool = typer.Option(False, "--force-schema", help="Allow unsafe schema changes"),
76
80
  json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
77
81
  ) -> None:
@@ -86,11 +90,15 @@ def import_cmd(
86
90
  TransferSpeedColumn,
87
91
  )
88
92
 
93
+ from sunstone_data.branch import current_data_branch
89
94
  from sunstone_data.import_ import run_import
90
95
 
91
96
  is_url = source.startswith(("http://", "https://"))
92
97
 
93
98
  try:
99
+ if branch is None:
100
+ branch = current_data_branch()
101
+
94
102
  if sys.stdout.isatty() and not json_output:
95
103
  with Progress(
96
104
  SpinnerColumn(),
@@ -10,7 +10,12 @@ from sunstone_data.cli._app import console, data_app
10
10
  @data_app.command("publish")
11
11
  def publish_cmd(
12
12
  table: str = typer.Argument(..., help="Table identifier (namespace.table)"),
13
- branch: str = typer.Option("main", "--branch", "-b", help="Branch to publish on"),
13
+ branch: str | None = typer.Option(
14
+ None,
15
+ "--branch",
16
+ "-b",
17
+ help="Branch to publish on (default: the normalized git branch, else the CI ref; fails if neither)",
18
+ ),
14
19
  version: str | None = typer.Option(
15
20
  None, "--version", help="Publish the table's current snapshot under this version label, e.g. v1.0"
16
21
  ),
@@ -27,6 +32,7 @@ def publish_cmd(
27
32
 
28
33
  from sunstone.lineage import Metadata
29
34
 
35
+ from sunstone_data.branch import current_data_branch
30
36
  from sunstone_data.catalog import get_catalog
31
37
  from sunstone_data.graph.dataset import DatasetDescription, DatasetVersion
32
38
  from sunstone_data.publish import publish
@@ -34,6 +40,9 @@ def publish_cmd(
34
40
  from sunstone_data.resolve import dataset_iri
35
41
 
36
42
  try:
43
+ if branch is None:
44
+ branch = current_data_branch()
45
+
37
46
  ref = TableRef.parse(table)
38
47
  identifier = f"{ref.namespace}.{ref.table}"
39
48
  versions: tuple[DatasetVersion, ...] = ()
@@ -1,4 +1,4 @@
1
- """graph-server refs: ``branches``, ``branch create|delete``, ``tags``,
1
+ """graph-server refs: ``branches``, ``branch create|delete|merge``, ``tags``,
2
2
  ``tag create|delete`` (spec 180 §8).
3
3
 
4
4
  Branch and tag names go into a URL path, so they are validated here before
@@ -93,6 +93,53 @@ def branch_delete_cmd(
93
93
  console.print(f"[green]Deleted branch[/green] {name}")
94
94
 
95
95
 
96
+ @branch_app.command("merge")
97
+ def branch_merge_cmd(
98
+ name: str = typer.Argument(..., help="Name of the branch to merge into main"),
99
+ strategy: str = typer.Option(
100
+ None,
101
+ "--strategy",
102
+ help="How to settle conflicts: report (default), branch-wins or main-wins",
103
+ ),
104
+ take_branch: list[str] = typer.Option(
105
+ [], "--take-branch", help="Graph IRI whose conflict takes the branch side (repeatable)"
106
+ ),
107
+ take_main: list[str] = typer.Option(
108
+ [], "--take-main", help="Graph IRI whose conflict keeps main's side (repeatable)"
109
+ ),
110
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
111
+ ) -> None:
112
+ """Merge a branch into main. Conflicts are reported unless resolved.
113
+
114
+ Exits 2 when conflicts remain, so CI knows to keep the branch.
115
+ """
116
+ with cli_errors(json_output):
117
+ from sunstone_data.identifiers import validate_ref_name
118
+
119
+ validate_ref_name(name, "branch")
120
+ if strategy is not None and strategy not in ("report", "branch-wins", "main-wins"):
121
+ raise ValueError(f"--strategy must be report, branch-wins or main-wins, not {strategy!r}")
122
+ resolve = [{"graph": g, "take": "branch"} for g in take_branch]
123
+ resolve += [{"graph": g, "take": "main"} for g in take_main]
124
+ result = _client().merge_branch(name, strategy, resolve or None)
125
+
126
+ merged, conflicts = result.get("merged", []), result.get("conflicts", [])
127
+ if json_output:
128
+ print(json.dumps(result, indent=2))
129
+ if conflicts:
130
+ raise typer.Exit(2)
131
+ return
132
+ color = "yellow" if conflicts else "green"
133
+ console.print(f"[{color}]Merged[/{color}] {name} into main: {len(merged)} merged, {len(conflicts)} in conflict")
134
+ for e in merged:
135
+ note = f" (took {e['resolution']})" if e.get("resolution") else ""
136
+ console.print(f" merged {e.get('table') or e['graph']}{note}")
137
+ for e in conflicts:
138
+ console.print(f" [red]conflict[/red] {e.get('table') or e['graph']}")
139
+ if conflicts:
140
+ raise typer.Exit(2)
141
+
142
+
96
143
  @data_app.command("tags")
97
144
  def tags_cmd(
98
145
  json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
@@ -20,7 +20,12 @@ FORMATS = ("table", "turtle", "jsonld")
20
20
  def show_cmd(
21
21
  table: str = typer.Argument(..., help="Table reference (namespace.table[@branch])"),
22
22
  output_format: str = typer.Option("table", "--format", "-f", help=f"One of: {', '.join(FORMATS)}"),
23
- branch: str = typer.Option("main", "--branch", "-b", help="Branch to read the graph from"),
23
+ branch: str | None = typer.Option(
24
+ None,
25
+ "--branch",
26
+ "-b",
27
+ help="Branch to read the graph from (default: the normalized git branch, else the CI ref; fails if neither)",
28
+ ),
24
29
  json_output: bool = typer.Option(False, "--json", help="Output as JSON-LD (same as --format jsonld)"),
25
30
  ) -> None:
26
31
  """Show a dataset's metadata graph.
@@ -35,6 +40,7 @@ def show_cmd(
35
40
  if chosen not in FORMATS:
36
41
  raise ValueError(f"Unknown format {output_format!r}. Use one of: {', '.join(FORMATS)}")
37
42
 
43
+ from sunstone_data.branch import current_data_branch
38
44
  from sunstone_data.graph.client import N_TRIPLES, get_graph_client
39
45
  from sunstone_data.query import TableRef
40
46
  from sunstone_data.resolve import dataset_iri
@@ -44,7 +50,7 @@ def show_cmd(
44
50
  client = get_graph_client()
45
51
  # A ref after `@` names a graph-server branch here: the graph is
46
52
  # per-branch, and versions/snapshots are inside it, not selectors on it.
47
- source_branch = ref.version_ref or branch
53
+ source_branch = ref.version_ref or branch or current_data_branch()
48
54
 
49
55
  if chosen == "turtle":
50
56
  print(client.get_graph(source_branch, iri))
@@ -58,6 +64,7 @@ def show_cmd(
58
64
  print(json.dumps(to_jsonld(graph), indent=2))
59
65
  return
60
66
 
67
+ console.print(f"Branch: [cyan]{source_branch}[/cyan]")
61
68
  _print_table(graph, iri)
62
69
 
63
70
 
@@ -16,7 +16,12 @@ from sunstone_data.cli._errors import cli_errors
16
16
  @data_app.command("history")
17
17
  def history_cmd(
18
18
  table: str = typer.Argument(..., help="Table reference (namespace.table[@branch])"),
19
- branch: str = typer.Option("main", "--branch", "-b", help="Catalog branch to read"),
19
+ branch: str | None = typer.Option(
20
+ None,
21
+ "--branch",
22
+ "-b",
23
+ help="Catalog branch to read (default: the normalized git branch, else the CI ref; fails if neither)",
24
+ ),
20
25
  json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
21
26
  ) -> None:
22
27
  """Show a table's Iceberg snapshot log, oldest snapshot first.
@@ -28,11 +33,13 @@ def history_cmd(
28
33
  with cli_errors(json_output):
29
34
  from datetime import datetime, timezone
30
35
 
36
+ from sunstone_data.branch import current_data_branch
31
37
  from sunstone_data.catalog import get_catalog
32
38
  from sunstone_data.query import TableRef
33
39
 
34
40
  ref = TableRef.parse(table)
35
- iceberg_table = get_catalog(ref.version_ref or branch).load_table(f"{ref.namespace}.{ref.table}")
41
+ source_branch = ref.version_ref or branch or current_data_branch()
42
+ iceberg_table = get_catalog(source_branch).load_table(f"{ref.namespace}.{ref.table}")
36
43
 
37
44
  rows = []
38
45
  for entry in iceberg_table.history():
@@ -54,6 +61,7 @@ def history_cmd(
54
61
  print(json.dumps(rows, indent=2))
55
62
  return
56
63
 
64
+ console.print(f"Branch: [cyan]{source_branch}[/cyan]")
57
65
  if not rows:
58
66
  console.print("[yellow]No snapshots found[/yellow]")
59
67
  return