dataspring-cli 0.4.3__py3-none-any.whl → 0.4.4__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.
- cli/_skills/dataspring-author/SKILL.md +101 -5
- cli/bundled_manifest.py +1 -1
- cli/contract.py +15 -4
- cli/generated.py +4 -3
- cli/main.py +162 -8
- {dataspring_cli-0.4.3.dist-info → dataspring_cli-0.4.4.dist-info}/METADATA +1 -1
- {dataspring_cli-0.4.3.dist-info → dataspring_cli-0.4.4.dist-info}/RECORD +9 -9
- {dataspring_cli-0.4.3.dist-info → dataspring_cli-0.4.4.dist-info}/WHEEL +0 -0
- {dataspring_cli-0.4.3.dist-info → dataspring_cli-0.4.4.dist-info}/entry_points.txt +0 -0
|
@@ -237,13 +237,14 @@ production.
|
|
|
237
237
|
```bash
|
|
238
238
|
dataspring datacore pull ./ws # deployed files, each exactly as pushed
|
|
239
239
|
dataspring datacore push ./ws # changed files only, into a new draft
|
|
240
|
+
dataspring datacore check --validate-only # seconds: parse + semantic validation, no build
|
|
240
241
|
dataspring datacore check # waits; exits 1 unless it passes
|
|
241
242
|
dataspring datacore deploy --then-build # admin; rebuilds changed models too
|
|
242
243
|
```
|
|
243
244
|
|
|
244
245
|
- **A new org** pulls its starting workspace (`datacore.yaml`, empty
|
|
245
|
-
`pipelines
|
|
246
|
-
|
|
246
|
+
`pipelines/` and `connections/`, and in `models/` a starting dbt project,
|
|
247
|
+
see 8.7).
|
|
247
248
|
- **A check** compiles every pipeline, builds the changed models and their
|
|
248
249
|
tests into `<org>_dev`, and dry-runs their SQL. It runs as an identity
|
|
249
250
|
that can write `<org>_dev` only: a project whose `generate_schema_name`
|
|
@@ -262,6 +263,12 @@ dataspring datacore deploy --then-build # admin; rebuilds changed models too
|
|
|
262
263
|
type_params: {numerator: gross_margin, denominator: revenue} # two metrics
|
|
263
264
|
```
|
|
264
265
|
|
|
266
|
+
- **Validate-only while you edit** (`datacore_run` `check` with `build:
|
|
267
|
+
false`; CLI `datacore check --validate-only`): pipelines compiled,
|
|
268
|
+
connections checked, `dbt parse`, the per-metric semantic validation and
|
|
269
|
+
the import's dry run, and no `dbt build`, so a broken metric or model is
|
|
270
|
+
reported in well under a minute and it never waits behind a check or a
|
|
271
|
+
load. It is never a green check: run a full `check` before `deploy`.
|
|
265
272
|
- **Deploy** needs a green check of that exact draft (any edit makes a new
|
|
266
273
|
draft) and starts a run of each windowed pipeline it changed.
|
|
267
274
|
|
|
@@ -328,10 +335,41 @@ whose bytes equal the stored one's.
|
|
|
328
335
|
|
|
329
336
|
Test an API's end bound with a one-day window before a backfill: a wrong
|
|
330
337
|
guess silently drops the last day of every chunk.
|
|
338
|
+
- **A window is one request** unless it has a `step`. For a source that caps
|
|
339
|
+
rows per request and does not page (e-regnskab: 20,000 item lines), a long
|
|
340
|
+
re-pull is cut into steps, one request each (`7d`, `2w`, `1M`; calendar
|
|
341
|
+
months, as backfill chunks are cut). `end: exclusive` holds per step, and
|
|
342
|
+
the scheduled window, a manual run and every backfill chunk all step:
|
|
343
|
+
|
|
344
|
+
```yaml
|
|
345
|
+
window: { lookback: 365d, step: 1M, param_start: createdFrom, param_end: createdTo, end: exclusive }
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
A rest resource only, and not a `resolve` child: step its parent.
|
|
349
|
+
- **`resolve` fans out over the parent's own window** (its lookback, or the
|
|
350
|
+
run's window), not the child's. When the child's rows lack the parent's
|
|
351
|
+
key, carry it: `include_from_parent: [hnInvoiceSalesID]` adds the column
|
|
352
|
+
`_<parent resource>_<field>` (`_invoices_hnInvoiceSalesID`) to every child
|
|
353
|
+
row; use it in the child's `primary_key` or rename it in `shape`.
|
|
354
|
+
`param` fills a `{param}` placeholder in the endpoint, or else is sent as
|
|
355
|
+
a query parameter.
|
|
356
|
+
- **An endpoint that answers a list of scalars** (`["budget", "forecast"]`)
|
|
357
|
+
loads one row per element in a column `value`; fan out over it with
|
|
358
|
+
`resolve: { field: value, ... }`.
|
|
331
359
|
- **A backfill** is `dataspring datacore run <pipeline> --start ... --end ...
|
|
360
|
+
--chunk 1M`: one child run per chunk, each started as the previous one
|
|
361
|
+
ends. The chunk is `<n>d`, `<n>w` or `<n>M` (`7d`, `2w`, `1M`, `3M` a
|
|
362
|
+
quarter, `12M` a year; no `Y`). A chunk before the last loads only the
|
|
363
|
+
windowed resources (and those they read rows from) and skips dbt; the last
|
|
364
|
+
chunk loads everything and builds the models once. A failed child fails
|
|
365
|
+
the backfill. One backfill per pipeline at a time.
|
|
366
|
+
- **One request per window.** A rest resource without a `paginator` sends
|
|
367
|
+
one request per window and nothing splits it, so a source that caps an
|
|
368
|
+
answer at N rows needs a `lookback`, and a `--chunk`, under N rows.
|
|
332
369
|
--chunk 1M` (or `3M`, `7d`, `2w`): one child run per chunk, each started as
|
|
333
370
|
the previous one ends. A chunk before the last loads only the windowed
|
|
334
|
-
resources
|
|
371
|
+
resources, their `resolve` children (fetched for that chunk's parent
|
|
372
|
+
rows) and those they read rows from, and skips dbt; the last chunk
|
|
335
373
|
loads everything and builds the models once. A failed child fails the
|
|
336
374
|
backfill. One backfill per pipeline at a time.
|
|
337
375
|
- **Stop** a queued or running run, or a whole backfill, with `dataspring
|
|
@@ -339,6 +377,63 @@ whose bytes equal the stored one's.
|
|
|
339
377
|
- **`reset`** drops a pipeline's raw tables and load state. It is
|
|
340
378
|
irreversible: ask first.
|
|
341
379
|
|
|
380
|
+
## 8.6 Writing a pipeline file
|
|
381
|
+
|
|
382
|
+
Do not read DataSpring's source for the grammar: it is served, cut from the
|
|
383
|
+
files the runner validates against.
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
dataspring datacore reference # the schemas, every connector: secrets, tables
|
|
387
|
+
dataspring datacore reference catalog kanpla # one connector, whole, with a file that uses it
|
|
388
|
+
dataspring datacore reference pipeline # the pipeline JSON schema (also: connection)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
MCP: `dataspring://datacore/reference[/<part>]` or `read(kind=
|
|
392
|
+
"datacore_reference", id=<part>)`; REST `GET /api/datacore/reference`.
|
|
393
|
+
Check the catalog first: a source with a connector is `connector: <name>`
|
|
394
|
+
plus `config` and `secrets`. Otherwise write an inline `source` (or
|
|
395
|
+
`sources`) and `resources`. The rules that bite:
|
|
396
|
+
|
|
397
|
+
- **Secrets are names**, `t-<org>-<connection>-<field>`, lowercase letters,
|
|
398
|
+
digits and hyphens only (no underscore). Set each with `dataspring secret
|
|
399
|
+
set <name> --stdin` before pushing the file that names it.
|
|
400
|
+
- **Windows** are whole inclusive days, `lookback` before today through
|
|
401
|
+
`lookahead` after, in the pipeline's timezone; declare `end: exclusive`
|
|
402
|
+
for an API whose end date is exclusive.
|
|
403
|
+
- **Loads**: `replace` is a full refresh; `merge` needs `primary_key` (or
|
|
404
|
+
`merge_key`); `window_scope: replace` deletes by the `partition` date, so
|
|
405
|
+
use it only when that is the date the API filters on.
|
|
406
|
+
- **Checks**: `min_rows` is refused on a `cursor` resource; use `freshness`.
|
|
407
|
+
- **A `resolve` child of a windowed parent** needs its own `window`, and the
|
|
408
|
+
parent its window in `params` (`{{ window.start }}`), or backfill chunks
|
|
409
|
+
skip the child.
|
|
410
|
+
|
|
411
|
+
The full guide with complete examples is "Writing a pipeline" in
|
|
412
|
+
`dataspring docs --full`.
|
|
413
|
+
|
|
414
|
+
## 8.7 The managed dbt project
|
|
415
|
+
|
|
416
|
+
The workspace's `models/` is a dbt project (dbt-core 1.12, BigQuery). A new
|
|
417
|
+
org starts with one that already keeps the contract; `dataspring datacore
|
|
418
|
+
pull` shows it. The contract:
|
|
419
|
+
|
|
420
|
+
- `profile: 'dataspring'` (the runner writes the profile; dataset
|
|
421
|
+
`<org>_marts`).
|
|
422
|
+
- Sources at `schema: "{{ env_var('DBT_RAW_SCHEMA') }}"` and `database:
|
|
423
|
+
"{{ env_var('DBT_GCP_PROJECT') }}"`, never a dataset name.
|
|
424
|
+
- `generate_schema_name` returns `DBT_SCHEMA_OVERRIDE` when set (a check:
|
|
425
|
+
`<org>_dev`), else `DBT_SCHEMA_PREFIX` + the layer (`+schema: staging` is
|
|
426
|
+
`<org>_staging`).
|
|
427
|
+
- A `metricflow_time_spine` model with `time_spine:` configured; include it
|
|
428
|
+
in `then.dbt_select`.
|
|
429
|
+
- Semantic models and metrics in YAML anywhere under `models/models/`;
|
|
430
|
+
`metric_edit` creates them in `models/models/semantic/metrics/`.
|
|
431
|
+
- `packages.yml` is fetched by `dbt deps` on every run (`dbt_utils`).
|
|
432
|
+
|
|
433
|
+
A deploy builds nothing: `deploy --then-build`, `datacore build`, or a
|
|
434
|
+
pipeline's `then.dbt_select` does. The full contract is "The managed dbt
|
|
435
|
+
project" in `dataspring docs --full`.
|
|
436
|
+
|
|
342
437
|
## 9. Parameter reference
|
|
343
438
|
|
|
344
439
|
<!-- dispatch-params: semantic_model_edit metric_edit board_edit briefing_edit import_manifest business_context_edit warehouse_edit datacore_edit datacore_run -->
|
|
@@ -587,6 +682,7 @@ MCP tool `datacore_run`; `POST /api/dispatch/datacore_run`. The datacore's check
|
|
|
587
682
|
| Parameter | Type | Required | Description |
|
|
588
683
|
|---|---|---|---|
|
|
589
684
|
| `note` | string | no | Optional note, recorded on the check run |
|
|
685
|
+
| `build` | boolean | no, default `True` | false = validate only: compile the pipelines, check the connections, dbt parse, the semantic validation per metric and the manifest import's dry run, with no dbt build (an answer in well under a minute, beside a running check or load). It is NEVER the green check deploy or auto_deploy needs |
|
|
590
686
|
|
|
591
687
|
**`deploy`** — Promote the checked draft to deployed (admin), or roll back to an earlier deployed snapshot; then_build also rebuilds changed models.
|
|
592
688
|
|
|
@@ -602,9 +698,9 @@ MCP tool `datacore_run`; `POST /api/dispatch/datacore_run`. The datacore's check
|
|
|
602
698
|
| Parameter | Type | Required | Description |
|
|
603
699
|
|---|---|---|---|
|
|
604
700
|
| `pipeline` | string | yes | Pipeline name (a deployed pipelines/<name>.yaml) |
|
|
605
|
-
| `window` | RunWindowParams | no | A date window to load instead of the pipeline's own; wider than backfill_chunk becomes a chunked backfill. Both ends are inclusive days, and so is every chunk; a resource whose API reads its end bound as exclusive declares `window.end: exclusive` in the pipeline and is sent the day after each chunk's last day |
|
|
701
|
+
| `window` | RunWindowParams | no | A date window to load instead of the pipeline's own; wider than backfill_chunk becomes a chunked backfill. Both ends are inclusive days, and so is every chunk; a resource whose API reads its end bound as exclusive declares `window.end: exclusive` in the pipeline and is sent the day after each chunk's last day. A rest resource whose window has a `step` sends one request per step of the window or chunk, not one for the whole window |
|
|
606
702
|
| `cursor` | RunCursorParams | no | {from: <value>}: a cursor to re-pull from |
|
|
607
|
-
| `backfill_chunk` | string | no | Chunk size for a windowed run:
|
|
703
|
+
| `backfill_chunk` | string | no | Chunk size for a windowed run: <n>d, <n>w or <n>M (days, weeks or calendar months; 1M is the default, 3M a quarter, 12M a year; there is no Y). Every chunk before the last loads only the windowed resources (with their resolve children and the ones they read rows from) and skips its dbt step, each skip recorded with its reason on the child run; the last chunk loads every resource and builds the models once |
|
|
608
704
|
|
|
609
705
|
**`build`** — Rebuild the deployed dbt models on the raw tables already loaded: no resource is loaded.
|
|
610
706
|
|
cli/bundled_manifest.py
CHANGED
|
@@ -6,7 +6,7 @@ the server's to say whether the server has operations this CLI does not
|
|
|
6
6
|
know. tests/test_skills_surface.py asserts it is current.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
MANIFEST_HASH = "
|
|
9
|
+
MANIFEST_HASH = "54af1f0a9d07ef692b63e0648d835ce5efc020bd9588db6d75897b36af4e0232"
|
|
10
10
|
TOOL_KEYS = (
|
|
11
11
|
"business_context_edit",
|
|
12
12
|
"semantic_model_edit",
|
cli/contract.py
CHANGED
|
@@ -656,6 +656,14 @@ class CheckDatacoreAction(DispatchParams):
|
|
|
656
656
|
|
|
657
657
|
action: Literal["check"]
|
|
658
658
|
note: str | None = Field(default=None, description="Optional note, recorded on the check run")
|
|
659
|
+
build: bool = Field(
|
|
660
|
+
default=True,
|
|
661
|
+
description=(
|
|
662
|
+
"false = validate only: compile the pipelines, check the connections, dbt parse, the semantic "
|
|
663
|
+
"validation per metric and the manifest import's dry run, with no dbt build (an answer in well under "
|
|
664
|
+
"a minute, beside a running check or load). It is NEVER the green check deploy or auto_deploy needs"
|
|
665
|
+
),
|
|
666
|
+
)
|
|
659
667
|
|
|
660
668
|
|
|
661
669
|
class DeployDatacoreAction(DispatchParams):
|
|
@@ -707,16 +715,19 @@ class RunPipelineAction(DispatchParams):
|
|
|
707
715
|
description=(
|
|
708
716
|
"A date window to load instead of the pipeline's own; wider than backfill_chunk becomes a chunked backfill. "
|
|
709
717
|
"Both ends are inclusive days, and so is every chunk; a resource whose API reads its end bound as exclusive "
|
|
710
|
-
"declares `window.end: exclusive` in the pipeline and is sent the day after each chunk's last day"
|
|
718
|
+
"declares `window.end: exclusive` in the pipeline and is sent the day after each chunk's last day. A rest resource "
|
|
719
|
+
"whose window has a `step` sends one request per step of the window or chunk, not one for the whole window"
|
|
711
720
|
),
|
|
712
721
|
)
|
|
713
722
|
cursor: RunCursorParams | None = Field(default=None, description="{from: <value>}: a cursor to re-pull from")
|
|
714
723
|
backfill_chunk: str | None = Field(
|
|
715
724
|
default=None,
|
|
716
725
|
description=(
|
|
717
|
-
"Chunk size for a windowed run:
|
|
718
|
-
"
|
|
719
|
-
"
|
|
726
|
+
"Chunk size for a windowed run: <n>d, <n>w or <n>M (days, weeks or calendar months; 1M is the default, "
|
|
727
|
+
"3M a quarter, 12M a year; there is no Y). Every chunk before the last "
|
|
728
|
+
"loads only the windowed resources (with their resolve children and the ones they read rows from) and "
|
|
729
|
+
"skips its dbt step, each skip recorded with its reason on the child run; the last chunk loads every "
|
|
730
|
+
"resource and builds the models once"
|
|
720
731
|
),
|
|
721
732
|
)
|
|
722
733
|
|
cli/generated.py
CHANGED
|
@@ -882,11 +882,12 @@ datacore_run = typer.Typer(name='datacore_run', help="The datacore's check-then-
|
|
|
882
882
|
@datacore_run.command('check', short_help='Check the current draft (member): compile every pipeline, build the changed dbt models into <org>_dev with their tests, and validate the semantic manifest per metric. Nothing reaches production.')
|
|
883
883
|
def datacore_run__check(
|
|
884
884
|
note: Annotated[Optional[str], typer.Option('--note', help='Optional note, recorded on the check run')] = None,
|
|
885
|
+
build: Annotated[Optional[bool], typer.Option('--build/--no-build', help="false = validate only: compile the pipelines, check the connections, dbt parse, the semantic validation per metric and the manifest import's dry run, with no dbt build (an answer in well under a minute, beside a running check or load). It is NEVER the green check deploy or auto_deploy needs (default: True)")] = None,
|
|
885
886
|
from_file: FromFile = None,
|
|
886
887
|
output: Output = None,
|
|
887
888
|
) -> None:
|
|
888
889
|
"""Check the current draft (member): compile every pipeline, build the changed dbt models into <org>_dev with their tests, and validate the semantic manifest per metric. Nothing reaches production."""
|
|
889
|
-
run_tool('datacore_run', contract.DatacoreRunParams, 'check', {'note': note}, from_file=from_file, output=output)
|
|
890
|
+
run_tool('datacore_run', contract.DatacoreRunParams, 'check', {'note': note, 'build': build}, from_file=from_file, output=output)
|
|
890
891
|
|
|
891
892
|
|
|
892
893
|
@datacore_run.command('deploy', short_help='Promote the checked draft to deployed (admin), or roll back to an earlier deployed snapshot; then_build also rebuilds changed models.')
|
|
@@ -905,9 +906,9 @@ def datacore_run__deploy(
|
|
|
905
906
|
@datacore_run.command('run', short_help='Load a pipeline now (admin): its own window, a given window, or a chunked backfill whose chunks run back to back.')
|
|
906
907
|
def datacore_run__run(
|
|
907
908
|
pipeline: Annotated[str, typer.Option('--pipeline', help='Pipeline name (a deployed pipelines/<name>.yaml)')],
|
|
908
|
-
window: Annotated[Optional[str], typer.Option('--window-json', help="A date window to load instead of the pipeline's own; wider than backfill_chunk becomes a chunked backfill. Both ends are inclusive days, and so is every chunk; a resource whose API reads its end bound as exclusive declares `window.end: exclusive` in the pipeline and is sent the day after each chunk's last day (JSON object with keys: start, end, or @path to a .json/.yaml file)")] = None,
|
|
909
|
+
window: Annotated[Optional[str], typer.Option('--window-json', help="A date window to load instead of the pipeline's own; wider than backfill_chunk becomes a chunked backfill. Both ends are inclusive days, and so is every chunk; a resource whose API reads its end bound as exclusive declares `window.end: exclusive` in the pipeline and is sent the day after each chunk's last day. A rest resource whose window has a `step` sends one request per step of the window or chunk, not one for the whole window (JSON object with keys: start, end, or @path to a .json/.yaml file)")] = None,
|
|
909
910
|
cursor: Annotated[Optional[str], typer.Option('--cursor-json', help='{from: <value>}: a cursor to re-pull from (JSON object with keys: from, or @path to a .json/.yaml file)')] = None,
|
|
910
|
-
backfill_chunk: Annotated[Optional[str], typer.Option('--backfill-chunk', help='Chunk size for a windowed run:
|
|
911
|
+
backfill_chunk: Annotated[Optional[str], typer.Option('--backfill-chunk', help='Chunk size for a windowed run: <n>d, <n>w or <n>M (days, weeks or calendar months; 1M is the default, 3M a quarter, 12M a year; there is no Y). Every chunk before the last loads only the windowed resources (with their resolve children and the ones they read rows from) and skips its dbt step, each skip recorded with its reason on the child run; the last chunk loads every resource and builds the models once')] = None,
|
|
911
912
|
from_file: FromFile = None,
|
|
912
913
|
output: Output = None,
|
|
913
914
|
) -> None:
|
cli/main.py
CHANGED
|
@@ -136,7 +136,7 @@ learned_app = typer.Typer(help="The learned trail: what DataSpring learned for t
|
|
|
136
136
|
app.add_typer(learned_app, name="learned")
|
|
137
137
|
warehouses_app = typer.Typer(help="The org's warehouses: external and managed, and which is active")
|
|
138
138
|
app.add_typer(warehouses_app, name="warehouses")
|
|
139
|
-
datacore_app = typer.Typer(help="The datacore workspace: pull, edit locally, push, check, deploy, build, run, cancel; schema, freshness and sample of its tables")
|
|
139
|
+
datacore_app = typer.Typer(help="The datacore workspace: pull, edit locally, push, check, deploy, build, run, cancel; schema, freshness and sample of its tables; the reference (pipeline and connection schemas, the connector catalog)")
|
|
140
140
|
app.add_typer(datacore_app, name="datacore")
|
|
141
141
|
from cli.skills_commands import skills_app # noqa: E402
|
|
142
142
|
app.add_typer(skills_app, name="skills")
|
|
@@ -2844,7 +2844,8 @@ def _check_finding(entry: dict) -> str:
|
|
|
2844
2844
|
def _print_check_report(run: dict) -> None:
|
|
2845
2845
|
report = run.get("check") or {}
|
|
2846
2846
|
status = run.get("status")
|
|
2847
|
-
|
|
2847
|
+
validate_only = run.get("build") is False or report.get("build") is False
|
|
2848
|
+
line = f"{'validate-only check' if validate_only else 'check'} {run.get('id')}: {status}"
|
|
2848
2849
|
if report.get("snapshot"):
|
|
2849
2850
|
line += f" (snapshot {report['snapshot']})"
|
|
2850
2851
|
(print_success if status == "succeeded" else print_error)(line)
|
|
@@ -2857,6 +2858,17 @@ def _print_check_report(run: dict) -> None:
|
|
|
2857
2858
|
dbt = report.get("dbt") or {}
|
|
2858
2859
|
if dbt.get("status") == "skipped" and dbt.get("reason"):
|
|
2859
2860
|
typer.echo(f" dbt skipped: {dbt['reason']}")
|
|
2861
|
+
elif dbt and validate_only:
|
|
2862
|
+
# Tick ip2: parsed and validated, nothing built; the counts are what the parse found.
|
|
2863
|
+
parsed = dbt.get("parsed") or {}
|
|
2864
|
+
counts = ", ".join(f"{parsed[k]} {k.replace('_', ' ')}" for k in ("models", "semantic_models", "metrics") if k in parsed)
|
|
2865
|
+
typer.echo(f" dbt {dbt.get('status')}: parsed and validated, nothing built" + (f" ({counts})" if counts else ""))
|
|
2866
|
+
if dbt.get("error"):
|
|
2867
|
+
typer.echo(f" {dbt['error']}")
|
|
2868
|
+
for failure in dbt.get("failures") or []:
|
|
2869
|
+
typer.echo(f" FAIL {_check_finding(failure)}")
|
|
2870
|
+
for warning in dbt.get("warnings") or []:
|
|
2871
|
+
typer.echo(f" warn {_check_finding(warning)}")
|
|
2860
2872
|
elif dbt:
|
|
2861
2873
|
typer.echo(
|
|
2862
2874
|
f" dbt {dbt.get('status')}: {dbt.get('models', 0)} models, {dbt.get('tests', 0)} tests, "
|
|
@@ -2882,6 +2894,8 @@ def _print_check_report(run: dict) -> None:
|
|
|
2882
2894
|
if target and dbt.get("models"):
|
|
2883
2895
|
typer.echo(f" read it dataspring sql \"SELECT * FROM {target}.<model> LIMIT 20\" "
|
|
2884
2896
|
f"(the models this check built; {target} expires tables after 7 days)")
|
|
2897
|
+
if validate_only:
|
|
2898
|
+
typer.echo(" note validate-only: never the green check a deploy needs; `dataspring datacore check` builds and tests")
|
|
2885
2899
|
|
|
2886
2900
|
|
|
2887
2901
|
def _poll_run(run_id: str, token: str, *, interval: float = 10.0, timeout: float = 1800.0) -> dict:
|
|
@@ -2905,10 +2919,25 @@ def _poll_run(run_id: str, token: str, *, interval: float = 10.0, timeout: float
|
|
|
2905
2919
|
@datacore_app.command("check")
|
|
2906
2920
|
def datacore_check_cmd(
|
|
2907
2921
|
wait: Annotated[bool, typer.Option("--wait/--no-wait", help="Poll the check run to its end")] = True,
|
|
2922
|
+
validate_only: Annotated[
|
|
2923
|
+
bool,
|
|
2924
|
+
typer.Option(
|
|
2925
|
+
"--validate-only",
|
|
2926
|
+
help="Parse and validate only (pipelines, connections, dbt parse, semantic and import validation), no dbt "
|
|
2927
|
+
"build; never the green check deploy needs",
|
|
2928
|
+
),
|
|
2929
|
+
] = False,
|
|
2908
2930
|
):
|
|
2909
2931
|
"""Check the draft: schema validation here, then a check run of the
|
|
2910
2932
|
tenant's job (compile, dbt build into <org>_dev, tests, dry-run bytes).
|
|
2911
2933
|
|
|
2934
|
+
--validate-only is the edit loop's cheap answer: the same file checks,
|
|
2935
|
+
pipelines compiled, connections checked, `dbt parse`, the semantic
|
|
2936
|
+
validation per metric and the manifest import's dry run, and NO dbt
|
|
2937
|
+
build, so it starts beside a running check or load and answers in well
|
|
2938
|
+
under a minute. It never counts as the green check `datacore deploy`
|
|
2939
|
+
(or auto_deploy) needs: run a full check before deploying.
|
|
2940
|
+
|
|
2912
2941
|
The check runs as the org's check identity, which can write <org>_dev and
|
|
2913
2942
|
nothing else, so it never touches production; a dbt project that would
|
|
2914
2943
|
build a model elsewhere is refused, naming the models. A workspace with
|
|
@@ -2923,24 +2952,27 @@ def datacore_check_cmd(
|
|
|
2923
2952
|
|
|
2924
2953
|
Example:
|
|
2925
2954
|
dataspring datacore check
|
|
2955
|
+
dataspring datacore check --validate-only
|
|
2926
2956
|
"""
|
|
2927
2957
|
require_auth()
|
|
2928
2958
|
token = auth.get_access_token()
|
|
2959
|
+
what = "validate-only check" if validate_only else "check"
|
|
2929
2960
|
try:
|
|
2930
|
-
result = call_tool("datacore_run", "check", {}, token=token)
|
|
2961
|
+
result = call_tool("datacore_run", "check", {"build": False} if validate_only else {}, token=token)
|
|
2931
2962
|
except Exception as e:
|
|
2932
2963
|
fail(e, role_action="check the datacore")
|
|
2933
2964
|
if not result.get("ok"):
|
|
2934
|
-
print_error(f"
|
|
2965
|
+
print_error(f"{what} failed at {result.get('stage')} for snapshot {result.get('snapshot')}")
|
|
2935
2966
|
for error in result.get("errors") or []:
|
|
2936
2967
|
typer.echo(f" {error}")
|
|
2937
2968
|
raise typer.Exit(1)
|
|
2938
2969
|
run_id = result["run_id"]
|
|
2939
|
-
print_info(f"
|
|
2970
|
+
print_info(f"{what} run {run_id} {result.get('status')} for snapshot {result.get('snapshot')}")
|
|
2940
2971
|
if not wait:
|
|
2941
2972
|
return
|
|
2942
2973
|
try:
|
|
2943
|
-
run
|
|
2974
|
+
# A validate-only run is over in seconds; a build takes minutes.
|
|
2975
|
+
run = _poll_run(run_id, token, interval=3.0 if validate_only else 10.0)
|
|
2944
2976
|
except typer.Exit:
|
|
2945
2977
|
raise
|
|
2946
2978
|
except Exception as e:
|
|
@@ -3069,7 +3101,7 @@ def datacore_run_cmd(
|
|
|
3069
3101
|
pipeline: Annotated[str, typer.Argument(help="Pipeline name")],
|
|
3070
3102
|
start: Annotated[Optional[str], typer.Option("--start", help="Window start, YYYY-MM-DD")] = None,
|
|
3071
3103
|
end: Annotated[Optional[str], typer.Option("--end", help="Window end, YYYY-MM-DD")] = None,
|
|
3072
|
-
chunk: Annotated[Optional[str], typer.Option("--chunk", help="Backfill chunk: 7d, 2w
|
|
3104
|
+
chunk: Annotated[Optional[str], typer.Option("--chunk", help="Backfill chunk: <n>d, <n>w or <n>M (7d, 2w, 1M the default, 3M a quarter, 12M a year). Chunks before the last load only windowed resources and skip dbt; the last loads everything and builds the models")] = None,
|
|
3073
3105
|
cursor_from: Annotated[Optional[str], typer.Option("--from", help="Cursor value to re-pull from")] = None,
|
|
3074
3106
|
wait: Annotated[bool, typer.Option("--wait", help="Poll the run to its end")] = False,
|
|
3075
3107
|
):
|
|
@@ -3077,7 +3109,10 @@ def datacore_run_cmd(
|
|
|
3077
3109
|
|
|
3078
3110
|
--start and --end are both inclusive days, and so is every chunk. A
|
|
3079
3111
|
resource whose API reads its end date as exclusive declares
|
|
3080
|
-
`end: exclusive` in its `window` and is sent the day after.
|
|
3112
|
+
`end: exclusive` in its `window` and is sent the day after. A rest
|
|
3113
|
+
resource whose `window` has a `step` (7d, 2w, 1M) sends one request
|
|
3114
|
+
per step of the run's window, and of every chunk, rather than one for
|
|
3115
|
+
the whole window (for a source with a per-request row cap).
|
|
3081
3116
|
|
|
3082
3117
|
A window wider than --chunk is a backfill: one child run per chunk, each
|
|
3083
3118
|
started as the previous one ends, one backfill per pipeline at a time.
|
|
@@ -3302,6 +3337,125 @@ def datacore_sample_cmd(
|
|
|
3302
3337
|
console.print(f"[dim]{statement} as {result.get('identity')}; job {result.get('job_id')}[/]")
|
|
3303
3338
|
|
|
3304
3339
|
|
|
3340
|
+
DATACORE_REFERENCE_PATH = "/api/datacore/reference"
|
|
3341
|
+
ReferencePart = Literal["pipeline", "connection", "connector", "catalog"]
|
|
3342
|
+
|
|
3343
|
+
|
|
3344
|
+
def _print_reference_index(payload: dict) -> None:
|
|
3345
|
+
for part, info in (payload.get("schemas") or {}).items():
|
|
3346
|
+
typer.echo(f"{part:<11} {info.get('file')}")
|
|
3347
|
+
typer.echo("")
|
|
3348
|
+
rows = [
|
|
3349
|
+
{"name": c["name"], "kind": "connector", "version": c.get("version"),
|
|
3350
|
+
"secrets": ", ".join(c.get("secrets") or []), "tables": ", ".join(c.get("tables") or []),
|
|
3351
|
+
"description": c.get("description", "")}
|
|
3352
|
+
for c in payload.get("connectors") or []
|
|
3353
|
+
] + [
|
|
3354
|
+
{"name": p["name"], "kind": "plugin", "version": p.get("version"),
|
|
3355
|
+
"secrets": ", ".join(p.get("secret_fields") or []), "tables": ", ".join(p.get("tables") or []),
|
|
3356
|
+
"description": ""}
|
|
3357
|
+
for p in payload.get("plugins") or []
|
|
3358
|
+
]
|
|
3359
|
+
format_output(rows, format="table", columns=["name", "kind", "version", "secrets", "tables", "description"],
|
|
3360
|
+
title="the connector catalog")
|
|
3361
|
+
typer.echo("")
|
|
3362
|
+
typer.echo(payload.get("secret_names", ""))
|
|
3363
|
+
typer.echo("Whole parts: dataspring datacore reference pipeline|connection|connector|catalog [<name>]")
|
|
3364
|
+
|
|
3365
|
+
|
|
3366
|
+
def _print_reference_entry(payload: dict) -> None:
|
|
3367
|
+
import yaml
|
|
3368
|
+
|
|
3369
|
+
typer.echo(f"{payload['name']} ({payload['kind']} version {payload.get('version')}): {payload.get('description', '')}".rstrip(": "))
|
|
3370
|
+
props = (payload.get("config_schema") or {}).get("properties") or {}
|
|
3371
|
+
required = set((payload.get("config_schema") or {}).get("required") or [])
|
|
3372
|
+
if props:
|
|
3373
|
+
format_output(
|
|
3374
|
+
[{"config": key, "required": "yes" if key in required else "no",
|
|
3375
|
+
"default": json.dumps(spec["default"]) if "default" in spec else "",
|
|
3376
|
+
"description": spec.get("description", "")} for key, spec in props.items()],
|
|
3377
|
+
format="table", columns=["config", "required", "default", "description"], title="config")
|
|
3378
|
+
secrets = payload.get("secrets") or []
|
|
3379
|
+
if secrets and isinstance(secrets[0], dict):
|
|
3380
|
+
format_output(
|
|
3381
|
+
[{"secret": s["field"], "required": "no" if s.get("optional") else "yes", "description": s.get("description", "")}
|
|
3382
|
+
for s in secrets],
|
|
3383
|
+
format="table", columns=["secret", "required", "description"], title="secrets")
|
|
3384
|
+
resources = payload.get("resources") or {}
|
|
3385
|
+
format_output(
|
|
3386
|
+
[{"resource": name, "table": res.get("table", name), "when": res.get("when", "")} for name, res in resources.items()],
|
|
3387
|
+
format="table", columns=["resource", "table", "when"], title="resources")
|
|
3388
|
+
if payload.get("example"):
|
|
3389
|
+
typer.echo("\npipelines/<name>.yaml:\n")
|
|
3390
|
+
typer.echo(yaml.safe_dump(payload["example"], sort_keys=False).rstrip())
|
|
3391
|
+
elif payload.get("usage"):
|
|
3392
|
+
typer.echo(f"\n{payload['usage']}")
|
|
3393
|
+
|
|
3394
|
+
|
|
3395
|
+
@datacore_app.command("reference")
|
|
3396
|
+
def datacore_reference_cmd(
|
|
3397
|
+
part: Annotated[Optional[ReferencePart], typer.Argument(help="pipeline, connection or connector (that JSON schema), or catalog")] = None,
|
|
3398
|
+
name: Annotated[Optional[str], typer.Argument(help="With catalog: one connector or plugin")] = None,
|
|
3399
|
+
format: Annotated[OutputFormat, typer.Option("--format", "-f", help="Output format (a schema prints as JSON unless yaml is asked for)")] = "table",
|
|
3400
|
+
):
|
|
3401
|
+
"""What a pipeline or connection file may say: the schemas and the connector catalog.
|
|
3402
|
+
|
|
3403
|
+
The same files the runner validates against, served by the server you
|
|
3404
|
+
are signed in to, so they match what datacore_edit and datacore check
|
|
3405
|
+
accept. With no argument: the schemas and every catalog connector and
|
|
3406
|
+
plugin, with the secrets it needs and the tables it loads.
|
|
3407
|
+
|
|
3408
|
+
Examples:
|
|
3409
|
+
dataspring datacore reference # the index and the catalog
|
|
3410
|
+
dataspring datacore reference pipeline > pipeline.schema.json
|
|
3411
|
+
dataspring datacore reference connection -f yaml
|
|
3412
|
+
dataspring datacore reference catalog # every connector, whole
|
|
3413
|
+
dataspring datacore reference catalog kanpla # one, with a pipeline file that uses it
|
|
3414
|
+
"""
|
|
3415
|
+
from urllib.parse import quote
|
|
3416
|
+
|
|
3417
|
+
if name and part != "catalog":
|
|
3418
|
+
print_error("a name goes with catalog: dataspring datacore reference catalog <name>")
|
|
3419
|
+
raise typer.Exit(2)
|
|
3420
|
+
path = DATACORE_REFERENCE_PATH
|
|
3421
|
+
if part:
|
|
3422
|
+
path += f"/{part}" + (f"/{quote(name, safe='')}" if name else "")
|
|
3423
|
+
require_auth()
|
|
3424
|
+
token = auth.get_access_token()
|
|
3425
|
+
try:
|
|
3426
|
+
payload = api_get(path, token)
|
|
3427
|
+
except Exception as e:
|
|
3428
|
+
fail(e, role_action="read the datacore reference")
|
|
3429
|
+
if part in ("pipeline", "connection", "connector"):
|
|
3430
|
+
if format == "yaml":
|
|
3431
|
+
format_output(payload["schema"], format="yaml")
|
|
3432
|
+
else:
|
|
3433
|
+
typer.echo(json.dumps(payload["schema"], indent=2, ensure_ascii=False))
|
|
3434
|
+
return
|
|
3435
|
+
if format != "table":
|
|
3436
|
+
format_output(payload, format=format)
|
|
3437
|
+
return
|
|
3438
|
+
if name:
|
|
3439
|
+
_print_reference_entry(payload)
|
|
3440
|
+
elif part == "catalog":
|
|
3441
|
+
_print_reference_index({
|
|
3442
|
+
"connectors": [
|
|
3443
|
+
{"name": n, "version": c.get("version"), "description": c.get("description", ""),
|
|
3444
|
+
"secrets": [f + ("" if not s.get("optional") else " (optional)") for f, s in (c.get("secrets") or {}).items()],
|
|
3445
|
+
"tables": [r.get("table", rn) for rn, r in (c.get("resources") or {}).items()]}
|
|
3446
|
+
for n, c in (payload.get("connectors") or {}).items()
|
|
3447
|
+
],
|
|
3448
|
+
"plugins": [
|
|
3449
|
+
{"name": n, "version": p.get("version"), "secret_fields": p.get("secrets") or [],
|
|
3450
|
+
"tables": [r.get("table", rn) for rn, r in (p.get("resources") or {}).items()]}
|
|
3451
|
+
for n, p in (payload.get("plugins") or {}).items()
|
|
3452
|
+
],
|
|
3453
|
+
"secret_names": payload.get("secret_names", ""),
|
|
3454
|
+
})
|
|
3455
|
+
else:
|
|
3456
|
+
_print_reference_index(payload)
|
|
3457
|
+
|
|
3458
|
+
|
|
3305
3459
|
@datacore_app.command("status")
|
|
3306
3460
|
def datacore_status(
|
|
3307
3461
|
format: Annotated[OutputFormat, typer.Option("--format", "-f", help="Output format")] = "table",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
cli/__init__.py,sha256=RJi9Y5W2jatsoYYtgi6Iu6UkHEi55lvsuNBdsY7pRXc,453
|
|
2
2
|
cli/auth.py,sha256=eeVI_oM2wLhT6ETkDnpMsThiNUT9OtHjPUOopifgc9w,19932
|
|
3
|
-
cli/bundled_manifest.py,sha256=
|
|
4
|
-
cli/contract.py,sha256=
|
|
5
|
-
cli/generated.py,sha256=
|
|
6
|
-
cli/main.py,sha256=
|
|
3
|
+
cli/bundled_manifest.py,sha256=uRMEYL7jtyRzAc9VzMEmwFqpPEDFGMTaz_01yuU01m4,936
|
|
4
|
+
cli/contract.py,sha256=6pdzc8VYOU5aZBBNaxa0featgggyTVBZ7jHZqFVPpYE,52234
|
|
5
|
+
cli/generated.py,sha256=qABzREL4iswFBBviv6eaEVi4pQ_wEarjwa_PKVgQVLo,91013
|
|
6
|
+
cli/main.py,sha256=zi6sB05JBYFBmUtUP2tPUQmR6c8yqEWTullCBDD9I_w,136498
|
|
7
7
|
cli/output.py,sha256=NoVJx66AswG3AI7Dqj2ZpoTXzgGh7H-tvFSNAJ25U8s,7358
|
|
8
8
|
cli/refusals.py,sha256=cRn81B7PgMK53rHHOtyryBUCMGrY-_ClOL3EcGUnNrI,10229
|
|
9
9
|
cli/runtime.py,sha256=W0H-Vv1AKNtk4mAYW1l8TOLUipykGGk2OKav0I_UO2M,7461
|
|
@@ -12,10 +12,10 @@ cli/skilltree.py,sha256=r-BJ2p1U5OVhIEZbkZoOgtJ2tuDo00CCQcd1fu3O1f8,12807
|
|
|
12
12
|
cli/upgrade.py,sha256=OGSVjTSTLygDB2M6tJCcIhQOosHqz0sqzhWlcmv-jfE,2944
|
|
13
13
|
cli/version.py,sha256=ARcAiq_TgTntMsJC_9isC9gcBymLQmkUcVm1zoyJy9k,4467
|
|
14
14
|
settings.py,sha256=z0RST-pgTT-A8z3ESg8emmCpGxAcF_9OhYaNRwrpGME,4737
|
|
15
|
-
cli/_skills/dataspring-author/SKILL.md,sha256=
|
|
15
|
+
cli/_skills/dataspring-author/SKILL.md,sha256=Xtc66OM0l3ORXgxBHsRJ6BnWC-fhzD4ATz3DNn_Ia40,42322
|
|
16
16
|
cli/_skills/dataspring-consume/SKILL.md,sha256=DAtg6EhxdDmqIXE1MoezFSKLM5EG8R0woBfIhI9b5IU,58253
|
|
17
17
|
cli/_skills/dataspring-correct/SKILL.md,sha256=YpvPPz4Tt4ihQA_LCkkhco8tfrGPsxmMS8JxFdf92zQ,6555
|
|
18
|
-
dataspring_cli-0.4.
|
|
19
|
-
dataspring_cli-0.4.
|
|
20
|
-
dataspring_cli-0.4.
|
|
21
|
-
dataspring_cli-0.4.
|
|
18
|
+
dataspring_cli-0.4.4.dist-info/METADATA,sha256=SA_RaqpfIwNIi-cjfZ4c4IZ9mz_nDP4fa5exR0syIUM,5395
|
|
19
|
+
dataspring_cli-0.4.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
20
|
+
dataspring_cli-0.4.4.dist-info/entry_points.txt,sha256=X4YdpJ6kFTyolhrFZNlzkXT3dhE_H_4E3YUHna6oXUM,44
|
|
21
|
+
dataspring_cli-0.4.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|