dataspring-cli 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cli/version.py ADDED
@@ -0,0 +1,123 @@
1
+ """``dataspring --version``: the CLI's version, and its skew against the server.
2
+
3
+ The wheel is built against one dispatch manifest (``cli/bundled_manifest.py``
4
+ carries its hash). A deployed server may be newer: it then has operations or
5
+ fields this CLI does not know, and the only way the CLI learns that is by
6
+ comparing hashes with ``GET /api/dispatch`` (decision D19). Not logged in,
7
+ or the server unreachable: the version still prints; the comparison is a
8
+ note, never a failure.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import json
15
+ import time
16
+ from importlib.metadata import PackageNotFoundError, version as _dist_version
17
+ from pathlib import Path
18
+
19
+ import httpx
20
+
21
+ DIST = "dataspring-cli"
22
+ UPGRADE_HINT = "run `dataspring upgrade`"
23
+
24
+ #: Where the server's hash is remembered between commands: next to the
25
+ #: credentials, keyed by a fingerprint of the access token, so it is fetched
26
+ #: once per login session (a token lives about an hour) and not per command.
27
+ CACHE_FILE = Path.home() / ".dataspring" / "manifest-check.json"
28
+ CACHE_TTL_SECONDS = 3600
29
+
30
+
31
+ def cli_version() -> str:
32
+ try:
33
+ return _dist_version(DIST)
34
+ except PackageNotFoundError:
35
+ return "unknown"
36
+
37
+
38
+ def bundled_hash() -> str | None:
39
+ try:
40
+ from cli.bundled_manifest import MANIFEST_HASH
41
+ except ImportError:
42
+ return None
43
+ return MANIFEST_HASH
44
+
45
+
46
+ def fetch_server_hash(base: str, token: str) -> str:
47
+ """The manifest hash ``GET /api/dispatch`` answers, bearer-authenticated."""
48
+ with httpx.Client(timeout=15.0) as client:
49
+ response = client.get(f"{base}/api/dispatch", headers={"Authorization": f"Bearer {token}"})
50
+ response.raise_for_status()
51
+ return response.json()["hash"]
52
+
53
+
54
+ def session_server_hash(base: str, token: str) -> str | None:
55
+ """The server's manifest hash for this login session, fetched at most
56
+ once per token (and once an hour); ``None`` when it cannot be read."""
57
+ fingerprint = hashlib.sha256(token.encode("utf-8")).hexdigest()[:16]
58
+ try:
59
+ cached = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
60
+ if cached.get("token") == fingerprint and time.time() - float(cached.get("at", 0)) < CACHE_TTL_SECONDS:
61
+ return cached.get("hash")
62
+ except (OSError, ValueError, AttributeError):
63
+ pass
64
+ try:
65
+ server = fetch_server_hash(base, token)
66
+ except (httpx.HTTPError, KeyError, ValueError):
67
+ return None
68
+ try:
69
+ CACHE_FILE.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
70
+ CACHE_FILE.write_text(json.dumps({"token": fingerprint, "hash": server, "at": time.time()}), encoding="utf-8")
71
+ except OSError:
72
+ pass
73
+ return server
74
+
75
+
76
+ _hinted = False
77
+
78
+
79
+ def skew_hint(base: str, token: str) -> str | None:
80
+ """The one line a command prints when the server's manifest differs from
81
+ the one this CLI was built against; once per process, ``None`` when in
82
+ sync or when the server could not be compared."""
83
+ global _hinted
84
+ if _hinted:
85
+ return None
86
+ server = session_server_hash(base, token)
87
+ bundled = bundled_hash()
88
+ if server is None or bundled is None or server == bundled:
89
+ return None
90
+ _hinted = True
91
+ return skew_lines(server, bundled)[1]
92
+
93
+
94
+ def _short(value: str | None) -> str:
95
+ return value[:12] if value else "(none)"
96
+
97
+
98
+ def skew_lines(server: str | None, bundled: str | None, *, server_note: str | None = None) -> list[str]:
99
+ """The manifest lines of the version report."""
100
+ if server_note:
101
+ return [f"manifest: cli {_short(bundled)}; {server_note}"]
102
+ if server == bundled:
103
+ return [f"manifest: server {_short(server)}, cli {_short(bundled)} (in sync)"]
104
+ return [
105
+ f"manifest: server {_short(server)}, cli {_short(bundled)}",
106
+ f"the server has operations or fields this CLI does not know; {UPGRADE_HINT}",
107
+ ]
108
+
109
+
110
+ def version_report(*, base: str, token: str | None) -> list[str]:
111
+ """Everything ``--version`` prints, one entry per line."""
112
+ lines = [f"{DIST} {cli_version()}"]
113
+ bundled = bundled_hash()
114
+ if token is None:
115
+ lines += skew_lines(None, bundled, server_note="log in to compare with the server")
116
+ return lines
117
+ try:
118
+ server = fetch_server_hash(base, token)
119
+ except (httpx.HTTPError, KeyError, ValueError) as e:
120
+ lines += skew_lines(None, bundled, server_note=f"server not compared ({type(e).__name__})")
121
+ return lines
122
+ lines += skew_lines(server, bundled)
123
+ return lines
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.5
2
+ Name: dataspring-cli
3
+ Version: 0.3.0
4
+ Summary: DataSpring CLI - Query metrics and manage dashboards from the terminal
5
+ Project-URL: Homepage, https://dataspring.app
6
+ Project-URL: Documentation, https://dataspring.app/docs
7
+ Author-email: DataSpring <support@dataspring.app>
8
+ License: Proprietary
9
+ Keywords: analytics,bi,dashboard,dbt,metricflow,metrics
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: Other/Proprietary License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
22
+ Requires-Python: >=3.12
23
+ Requires-Dist: google-auth-oauthlib>=1.4.1
24
+ Requires-Dist: google-auth>=2.57.0
25
+ Requires-Dist: httpx>=0.28.1
26
+ Requires-Dist: pydantic-settings>=2.15.0
27
+ Requires-Dist: pydantic>=2.13.4
28
+ Requires-Dist: pyyaml>=6.0.3
29
+ Requires-Dist: rich>=15.0.0
30
+ Requires-Dist: typer>=0.27.1
31
+ Description-Content-Type: text/markdown
32
+
33
+ # DataSpring CLI
34
+
35
+ Query metrics and manage dashboards from the terminal.
36
+
37
+ ## Installation
38
+
39
+ ### Using uv (Recommended)
40
+
41
+ ```bash
42
+ # Install from local build
43
+ uv tool install ./backend
44
+
45
+ # Or install from GitHub (once published)
46
+ uv tool install git+https://github.com/dataspring/dataspring#subdirectory=backend
47
+ ```
48
+
49
+ ### Using pip
50
+
51
+ ```bash
52
+ pip install ./backend
53
+ ```
54
+
55
+ ## Quick Start
56
+
57
+ ```bash
58
+ # Login with Google OAuth
59
+ dataspring login
60
+
61
+ # Check who you're logged in as
62
+ dataspring whoami
63
+
64
+ # List available metrics
65
+ dataspring metrics list
66
+
67
+ # Query metrics
68
+ dataspring query -m total_revenue -g month --limit 5
69
+
70
+ # Get visualization suggestions
71
+ dataspring query -m total_revenue -g month --suggest-viz
72
+
73
+ # Export to JSON
74
+ dataspring query -m total_revenue --format json > data.json
75
+ ```
76
+
77
+ ## Commands
78
+
79
+ ### Authentication
80
+
81
+ ```bash
82
+ dataspring login # Login via Google OAuth (opens browser)
83
+ dataspring whoami # Show current user and organization
84
+ dataspring logout # Clear stored credentials
85
+ dataspring org list # List your organizations
86
+ dataspring org switch ID # Switch to a different organization
87
+ ```
88
+
89
+ ### Querying Metrics
90
+
91
+ ```bash
92
+ # List available metrics
93
+ dataspring metrics list
94
+ dataspring metrics list --format json
95
+
96
+ # Get metric details
97
+ dataspring metrics show total_revenue
98
+
99
+ # Query data
100
+ dataspring query -m revenue -g month # Monthly revenue
101
+ dataspring query -m revenue -d region --limit 10 # By region
102
+ dataspring query -m revenue -m orders -g week # Multiple metrics
103
+ dataspring query -m revenue --start 2024-01-01 --end 2024-12-31
104
+
105
+ # With visualization suggestion
106
+ dataspring query -m revenue -g month --suggest-viz
107
+
108
+ # List dimensions
109
+ dataspring dimensions list
110
+ ```
111
+
112
+ ### Dashboard Management
113
+
114
+ ```bash
115
+ # List dashboards
116
+ dataspring dashboards list
117
+
118
+ # Show dashboard details
119
+ dataspring dashboards show DASHBOARD_ID
120
+
121
+ # Create a new dashboard
122
+ dataspring dashboards create "My Dashboard"
123
+ dataspring dashboards create "Team Metrics" -v org # Visible to team
124
+
125
+ # Delete a dashboard
126
+ dataspring dashboards delete DASHBOARD_ID
127
+ dataspring dashboards delete DASHBOARD_ID --yes # Skip confirmation
128
+ ```
129
+
130
+ ### Manifest Management (Admin Only)
131
+
132
+ ```bash
133
+ # View manifest status
134
+ dataspring manifest status
135
+
136
+ # Export manifest
137
+ dataspring manifest export -o manifest.yaml
138
+
139
+ # Import manifest
140
+ dataspring manifest upload manifest.yaml
141
+ dataspring manifest upload manifest.yaml --force # Overwrite conflicts
142
+
143
+ # Semantic models
144
+ dataspring models list
145
+ dataspring models show orders
146
+ dataspring models create model.yaml
147
+ dataspring models update orders updated.yaml
148
+ dataspring models delete orders
149
+
150
+ # Metrics
151
+ dataspring metrics create metric.yaml
152
+ dataspring metrics update total_revenue updated.yaml
153
+ dataspring metrics delete total_revenue
154
+ dataspring metrics preview metric.yaml # Test before saving
155
+ ```
156
+
157
+ ## Output Formats
158
+
159
+ All commands support `--format`:
160
+
161
+ | Format | Description |
162
+ |--------|-------------|
163
+ | `table` | Human-readable ASCII table (default) |
164
+ | `json` | JSON output for scripting |
165
+ | `yaml` | YAML output for config editing |
166
+
167
+ ```bash
168
+ dataspring metrics list --format json | jq '.[0]'
169
+ dataspring dashboards show abc123 --format yaml > dashboard.yaml
170
+ ```
171
+
172
+ ## Configuration
173
+
174
+ Credentials are stored in `~/.dataspring/`:
175
+
176
+ ```
177
+ ~/.dataspring/
178
+ ├── credentials.json # OAuth tokens (encrypted)
179
+ └── config.json # User preferences
180
+ ```
181
+
182
+ ## Development
183
+
184
+ ```bash
185
+ # Build the wheel
186
+ cd backend
187
+ uv build
188
+
189
+ # Install locally for testing
190
+ uv tool install dist/dataspring_cli-*.whl --force
191
+
192
+ # Run tests
193
+ uv run pytest tests/ -v
194
+ ```
195
+
196
+ ## Environment Variables
197
+
198
+ | Variable | Description |
199
+ |----------|-------------|
200
+ | `FIRESTORE_EMULATOR_HOST` | Use Firestore emulator |
201
+ | `FIREBASE_AUTH_EMULATOR_HOST` | Use Auth emulator |
202
+ | `ENV` | Environment: `production`, `development`, `test` |
@@ -0,0 +1,20 @@
1
+ cli/__init__.py,sha256=RJi9Y5W2jatsoYYtgi6Iu6UkHEi55lvsuNBdsY7pRXc,453
2
+ cli/auth.py,sha256=rg6CIt0BcG7Ags0WRsFsso3ol7Au7dYMkoUloJq3AGQ,13150
3
+ cli/bundled_manifest.py,sha256=KLtAHwOJHT-5l_B_v3uJJNIdvvILtRjBbOz8LayocLw,988
4
+ cli/contract.py,sha256=68XWcf9EgsIFqey4kzzpJCoR01Pkl6tNmqRDjcX7yrk,40174
5
+ cli/generated.py,sha256=MtN1u2oM1xl_v-Q2cZ75lkdI64b1DpaGJ1vsM2H6UmI,84414
6
+ cli/main.py,sha256=tKvRk-FfCvxd1vl-TVCcdVZRawknJbdwL_NMwe3uuwM,112840
7
+ cli/output.py,sha256=DOficOs7Dfi4hbR6i-39HFxwZBk1FI_oUZeZP57XPjs,6853
8
+ cli/runtime.py,sha256=um_WqkG911j_cJEmpLlaU-csrCortTaYbFY024VvVAE,7456
9
+ cli/skills_commands.py,sha256=-ZvMN-w9T6P2Tc05iAY4THgX5vMsAF6jj9-oTjNOD3Q,8868
10
+ cli/skilltree.py,sha256=r-BJ2p1U5OVhIEZbkZoOgtJ2tuDo00CCQcd1fu3O1f8,12807
11
+ cli/upgrade.py,sha256=UfNPzwy0lal5D6NBzS3lgzDTNUOrcLNIvW1lfQpn3pU,2095
12
+ cli/version.py,sha256=ARcAiq_TgTntMsJC_9isC9gcBymLQmkUcVm1zoyJy9k,4467
13
+ settings.py,sha256=Ok4qe_nkAxhvq8oXHH74zq99B9hnSrvl738GIG0F95s,3520
14
+ cli/_skills/dataspring-author/SKILL.md,sha256=GdZvc_CpocT7UUmwEST_IUBv1qg85jVZ6gwPrlKIM4c,19693
15
+ cli/_skills/dataspring-consume/SKILL.md,sha256=rQ-xwBesfu91k9OAaYxLMoOwQxSIqW9mXBNwTUvi8XI,38829
16
+ cli/_skills/dataspring-correct/SKILL.md,sha256=SYgHkHKhn2fis1kc7m_cnDWss_lSZ1eJXj1Br8I0T6E,6448
17
+ dataspring_cli-0.3.0.dist-info/METADATA,sha256=p-tp8r74D4dOIk0jyL9G_jUggmn5BMg_05ANye85G-8,5091
18
+ dataspring_cli-0.3.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
19
+ dataspring_cli-0.3.0.dist-info/entry_points.txt,sha256=X4YdpJ6kFTyolhrFZNlzkXT3dhE_H_4E3YUHna6oXUM,44
20
+ dataspring_cli-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dataspring = cli.main:app
settings.py ADDED
@@ -0,0 +1,78 @@
1
+ """Application settings using Pydantic BaseSettings."""
2
+
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+
5
+
6
+ class Settings(BaseSettings):
7
+ """Application settings loaded from environment variables and .env file."""
8
+
9
+ model_config = SettingsConfigDict(
10
+ env_file=".env",
11
+ env_file_encoding="utf-8",
12
+ extra="ignore",
13
+ )
14
+
15
+ # The Google OAuth client the CLI logs in with. A Google access token is
16
+ # only accepted as a DataSpring credential when it was minted for this
17
+ # client (see auth.verify_oauth_token); must match
18
+ # cli.auth.CLIAuthManager.CLI_OAUTH_CLIENT_ID.
19
+ cli_oauth_client_id: str = (
20
+ "910366018047-ekplje5p344nbq60o8vqk88378turj52.apps.googleusercontent.com"
21
+ )
22
+
23
+ # MCP OAuth Configuration (for Claude.ai integration)
24
+ # Create OAuth 2.0 credentials in Google Cloud Console:
25
+ # 1. Go to APIs & Services > Credentials
26
+ # 2. Create OAuth 2.0 Client ID (Web application)
27
+ # 3. Add authorized redirect URI: <your-server-url>/mcp/auth/callback
28
+ mcp_oauth_client_id: str | None = None
29
+ mcp_oauth_client_secret: str | None = None
30
+ mcp_server_base_url: str = "https://dataspring.app/api"
31
+ # JWT signing key for MCP tokens - MUST be persistent across server restarts
32
+ # Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))"
33
+ mcp_jwt_signing_key: str | None = None
34
+ # Public origin of the DataSpring dashboard SPA (used for MCP Apps embed URLs).
35
+ # In prod: https://dataspring.app. Locally: http://localhost:5173.
36
+ dataspring_frontend_base_url: str = "https://dataspring.app"
37
+
38
+ # Internal secret for Cloud Function -> Backend communication
39
+ # Used by the scheduled reports executor to authenticate with the backend
40
+ # Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))"
41
+ internal_secret: str | None = None
42
+
43
+ # Deep health probe authentication and Nightnurse heartbeat sink.
44
+ # HEALTH_PROBE_SECRET is sent by Cloud Scheduler. NIGHTNURSE_PING_URL is a
45
+ # bearer URL, stored in Secret Manager, that receives success/fail pings.
46
+ health_probe_secret: str | None = None
47
+ nightnurse_ping_url: str | None = None
48
+
49
+ # BigQuery cost controls (warehouse.BigQueryClient; review item 16).
50
+ # Every job the backend submits carries these. A per-org override for the
51
+ # byte cap lives on the warehouse config document (`bigquery.max_bytes_billed`).
52
+ #
53
+ # The most one query may bill, in bytes. BigQuery refuses a job that would
54
+ # scan more, before it runs, and the refusal reaches the caller as
55
+ # `error_code: bytes_billed_exceeded`. 10 GiB is ~$0.06 on on-demand
56
+ # pricing and far above any dashboard or agent query on a modelled table.
57
+ bigquery_max_bytes_billed: int = 10 * 1024**3
58
+ # Server-side job timeout (BigQuery stops the job) and the client-side wait
59
+ # on its result; on expiry the job is cancelled, never abandoned.
60
+ bigquery_job_timeout_seconds: int = 60
61
+ # Rows a query may return when the caller sets no limit. Applies to every
62
+ # warehouse (BigQuery via `result(max_results=)`, DuckDB via `fetchmany`).
63
+ query_max_rows: int = 10_000
64
+
65
+ # Feature flags
66
+ schedules_enabled: bool = False # Scheduled reports (email delivery not implemented yet)
67
+
68
+
69
+ # Singleton instance
70
+ _settings: Settings | None = None
71
+
72
+
73
+ def get_settings() -> Settings:
74
+ """Get the singleton settings instance."""
75
+ global _settings
76
+ if _settings is None:
77
+ _settings = Settings()
78
+ return _settings