dataspring-cli 0.4.3__tar.gz → 0.4.5__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.
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/PKG-INFO +1 -1
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/bundled_manifest.py +1 -1
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/contract.py +15 -4
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/generated.py +4 -3
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/main.py +180 -16
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/pyproject.toml +1 -1
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/skills/dataspring-author/SKILL.md +105 -7
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/skills/dataspring-consume/SKILL.md +16 -9
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/.gitignore +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/README.md +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/__init__.py +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/auth.py +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/output.py +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/refusals.py +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/runtime.py +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/skills_commands.py +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/skilltree.py +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/upgrade.py +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/cli/version.py +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/hatch_build.py +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/settings.py +0 -0
- {dataspring_cli-0.4.3 → dataspring_cli-0.4.5}/skills/dataspring-correct/SKILL.md +0 -0
|
@@ -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",
|
|
@@ -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
|
|
|
@@ -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:
|
|
@@ -94,7 +94,9 @@ class DataSpringGroup(TyperGroup):
|
|
|
94
94
|
#: exit codes an agent branches on (hub D-99, suite standards §6).
|
|
95
95
|
ROOT_HELP = """DataSpring CLI: query metrics, run the datacore and manage boards from the terminal.
|
|
96
96
|
|
|
97
|
-
Sign in with `dataspring auth login
|
|
97
|
+
Sign in with `dataspring auth login`: it prints a code and a link; open the link on any device and approve (works over SSH and in containers too). Every command acts in one org: --org, then DATASPRING_ORG or FORSPRING_ORG, then .forspring.json, then the key's own org, then the only stored key; `dataspring whoami` says which. No command changes the org for later ones.
|
|
98
|
+
|
|
99
|
+
Agents over MCP need no CLI: `claude mcp add --transport http --scope user dataspring https://dataspring.app/api/mcp/`, then sign in and approve the org in the browser. For CI with no person present: `export DATASPRING_API_KEY=<key>` (a key from the console's "Automation and CI"; it acts in its own org).
|
|
98
100
|
|
|
99
101
|
A refusal prints `<reason>: <what happened> Fix: <what fixes it>` and exits 4 (not_signed_in, product_off, org_suspended, role_too_low, org_mismatch), 3 (not_a_member), 2 (org_required) or 1 (account_unavailable: retry). Any other failure exits 1, a usage error 2, a removed command 64."""
|
|
100
102
|
|
|
@@ -136,7 +138,7 @@ learned_app = typer.Typer(help="The learned trail: what DataSpring learned for t
|
|
|
136
138
|
app.add_typer(learned_app, name="learned")
|
|
137
139
|
warehouses_app = typer.Typer(help="The org's warehouses: external and managed, and which is active")
|
|
138
140
|
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")
|
|
141
|
+
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
142
|
app.add_typer(datacore_app, name="datacore")
|
|
141
143
|
from cli.skills_commands import skills_app # noqa: E402
|
|
142
144
|
app.add_typer(skills_app, name="skills")
|
|
@@ -593,17 +595,25 @@ def auth_login(
|
|
|
593
595
|
no_browser: Annotated[bool, typer.Option("--no-browser", help="Do not try to open a browser; just print the URL")] = False,
|
|
594
596
|
all_orgs: Annotated[bool, typer.Option("--all-orgs", help="Ask for a key in every org you belong to, not only the one you approve (the browser says so)")] = False,
|
|
595
597
|
):
|
|
596
|
-
"""Sign in
|
|
598
|
+
"""Sign in: the CLI prints a code and a link; open the link on any device and approve.
|
|
597
599
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
600
|
+
It works over SSH and in containers too: the machine running the CLI
|
|
601
|
+
needs no browser. The URL and the code are printed at once (also when the
|
|
602
|
+
output is redirected, so an agent can run it in the background and hand
|
|
603
|
+
both to a person), then it waits for the approval. Mints an ordinary API
|
|
604
|
+
key labelled 'CLI on <hostname>' for the org you approve (one per org you
|
|
605
|
+
are in with --all-orgs), stored in ~/.dataspring/credentials.json.
|
|
603
606
|
|
|
604
607
|
Logging in picks no org for later commands: with more than one key
|
|
605
608
|
stored, name it per command (--org, DATASPRING_ORG, .forspring.json).
|
|
606
|
-
|
|
609
|
+
|
|
610
|
+
Automation and CI, with no person present: export DATASPRING_API_KEY=<key>
|
|
611
|
+
(a key made in the console's connect page, under "Automation and CI"); it
|
|
612
|
+
always wins over the stored keys and acts in its own org. --key stores
|
|
613
|
+
such a key instead.
|
|
614
|
+
|
|
615
|
+
Agents over MCP do not need this: claude mcp add --transport http
|
|
616
|
+
--scope user dataspring https://dataspring.app/api/mcp/
|
|
607
617
|
"""
|
|
608
618
|
try:
|
|
609
619
|
if key:
|
|
@@ -2844,7 +2854,8 @@ def _check_finding(entry: dict) -> str:
|
|
|
2844
2854
|
def _print_check_report(run: dict) -> None:
|
|
2845
2855
|
report = run.get("check") or {}
|
|
2846
2856
|
status = run.get("status")
|
|
2847
|
-
|
|
2857
|
+
validate_only = run.get("build") is False or report.get("build") is False
|
|
2858
|
+
line = f"{'validate-only check' if validate_only else 'check'} {run.get('id')}: {status}"
|
|
2848
2859
|
if report.get("snapshot"):
|
|
2849
2860
|
line += f" (snapshot {report['snapshot']})"
|
|
2850
2861
|
(print_success if status == "succeeded" else print_error)(line)
|
|
@@ -2857,6 +2868,17 @@ def _print_check_report(run: dict) -> None:
|
|
|
2857
2868
|
dbt = report.get("dbt") or {}
|
|
2858
2869
|
if dbt.get("status") == "skipped" and dbt.get("reason"):
|
|
2859
2870
|
typer.echo(f" dbt skipped: {dbt['reason']}")
|
|
2871
|
+
elif dbt and validate_only:
|
|
2872
|
+
# Tick ip2: parsed and validated, nothing built; the counts are what the parse found.
|
|
2873
|
+
parsed = dbt.get("parsed") or {}
|
|
2874
|
+
counts = ", ".join(f"{parsed[k]} {k.replace('_', ' ')}" for k in ("models", "semantic_models", "metrics") if k in parsed)
|
|
2875
|
+
typer.echo(f" dbt {dbt.get('status')}: parsed and validated, nothing built" + (f" ({counts})" if counts else ""))
|
|
2876
|
+
if dbt.get("error"):
|
|
2877
|
+
typer.echo(f" {dbt['error']}")
|
|
2878
|
+
for failure in dbt.get("failures") or []:
|
|
2879
|
+
typer.echo(f" FAIL {_check_finding(failure)}")
|
|
2880
|
+
for warning in dbt.get("warnings") or []:
|
|
2881
|
+
typer.echo(f" warn {_check_finding(warning)}")
|
|
2860
2882
|
elif dbt:
|
|
2861
2883
|
typer.echo(
|
|
2862
2884
|
f" dbt {dbt.get('status')}: {dbt.get('models', 0)} models, {dbt.get('tests', 0)} tests, "
|
|
@@ -2882,6 +2904,8 @@ def _print_check_report(run: dict) -> None:
|
|
|
2882
2904
|
if target and dbt.get("models"):
|
|
2883
2905
|
typer.echo(f" read it dataspring sql \"SELECT * FROM {target}.<model> LIMIT 20\" "
|
|
2884
2906
|
f"(the models this check built; {target} expires tables after 7 days)")
|
|
2907
|
+
if validate_only:
|
|
2908
|
+
typer.echo(" note validate-only: never the green check a deploy needs; `dataspring datacore check` builds and tests")
|
|
2885
2909
|
|
|
2886
2910
|
|
|
2887
2911
|
def _poll_run(run_id: str, token: str, *, interval: float = 10.0, timeout: float = 1800.0) -> dict:
|
|
@@ -2905,10 +2929,25 @@ def _poll_run(run_id: str, token: str, *, interval: float = 10.0, timeout: float
|
|
|
2905
2929
|
@datacore_app.command("check")
|
|
2906
2930
|
def datacore_check_cmd(
|
|
2907
2931
|
wait: Annotated[bool, typer.Option("--wait/--no-wait", help="Poll the check run to its end")] = True,
|
|
2932
|
+
validate_only: Annotated[
|
|
2933
|
+
bool,
|
|
2934
|
+
typer.Option(
|
|
2935
|
+
"--validate-only",
|
|
2936
|
+
help="Parse and validate only (pipelines, connections, dbt parse, semantic and import validation), no dbt "
|
|
2937
|
+
"build; never the green check deploy needs",
|
|
2938
|
+
),
|
|
2939
|
+
] = False,
|
|
2908
2940
|
):
|
|
2909
2941
|
"""Check the draft: schema validation here, then a check run of the
|
|
2910
2942
|
tenant's job (compile, dbt build into <org>_dev, tests, dry-run bytes).
|
|
2911
2943
|
|
|
2944
|
+
--validate-only is the edit loop's cheap answer: the same file checks,
|
|
2945
|
+
pipelines compiled, connections checked, `dbt parse`, the semantic
|
|
2946
|
+
validation per metric and the manifest import's dry run, and NO dbt
|
|
2947
|
+
build, so it starts beside a running check or load and answers in well
|
|
2948
|
+
under a minute. It never counts as the green check `datacore deploy`
|
|
2949
|
+
(or auto_deploy) needs: run a full check before deploying.
|
|
2950
|
+
|
|
2912
2951
|
The check runs as the org's check identity, which can write <org>_dev and
|
|
2913
2952
|
nothing else, so it never touches production; a dbt project that would
|
|
2914
2953
|
build a model elsewhere is refused, naming the models. A workspace with
|
|
@@ -2923,24 +2962,27 @@ def datacore_check_cmd(
|
|
|
2923
2962
|
|
|
2924
2963
|
Example:
|
|
2925
2964
|
dataspring datacore check
|
|
2965
|
+
dataspring datacore check --validate-only
|
|
2926
2966
|
"""
|
|
2927
2967
|
require_auth()
|
|
2928
2968
|
token = auth.get_access_token()
|
|
2969
|
+
what = "validate-only check" if validate_only else "check"
|
|
2929
2970
|
try:
|
|
2930
|
-
result = call_tool("datacore_run", "check", {}, token=token)
|
|
2971
|
+
result = call_tool("datacore_run", "check", {"build": False} if validate_only else {}, token=token)
|
|
2931
2972
|
except Exception as e:
|
|
2932
2973
|
fail(e, role_action="check the datacore")
|
|
2933
2974
|
if not result.get("ok"):
|
|
2934
|
-
print_error(f"
|
|
2975
|
+
print_error(f"{what} failed at {result.get('stage')} for snapshot {result.get('snapshot')}")
|
|
2935
2976
|
for error in result.get("errors") or []:
|
|
2936
2977
|
typer.echo(f" {error}")
|
|
2937
2978
|
raise typer.Exit(1)
|
|
2938
2979
|
run_id = result["run_id"]
|
|
2939
|
-
print_info(f"
|
|
2980
|
+
print_info(f"{what} run {run_id} {result.get('status')} for snapshot {result.get('snapshot')}")
|
|
2940
2981
|
if not wait:
|
|
2941
2982
|
return
|
|
2942
2983
|
try:
|
|
2943
|
-
run
|
|
2984
|
+
# A validate-only run is over in seconds; a build takes minutes.
|
|
2985
|
+
run = _poll_run(run_id, token, interval=3.0 if validate_only else 10.0)
|
|
2944
2986
|
except typer.Exit:
|
|
2945
2987
|
raise
|
|
2946
2988
|
except Exception as e:
|
|
@@ -3069,7 +3111,7 @@ def datacore_run_cmd(
|
|
|
3069
3111
|
pipeline: Annotated[str, typer.Argument(help="Pipeline name")],
|
|
3070
3112
|
start: Annotated[Optional[str], typer.Option("--start", help="Window start, YYYY-MM-DD")] = None,
|
|
3071
3113
|
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
|
|
3114
|
+
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
3115
|
cursor_from: Annotated[Optional[str], typer.Option("--from", help="Cursor value to re-pull from")] = None,
|
|
3074
3116
|
wait: Annotated[bool, typer.Option("--wait", help="Poll the run to its end")] = False,
|
|
3075
3117
|
):
|
|
@@ -3077,7 +3119,10 @@ def datacore_run_cmd(
|
|
|
3077
3119
|
|
|
3078
3120
|
--start and --end are both inclusive days, and so is every chunk. A
|
|
3079
3121
|
resource whose API reads its end date as exclusive declares
|
|
3080
|
-
`end: exclusive` in its `window` and is sent the day after.
|
|
3122
|
+
`end: exclusive` in its `window` and is sent the day after. A rest
|
|
3123
|
+
resource whose `window` has a `step` (7d, 2w, 1M) sends one request
|
|
3124
|
+
per step of the run's window, and of every chunk, rather than one for
|
|
3125
|
+
the whole window (for a source with a per-request row cap).
|
|
3081
3126
|
|
|
3082
3127
|
A window wider than --chunk is a backfill: one child run per chunk, each
|
|
3083
3128
|
started as the previous one ends, one backfill per pipeline at a time.
|
|
@@ -3302,6 +3347,125 @@ def datacore_sample_cmd(
|
|
|
3302
3347
|
console.print(f"[dim]{statement} as {result.get('identity')}; job {result.get('job_id')}[/]")
|
|
3303
3348
|
|
|
3304
3349
|
|
|
3350
|
+
DATACORE_REFERENCE_PATH = "/api/datacore/reference"
|
|
3351
|
+
ReferencePart = Literal["pipeline", "connection", "connector", "catalog"]
|
|
3352
|
+
|
|
3353
|
+
|
|
3354
|
+
def _print_reference_index(payload: dict) -> None:
|
|
3355
|
+
for part, info in (payload.get("schemas") or {}).items():
|
|
3356
|
+
typer.echo(f"{part:<11} {info.get('file')}")
|
|
3357
|
+
typer.echo("")
|
|
3358
|
+
rows = [
|
|
3359
|
+
{"name": c["name"], "kind": "connector", "version": c.get("version"),
|
|
3360
|
+
"secrets": ", ".join(c.get("secrets") or []), "tables": ", ".join(c.get("tables") or []),
|
|
3361
|
+
"description": c.get("description", "")}
|
|
3362
|
+
for c in payload.get("connectors") or []
|
|
3363
|
+
] + [
|
|
3364
|
+
{"name": p["name"], "kind": "plugin", "version": p.get("version"),
|
|
3365
|
+
"secrets": ", ".join(p.get("secret_fields") or []), "tables": ", ".join(p.get("tables") or []),
|
|
3366
|
+
"description": ""}
|
|
3367
|
+
for p in payload.get("plugins") or []
|
|
3368
|
+
]
|
|
3369
|
+
format_output(rows, format="table", columns=["name", "kind", "version", "secrets", "tables", "description"],
|
|
3370
|
+
title="the connector catalog")
|
|
3371
|
+
typer.echo("")
|
|
3372
|
+
typer.echo(payload.get("secret_names", ""))
|
|
3373
|
+
typer.echo("Whole parts: dataspring datacore reference pipeline|connection|connector|catalog [<name>]")
|
|
3374
|
+
|
|
3375
|
+
|
|
3376
|
+
def _print_reference_entry(payload: dict) -> None:
|
|
3377
|
+
import yaml
|
|
3378
|
+
|
|
3379
|
+
typer.echo(f"{payload['name']} ({payload['kind']} version {payload.get('version')}): {payload.get('description', '')}".rstrip(": "))
|
|
3380
|
+
props = (payload.get("config_schema") or {}).get("properties") or {}
|
|
3381
|
+
required = set((payload.get("config_schema") or {}).get("required") or [])
|
|
3382
|
+
if props:
|
|
3383
|
+
format_output(
|
|
3384
|
+
[{"config": key, "required": "yes" if key in required else "no",
|
|
3385
|
+
"default": json.dumps(spec["default"]) if "default" in spec else "",
|
|
3386
|
+
"description": spec.get("description", "")} for key, spec in props.items()],
|
|
3387
|
+
format="table", columns=["config", "required", "default", "description"], title="config")
|
|
3388
|
+
secrets = payload.get("secrets") or []
|
|
3389
|
+
if secrets and isinstance(secrets[0], dict):
|
|
3390
|
+
format_output(
|
|
3391
|
+
[{"secret": s["field"], "required": "no" if s.get("optional") else "yes", "description": s.get("description", "")}
|
|
3392
|
+
for s in secrets],
|
|
3393
|
+
format="table", columns=["secret", "required", "description"], title="secrets")
|
|
3394
|
+
resources = payload.get("resources") or {}
|
|
3395
|
+
format_output(
|
|
3396
|
+
[{"resource": name, "table": res.get("table", name), "when": res.get("when", "")} for name, res in resources.items()],
|
|
3397
|
+
format="table", columns=["resource", "table", "when"], title="resources")
|
|
3398
|
+
if payload.get("example"):
|
|
3399
|
+
typer.echo("\npipelines/<name>.yaml:\n")
|
|
3400
|
+
typer.echo(yaml.safe_dump(payload["example"], sort_keys=False).rstrip())
|
|
3401
|
+
elif payload.get("usage"):
|
|
3402
|
+
typer.echo(f"\n{payload['usage']}")
|
|
3403
|
+
|
|
3404
|
+
|
|
3405
|
+
@datacore_app.command("reference")
|
|
3406
|
+
def datacore_reference_cmd(
|
|
3407
|
+
part: Annotated[Optional[ReferencePart], typer.Argument(help="pipeline, connection or connector (that JSON schema), or catalog")] = None,
|
|
3408
|
+
name: Annotated[Optional[str], typer.Argument(help="With catalog: one connector or plugin")] = None,
|
|
3409
|
+
format: Annotated[OutputFormat, typer.Option("--format", "-f", help="Output format (a schema prints as JSON unless yaml is asked for)")] = "table",
|
|
3410
|
+
):
|
|
3411
|
+
"""What a pipeline or connection file may say: the schemas and the connector catalog.
|
|
3412
|
+
|
|
3413
|
+
The same files the runner validates against, served by the server you
|
|
3414
|
+
are signed in to, so they match what datacore_edit and datacore check
|
|
3415
|
+
accept. With no argument: the schemas and every catalog connector and
|
|
3416
|
+
plugin, with the secrets it needs and the tables it loads.
|
|
3417
|
+
|
|
3418
|
+
Examples:
|
|
3419
|
+
dataspring datacore reference # the index and the catalog
|
|
3420
|
+
dataspring datacore reference pipeline > pipeline.schema.json
|
|
3421
|
+
dataspring datacore reference connection -f yaml
|
|
3422
|
+
dataspring datacore reference catalog # every connector, whole
|
|
3423
|
+
dataspring datacore reference catalog kanpla # one, with a pipeline file that uses it
|
|
3424
|
+
"""
|
|
3425
|
+
from urllib.parse import quote
|
|
3426
|
+
|
|
3427
|
+
if name and part != "catalog":
|
|
3428
|
+
print_error("a name goes with catalog: dataspring datacore reference catalog <name>")
|
|
3429
|
+
raise typer.Exit(2)
|
|
3430
|
+
path = DATACORE_REFERENCE_PATH
|
|
3431
|
+
if part:
|
|
3432
|
+
path += f"/{part}" + (f"/{quote(name, safe='')}" if name else "")
|
|
3433
|
+
require_auth()
|
|
3434
|
+
token = auth.get_access_token()
|
|
3435
|
+
try:
|
|
3436
|
+
payload = api_get(path, token)
|
|
3437
|
+
except Exception as e:
|
|
3438
|
+
fail(e, role_action="read the datacore reference")
|
|
3439
|
+
if part in ("pipeline", "connection", "connector"):
|
|
3440
|
+
if format == "yaml":
|
|
3441
|
+
format_output(payload["schema"], format="yaml")
|
|
3442
|
+
else:
|
|
3443
|
+
typer.echo(json.dumps(payload["schema"], indent=2, ensure_ascii=False))
|
|
3444
|
+
return
|
|
3445
|
+
if format != "table":
|
|
3446
|
+
format_output(payload, format=format)
|
|
3447
|
+
return
|
|
3448
|
+
if name:
|
|
3449
|
+
_print_reference_entry(payload)
|
|
3450
|
+
elif part == "catalog":
|
|
3451
|
+
_print_reference_index({
|
|
3452
|
+
"connectors": [
|
|
3453
|
+
{"name": n, "version": c.get("version"), "description": c.get("description", ""),
|
|
3454
|
+
"secrets": [f + ("" if not s.get("optional") else " (optional)") for f, s in (c.get("secrets") or {}).items()],
|
|
3455
|
+
"tables": [r.get("table", rn) for rn, r in (c.get("resources") or {}).items()]}
|
|
3456
|
+
for n, c in (payload.get("connectors") or {}).items()
|
|
3457
|
+
],
|
|
3458
|
+
"plugins": [
|
|
3459
|
+
{"name": n, "version": p.get("version"), "secret_fields": p.get("secrets") or [],
|
|
3460
|
+
"tables": [r.get("table", rn) for rn, r in (p.get("resources") or {}).items()]}
|
|
3461
|
+
for n, p in (payload.get("plugins") or {}).items()
|
|
3462
|
+
],
|
|
3463
|
+
"secret_names": payload.get("secret_names", ""),
|
|
3464
|
+
})
|
|
3465
|
+
else:
|
|
3466
|
+
_print_reference_index(payload)
|
|
3467
|
+
|
|
3468
|
+
|
|
3305
3469
|
@datacore_app.command("status")
|
|
3306
3470
|
def datacore_status(
|
|
3307
3471
|
format: Annotated[OutputFormat, typer.Option("--format", "-f", help="Output format")] = "table",
|
|
@@ -21,8 +21,10 @@ at once (run it in the background and hand both to the person), then
|
|
|
21
21
|
echo '{"org": "acme"}' > .forspring.json && dataspring whoami --json
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
MCP:
|
|
25
|
-
|
|
24
|
+
MCP: `claude mcp add --transport http --scope user dataspring
|
|
25
|
+
https://dataspring.app/api/mcp/` (or the same address in any client); the
|
|
26
|
+
person signs in and the consent page names the org, bound to that connection.
|
|
27
|
+
With no person present: `Authorization: Bearer ds_...` (the key's own org).
|
|
26
28
|
|
|
27
29
|
A refusal leads with a reason and ends with its fix; match the reason. The
|
|
28
30
|
ones you meet while building: `role_too_low` (connections, pipelines,
|
|
@@ -237,13 +239,14 @@ production.
|
|
|
237
239
|
```bash
|
|
238
240
|
dataspring datacore pull ./ws # deployed files, each exactly as pushed
|
|
239
241
|
dataspring datacore push ./ws # changed files only, into a new draft
|
|
242
|
+
dataspring datacore check --validate-only # seconds: parse + semantic validation, no build
|
|
240
243
|
dataspring datacore check # waits; exits 1 unless it passes
|
|
241
244
|
dataspring datacore deploy --then-build # admin; rebuilds changed models too
|
|
242
245
|
```
|
|
243
246
|
|
|
244
247
|
- **A new org** pulls its starting workspace (`datacore.yaml`, empty
|
|
245
|
-
`pipelines
|
|
246
|
-
|
|
248
|
+
`pipelines/` and `connections/`, and in `models/` a starting dbt project,
|
|
249
|
+
see 8.7).
|
|
247
250
|
- **A check** compiles every pipeline, builds the changed models and their
|
|
248
251
|
tests into `<org>_dev`, and dry-runs their SQL. It runs as an identity
|
|
249
252
|
that can write `<org>_dev` only: a project whose `generate_schema_name`
|
|
@@ -262,6 +265,12 @@ dataspring datacore deploy --then-build # admin; rebuilds changed models too
|
|
|
262
265
|
type_params: {numerator: gross_margin, denominator: revenue} # two metrics
|
|
263
266
|
```
|
|
264
267
|
|
|
268
|
+
- **Validate-only while you edit** (`datacore_run` `check` with `build:
|
|
269
|
+
false`; CLI `datacore check --validate-only`): pipelines compiled,
|
|
270
|
+
connections checked, `dbt parse`, the per-metric semantic validation and
|
|
271
|
+
the import's dry run, and no `dbt build`, so a broken metric or model is
|
|
272
|
+
reported in well under a minute and it never waits behind a check or a
|
|
273
|
+
load. It is never a green check: run a full `check` before `deploy`.
|
|
265
274
|
- **Deploy** needs a green check of that exact draft (any edit makes a new
|
|
266
275
|
draft) and starts a run of each windowed pipeline it changed.
|
|
267
276
|
|
|
@@ -328,10 +337,41 @@ whose bytes equal the stored one's.
|
|
|
328
337
|
|
|
329
338
|
Test an API's end bound with a one-day window before a backfill: a wrong
|
|
330
339
|
guess silently drops the last day of every chunk.
|
|
340
|
+
- **A window is one request** unless it has a `step`. For a source that caps
|
|
341
|
+
rows per request and does not page (e-regnskab: 20,000 item lines), a long
|
|
342
|
+
re-pull is cut into steps, one request each (`7d`, `2w`, `1M`; calendar
|
|
343
|
+
months, as backfill chunks are cut). `end: exclusive` holds per step, and
|
|
344
|
+
the scheduled window, a manual run and every backfill chunk all step:
|
|
345
|
+
|
|
346
|
+
```yaml
|
|
347
|
+
window: { lookback: 365d, step: 1M, param_start: createdFrom, param_end: createdTo, end: exclusive }
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
A rest resource only, and not a `resolve` child: step its parent.
|
|
351
|
+
- **`resolve` fans out over the parent's own window** (its lookback, or the
|
|
352
|
+
run's window), not the child's. When the child's rows lack the parent's
|
|
353
|
+
key, carry it: `include_from_parent: [hnInvoiceSalesID]` adds the column
|
|
354
|
+
`_<parent resource>_<field>` (`_invoices_hnInvoiceSalesID`) to every child
|
|
355
|
+
row; use it in the child's `primary_key` or rename it in `shape`.
|
|
356
|
+
`param` fills a `{param}` placeholder in the endpoint, or else is sent as
|
|
357
|
+
a query parameter.
|
|
358
|
+
- **An endpoint that answers a list of scalars** (`["budget", "forecast"]`)
|
|
359
|
+
loads one row per element in a column `value`; fan out over it with
|
|
360
|
+
`resolve: { field: value, ... }`.
|
|
331
361
|
- **A backfill** is `dataspring datacore run <pipeline> --start ... --end ...
|
|
362
|
+
--chunk 1M`: one child run per chunk, each started as the previous one
|
|
363
|
+
ends. The chunk is `<n>d`, `<n>w` or `<n>M` (`7d`, `2w`, `1M`, `3M` a
|
|
364
|
+
quarter, `12M` a year; no `Y`). A chunk before the last loads only the
|
|
365
|
+
windowed resources (and those they read rows from) and skips dbt; the last
|
|
366
|
+
chunk loads everything and builds the models once. A failed child fails
|
|
367
|
+
the backfill. One backfill per pipeline at a time.
|
|
368
|
+
- **One request per window.** A rest resource without a `paginator` sends
|
|
369
|
+
one request per window and nothing splits it, so a source that caps an
|
|
370
|
+
answer at N rows needs a `lookback`, and a `--chunk`, under N rows.
|
|
332
371
|
--chunk 1M` (or `3M`, `7d`, `2w`): one child run per chunk, each started as
|
|
333
372
|
the previous one ends. A chunk before the last loads only the windowed
|
|
334
|
-
resources
|
|
373
|
+
resources, their `resolve` children (fetched for that chunk's parent
|
|
374
|
+
rows) and those they read rows from, and skips dbt; the last chunk
|
|
335
375
|
loads everything and builds the models once. A failed child fails the
|
|
336
376
|
backfill. One backfill per pipeline at a time.
|
|
337
377
|
- **Stop** a queued or running run, or a whole backfill, with `dataspring
|
|
@@ -339,6 +379,63 @@ whose bytes equal the stored one's.
|
|
|
339
379
|
- **`reset`** drops a pipeline's raw tables and load state. It is
|
|
340
380
|
irreversible: ask first.
|
|
341
381
|
|
|
382
|
+
## 8.6 Writing a pipeline file
|
|
383
|
+
|
|
384
|
+
Do not read DataSpring's source for the grammar: it is served, cut from the
|
|
385
|
+
files the runner validates against.
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
dataspring datacore reference # the schemas, every connector: secrets, tables
|
|
389
|
+
dataspring datacore reference catalog kanpla # one connector, whole, with a file that uses it
|
|
390
|
+
dataspring datacore reference pipeline # the pipeline JSON schema (also: connection)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
MCP: `dataspring://datacore/reference[/<part>]` or `read(kind=
|
|
394
|
+
"datacore_reference", id=<part>)`; REST `GET /api/datacore/reference`.
|
|
395
|
+
Check the catalog first: a source with a connector is `connector: <name>`
|
|
396
|
+
plus `config` and `secrets`. Otherwise write an inline `source` (or
|
|
397
|
+
`sources`) and `resources`. The rules that bite:
|
|
398
|
+
|
|
399
|
+
- **Secrets are names**, `t-<org>-<connection>-<field>`, lowercase letters,
|
|
400
|
+
digits and hyphens only (no underscore). Set each with `dataspring secret
|
|
401
|
+
set <name> --stdin` before pushing the file that names it.
|
|
402
|
+
- **Windows** are whole inclusive days, `lookback` before today through
|
|
403
|
+
`lookahead` after, in the pipeline's timezone; declare `end: exclusive`
|
|
404
|
+
for an API whose end date is exclusive.
|
|
405
|
+
- **Loads**: `replace` is a full refresh; `merge` needs `primary_key` (or
|
|
406
|
+
`merge_key`); `window_scope: replace` deletes by the `partition` date, so
|
|
407
|
+
use it only when that is the date the API filters on.
|
|
408
|
+
- **Checks**: `min_rows` is refused on a `cursor` resource; use `freshness`.
|
|
409
|
+
- **A `resolve` child of a windowed parent** needs its own `window`, and the
|
|
410
|
+
parent its window in `params` (`{{ window.start }}`), or backfill chunks
|
|
411
|
+
skip the child.
|
|
412
|
+
|
|
413
|
+
The full guide with complete examples is "Writing a pipeline" in
|
|
414
|
+
`dataspring docs --full`.
|
|
415
|
+
|
|
416
|
+
## 8.7 The managed dbt project
|
|
417
|
+
|
|
418
|
+
The workspace's `models/` is a dbt project (dbt-core 1.12, BigQuery). A new
|
|
419
|
+
org starts with one that already keeps the contract; `dataspring datacore
|
|
420
|
+
pull` shows it. The contract:
|
|
421
|
+
|
|
422
|
+
- `profile: 'dataspring'` (the runner writes the profile; dataset
|
|
423
|
+
`<org>_marts`).
|
|
424
|
+
- Sources at `schema: "{{ env_var('DBT_RAW_SCHEMA') }}"` and `database:
|
|
425
|
+
"{{ env_var('DBT_GCP_PROJECT') }}"`, never a dataset name.
|
|
426
|
+
- `generate_schema_name` returns `DBT_SCHEMA_OVERRIDE` when set (a check:
|
|
427
|
+
`<org>_dev`), else `DBT_SCHEMA_PREFIX` + the layer (`+schema: staging` is
|
|
428
|
+
`<org>_staging`).
|
|
429
|
+
- A `metricflow_time_spine` model with `time_spine:` configured; include it
|
|
430
|
+
in `then.dbt_select`.
|
|
431
|
+
- Semantic models and metrics in YAML anywhere under `models/models/`;
|
|
432
|
+
`metric_edit` creates them in `models/models/semantic/metrics/`.
|
|
433
|
+
- `packages.yml` is fetched by `dbt deps` on every run (`dbt_utils`).
|
|
434
|
+
|
|
435
|
+
A deploy builds nothing: `deploy --then-build`, `datacore build`, or a
|
|
436
|
+
pipeline's `then.dbt_select` does. The full contract is "The managed dbt
|
|
437
|
+
project" in `dataspring docs --full`.
|
|
438
|
+
|
|
342
439
|
## 9. Parameter reference
|
|
343
440
|
|
|
344
441
|
<!-- dispatch-params: semantic_model_edit metric_edit board_edit briefing_edit import_manifest business_context_edit warehouse_edit datacore_edit datacore_run -->
|
|
@@ -587,6 +684,7 @@ MCP tool `datacore_run`; `POST /api/dispatch/datacore_run`. The datacore's check
|
|
|
587
684
|
| Parameter | Type | Required | Description |
|
|
588
685
|
|---|---|---|---|
|
|
589
686
|
| `note` | string | no | Optional note, recorded on the check run |
|
|
687
|
+
| `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
688
|
|
|
591
689
|
**`deploy`** — Promote the checked draft to deployed (admin), or roll back to an earlier deployed snapshot; then_build also rebuilds changed models.
|
|
592
690
|
|
|
@@ -602,9 +700,9 @@ MCP tool `datacore_run`; `POST /api/dispatch/datacore_run`. The datacore's check
|
|
|
602
700
|
| Parameter | Type | Required | Description |
|
|
603
701
|
|---|---|---|---|
|
|
604
702
|
| `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 |
|
|
703
|
+
| `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
704
|
| `cursor` | RunCursorParams | no | {from: <value>}: a cursor to re-pull from |
|
|
607
|
-
| `backfill_chunk` | string | no | Chunk size for a windowed run:
|
|
705
|
+
| `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
706
|
|
|
609
707
|
**`build`** — Rebuild the deployed dbt models on the raw tables already loaded: no resource is loaded.
|
|
610
708
|
|
|
@@ -8,8 +8,8 @@ description: How to query DataSpring correctly to answer analytical questions. U
|
|
|
8
8
|
You are querying DataSpring. Three surfaces are available — pick the
|
|
9
9
|
one the caller is using:
|
|
10
10
|
|
|
11
|
-
- **MCP** — agent connected to
|
|
12
|
-
|
|
11
|
+
- **MCP** — agent connected to DataSpring's MCP server (Claude.ai,
|
|
12
|
+
Claude Code, Cursor, etc.). Tool calls.
|
|
13
13
|
- **CLI** — `dataspring` from the terminal. Shell commands.
|
|
14
14
|
- **REST** — direct HTTP. Scripts, integrations.
|
|
15
15
|
|
|
@@ -80,11 +80,18 @@ which role, is the account's answer on every request. DataSpring has no
|
|
|
80
80
|
parent. `DATASPRING_API_KEY=ds_...` (CI) acts in the key's own org.
|
|
81
81
|
`dataspring whoami --json` says which org a command here acts in and why
|
|
82
82
|
(`org_from`). There is no command that switches org for later commands.
|
|
83
|
-
- **MCP.**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
- **MCP.** The address is `https://dataspring.app/api/mcp/`, with no org
|
|
84
|
+
in it. Claude Code: `claude mcp add --transport http --scope user
|
|
85
|
+
dataspring https://dataspring.app/api/mcp/`, then `/mcp` → *Authenticate*
|
|
86
|
+
(`claude mcp login dataspring` with no browser); Claude.ai: *Customize →
|
|
87
|
+
Connectors* → *Add custom connector* with that URL. The person signs in
|
|
88
|
+
with their Forspring account and DataSpring's consent page names the org
|
|
89
|
+
the connection works in (their only one, or the one they choose); the
|
|
90
|
+
choice is bound to the connection. To work in two orgs, add two
|
|
91
|
+
connections; no tool moves a connection. With no person present (CI), the
|
|
92
|
+
same address takes `Authorization: Bearer ds_...` (a key made in the
|
|
93
|
+
console's connect page, under "Automation and CI"): the key acts in its
|
|
94
|
+
own org, and `?org=` may only repeat it.
|
|
88
95
|
- **REST.** `Authorization: Bearer ds_...`; the key is the org.
|
|
89
96
|
|
|
90
97
|
Never pick an org for the person. When the org is ambiguous you are refused
|
|
@@ -595,12 +602,12 @@ the reason, never on the prose, and pass the fix on verbatim.
|
|
|
595
602
|
| Reason | CLI exit | What you do |
|
|
596
603
|
|---|---|---|
|
|
597
604
|
| `not_signed_in` | 4 | CLI: run `dataspring auth login` and hand the person its URL and code. MCP: reconnect and sign in. Never ask for a password or a key in chat. |
|
|
598
|
-
| `org_required` | 2 | Several orgs, none named. Ask the person which one (the refusal lists them); then `--org`, `DATASPRING_ORG` or `.forspring.json`. Never guess. |
|
|
605
|
+
| `org_required` | 2 | Several orgs, none named. Ask the person which one (the refusal lists them); then `--org`, `DATASPRING_ORG` or `.forspring.json`. MCP: they reconnect and choose it on the consent page. Never guess. |
|
|
599
606
|
| `not_a_member` | 3 | Check the org id's spelling against the list. Otherwise only an owner or admin of that org can add the person, at the Forspring account; say so and stop. |
|
|
600
607
|
| `product_off` | 4 | DataSpring is not switched on for this person in that org. Tell them who can switch it on (`contacts`, `fix_url`); do not retry. |
|
|
601
608
|
| `org_suspended` | 4 | Billing stopped DataSpring for the whole org. Tell them an owner can restart it on the billing page; do not retry. |
|
|
602
609
|
| `role_too_low` | 4 | Their role cannot do this. Offer what it can do (a personal preference or quick metric instead of an org-wide change), or name the role needed. |
|
|
603
|
-
| `org_mismatch` | 4 | The key or connection is for another org than the one named. Use that org's key or
|
|
610
|
+
| `org_mismatch` | 4 | The key or connection (approved for one org) is for another org than the one named. Use that org's key or connection, or drop the conflicting `--org` / `?org=`. |
|
|
604
611
|
| `account_unavailable` | 1 | Forspring's account did not answer, so access could not be checked. Retry once after a few seconds; nothing changed. |
|
|
605
612
|
|
|
606
613
|
Other CLI exits: 0 success, 1 any other failure, 2 a usage error, 64 a
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|