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/__init__.py +15 -0
- cli/_skills/dataspring-author/SKILL.md +401 -0
- cli/_skills/dataspring-consume/SKILL.md +712 -0
- cli/_skills/dataspring-correct/SKILL.md +124 -0
- cli/auth.py +375 -0
- cli/bundled_manifest.py +36 -0
- cli/contract.py +1138 -0
- cli/generated.py +1297 -0
- cli/main.py +3232 -0
- cli/output.py +266 -0
- cli/runtime.py +201 -0
- cli/skills_commands.py +247 -0
- cli/skilltree.py +350 -0
- cli/upgrade.py +66 -0
- cli/version.py +123 -0
- dataspring_cli-0.3.0.dist-info/METADATA +202 -0
- dataspring_cli-0.3.0.dist-info/RECORD +20 -0
- dataspring_cli-0.3.0.dist-info/WHEEL +4 -0
- dataspring_cli-0.3.0.dist-info/entry_points.txt +2 -0
- settings.py +78 -0
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,,
|
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
|