analyticsreceipts 0.0.2__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.
@@ -0,0 +1,374 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/apache/ossie/core-spec/ossie-schema.json",
4
+ "title": "Apache Ossie Core Metadata Specification",
5
+ "description": "JSON Schema for validating a single Apache Ossie semantic model document",
6
+ "type": "object",
7
+ "properties": {
8
+ "version": {
9
+ "type": "string",
10
+ "const": "0.2.0.dev0",
11
+ "description": "Apache Ossie specification version"
12
+ },
13
+ "name": {
14
+ "$ref": "#/$defs/SemanticModel/properties/name"
15
+ },
16
+ "description": {
17
+ "$ref": "#/$defs/SemanticModel/properties/description"
18
+ },
19
+ "ai_context": {
20
+ "$ref": "#/$defs/SemanticModel/properties/ai_context"
21
+ },
22
+ "datasets": {
23
+ "$ref": "#/$defs/SemanticModel/properties/datasets"
24
+ },
25
+ "relationships": {
26
+ "$ref": "#/$defs/SemanticModel/properties/relationships"
27
+ },
28
+ "metrics": {
29
+ "$ref": "#/$defs/SemanticModel/properties/metrics"
30
+ },
31
+ "custom_extensions": {
32
+ "$ref": "#/$defs/SemanticModel/properties/custom_extensions"
33
+ }
34
+ },
35
+ "required": ["version", "name", "datasets"],
36
+ "additionalProperties": false,
37
+ "$defs": {
38
+ "Dialect": {
39
+ "type": "string",
40
+ "enum": ["ANSI_SQL", "SNOWFLAKE", "MDX", "TABLEAU", "DATABRICKS", "MAQL", "BIGQUERY", "SIGMA", "THOUGHTSPOT", "DAX", "OSSIE_SQL_2026"],
41
+ "description": "Supported SQL and expression language dialects"
42
+ },
43
+ "Vendor": {
44
+ "type": "string",
45
+ "examples": ["COMMON", "SNOWFLAKE", "SALESFORCE", "DBT", "DATABRICKS", "GOODDATA", "WISDOM", "POWER_BI"],
46
+ "description": "Vendor name for custom extensions. Any string value is accepted."
47
+ },
48
+ "AIContext": {
49
+ "description": "Additional context for AI tools",
50
+ "oneOf": [
51
+ {
52
+ "type": "string"
53
+ },
54
+ {
55
+ "type": "object",
56
+ "properties": {
57
+ "instructions": {
58
+ "type": "string",
59
+ "description": "Instructions for AI on how to use this entity"
60
+ },
61
+ "synonyms": {
62
+ "type": "array",
63
+ "items": {
64
+ "type": "string"
65
+ },
66
+ "description": "Alternative names and terms"
67
+ },
68
+ "examples": {
69
+ "type": "array",
70
+ "items": {
71
+ "type": "string"
72
+ },
73
+ "description": "Sample questions or use cases"
74
+ }
75
+ },
76
+ "additionalProperties": true
77
+ }
78
+ ]
79
+ },
80
+ "CustomExtension": {
81
+ "type": "object",
82
+ "description": "Vendor-specific attributes for extensibility",
83
+ "properties": {
84
+ "vendor_name": {
85
+ "$ref": "#/$defs/Vendor"
86
+ },
87
+ "data": {
88
+ "type": "string",
89
+ "description": "JSON string containing vendor-specific data"
90
+ }
91
+ },
92
+ "required": ["vendor_name", "data"],
93
+ "additionalProperties": false
94
+ },
95
+ "DialectExpression": {
96
+ "type": "object",
97
+ "description": "Expression in a specific dialect",
98
+ "properties": {
99
+ "dialect": {
100
+ "$ref": "#/$defs/Dialect"
101
+ },
102
+ "expression": {
103
+ "type": "string",
104
+ "description": "SQL or dialect-specific expression"
105
+ }
106
+ },
107
+ "required": ["dialect", "expression"],
108
+ "additionalProperties": false
109
+ },
110
+ "Expression": {
111
+ "type": "object",
112
+ "description": "Expression definition with multi-dialect support",
113
+ "properties": {
114
+ "dialects": {
115
+ "type": "array",
116
+ "items": {
117
+ "$ref": "#/$defs/DialectExpression"
118
+ },
119
+ "minItems": 1
120
+ }
121
+ },
122
+ "required": ["dialects"],
123
+ "additionalProperties": false
124
+ },
125
+ "DataType": {
126
+ "type": "string",
127
+ "enum": [
128
+ "String",
129
+ "Integer",
130
+ "Decimal",
131
+ "Float",
132
+ "Boolean",
133
+ "Date",
134
+ "Time",
135
+ "DateTime",
136
+ "DateTimeTz",
137
+ "Opaque"
138
+ ],
139
+ "description": "Logical data type for fields and metrics, independent of role (e.g. dimension vs fact) and physical representation. `Decimal` is exact base-10 with unspecified precision and scale; `Float` is approximate. `DateTime` has no timezone or offset, while `DateTimeTz` identifies an instant using offset or timezone context but does not guarantee preservation of a named timezone. Omit `datatype` when unknown; use `Opaque` plus `custom_extensions` for a known type outside the portable vocabulary."
140
+ },
141
+ "Dimension": {
142
+ "type": "object",
143
+ "description": "Dimension metadata",
144
+ "properties": {
145
+ "is_time": {
146
+ "type": "boolean",
147
+ "description": "Temporal-role marker. When true, consumers that distinguish time dimensions (e.g. for time-series analysis or temporal filtering) should treat this field as a time dimension. This is a *role* flag, independent of the field's data type: a field with `is_time: true` may carry any `datatype` (e.g. `Integer` for a year grain, `String` for a month name, as well as temporal data types). When `is_time` is unset, it defaults to `true` if `datatype` is one of `Date`, `Time`, `DateTime`, or `DateTimeTz`, and `false` otherwise. Set `is_time: false` explicitly to opt a temporal-typed column (such as an audit timestamp) out of time-dimension treatment."
148
+ }
149
+ },
150
+ "additionalProperties": false
151
+ },
152
+ "Field": {
153
+ "type": "object",
154
+ "description": "Row-level attribute for grouping, filtering, and metric expressions",
155
+ "properties": {
156
+ "name": {
157
+ "type": "string",
158
+ "minLength": 1,
159
+ "description": "Unique identifier for the field within the dataset"
160
+ },
161
+ "expression": {
162
+ "$ref": "#/$defs/Expression"
163
+ },
164
+ "dimension": {
165
+ "$ref": "#/$defs/Dimension"
166
+ },
167
+ "label": {
168
+ "type": "string",
169
+ "description": "Label for categorization"
170
+ },
171
+ "description": {
172
+ "type": "string",
173
+ "description": "Human-readable description"
174
+ },
175
+ "datatype": {
176
+ "$ref": "#/$defs/DataType"
177
+ },
178
+ "ai_context": {
179
+ "$ref": "#/$defs/AIContext"
180
+ },
181
+ "custom_extensions": {
182
+ "type": "array",
183
+ "items": {
184
+ "$ref": "#/$defs/CustomExtension"
185
+ }
186
+ }
187
+ },
188
+ "required": ["name", "expression"],
189
+ "additionalProperties": false
190
+ },
191
+ "Dataset": {
192
+ "type": "object",
193
+ "description": "Logical dataset representing a business entity (fact or dimension table)",
194
+ "properties": {
195
+ "name": {
196
+ "type": "string",
197
+ "minLength": 1,
198
+ "description": "Unique identifier for the dataset"
199
+ },
200
+ "source": {
201
+ "type": "string",
202
+ "minLength": 1,
203
+ "description": "Reference to underlying physical table/view (database.schema.table) or query"
204
+ },
205
+ "primary_key": {
206
+ "type": "array",
207
+ "items": {
208
+ "type": "string"
209
+ },
210
+ "description": "Primary key columns (single or composite)"
211
+ },
212
+ "unique_keys": {
213
+ "type": "array",
214
+ "items": {
215
+ "type": "array",
216
+ "items": {
217
+ "type": "string"
218
+ }
219
+ },
220
+ "description": "Array of unique key definitions (each can be single or composite)"
221
+ },
222
+ "description": {
223
+ "type": "string",
224
+ "description": "Human-readable description"
225
+ },
226
+ "ai_context": {
227
+ "$ref": "#/$defs/AIContext"
228
+ },
229
+ "fields": {
230
+ "type": "array",
231
+ "items": {
232
+ "$ref": "#/$defs/Field"
233
+ }
234
+ },
235
+ "custom_extensions": {
236
+ "type": "array",
237
+ "items": {
238
+ "$ref": "#/$defs/CustomExtension"
239
+ }
240
+ }
241
+ },
242
+ "required": ["name", "source"],
243
+ "additionalProperties": false
244
+ },
245
+ "Relationship": {
246
+ "type": "object",
247
+ "description": "Foreign key relationship between datasets",
248
+ "properties": {
249
+ "name": {
250
+ "type": "string",
251
+ "minLength": 1,
252
+ "description": "Unique identifier for the relationship"
253
+ },
254
+ "from": {
255
+ "type": "string",
256
+ "minLength": 1,
257
+ "description": "Dataset on the many side of the relationship"
258
+ },
259
+ "to": {
260
+ "type": "string",
261
+ "minLength": 1,
262
+ "description": "Dataset on the one side of the relationship"
263
+ },
264
+ "from_columns": {
265
+ "type": "array",
266
+ "items": {
267
+ "type": "string"
268
+ },
269
+ "minItems": 1,
270
+ "description": "Foreign key columns in the 'from' dataset"
271
+ },
272
+ "to_columns": {
273
+ "type": "array",
274
+ "items": {
275
+ "type": "string"
276
+ },
277
+ "minItems": 1,
278
+ "description": "Primary/unique key columns in the 'to' dataset"
279
+ },
280
+ "ai_context": {
281
+ "$ref": "#/$defs/AIContext"
282
+ },
283
+ "custom_extensions": {
284
+ "type": "array",
285
+ "items": {
286
+ "$ref": "#/$defs/CustomExtension"
287
+ }
288
+ }
289
+ },
290
+ "required": ["name", "from", "to", "from_columns", "to_columns"],
291
+ "additionalProperties": false
292
+ },
293
+ "Metric": {
294
+ "type": "object",
295
+ "description": "Quantitative measure defined on business data",
296
+ "properties": {
297
+ "name": {
298
+ "type": "string",
299
+ "minLength": 1,
300
+ "description": "Unique identifier for the metric"
301
+ },
302
+ "expression": {
303
+ "$ref": "#/$defs/Expression"
304
+ },
305
+ "description": {
306
+ "type": "string",
307
+ "description": "Human-readable description of what the metric measures"
308
+ },
309
+ "datatype": {
310
+ "$ref": "#/$defs/DataType"
311
+ },
312
+ "ai_context": {
313
+ "$ref": "#/$defs/AIContext"
314
+ },
315
+ "custom_extensions": {
316
+ "type": "array",
317
+ "items": {
318
+ "$ref": "#/$defs/CustomExtension"
319
+ }
320
+ }
321
+ },
322
+ "required": ["name", "expression"],
323
+ "additionalProperties": false
324
+ },
325
+ "SemanticModel": {
326
+ "type": "object",
327
+ "description": "Top-level container representing a complete semantic model",
328
+ "properties": {
329
+ "name": {
330
+ "type": "string",
331
+ "minLength": 1,
332
+ "description": "Unique identifier for the semantic model"
333
+ },
334
+ "description": {
335
+ "type": "string",
336
+ "description": "Human-readable description"
337
+ },
338
+ "ai_context": {
339
+ "$ref": "#/$defs/AIContext"
340
+ },
341
+ "datasets": {
342
+ "type": "array",
343
+ "items": {
344
+ "$ref": "#/$defs/Dataset"
345
+ },
346
+ "minItems": 1,
347
+ "description": "Collection of logical datasets"
348
+ },
349
+ "relationships": {
350
+ "type": "array",
351
+ "items": {
352
+ "$ref": "#/$defs/Relationship"
353
+ },
354
+ "description": "Defines how datasets are connected"
355
+ },
356
+ "metrics": {
357
+ "type": "array",
358
+ "items": {
359
+ "$ref": "#/$defs/Metric"
360
+ },
361
+ "description": "Quantifiable measures spanning datasets"
362
+ },
363
+ "custom_extensions": {
364
+ "type": "array",
365
+ "items": {
366
+ "$ref": "#/$defs/CustomExtension"
367
+ }
368
+ }
369
+ },
370
+ "required": ["name", "datasets"],
371
+ "additionalProperties": false
372
+ }
373
+ }
374
+ }
@@ -0,0 +1,185 @@
1
+ """Read-only execution with caps, and the receipt emitted on the execution path.
2
+
3
+ The connection is opened read-only with external access disabled and the configuration locked, so
4
+ a statement cannot read or write files outside the database (no read_csv, no COPY TO) or loosen
5
+ those settings. Every result is capped by rows and by time. The same instance is capped in memory, threads
6
+ and temp-file disk use (set before the lock), and at most MAX_CONCURRENT_RUNS statements run at once.
7
+ """
8
+
9
+ import datetime as dt
10
+ import decimal
11
+ import hashlib
12
+ import json
13
+ import threading
14
+ import time
15
+ import uuid
16
+
17
+ import duckdb
18
+
19
+ RECEIPT_VERSION = "2" # 2: `compiler` required and tied to `kind`; no non_additive_guard check
20
+ ROW_CAP = 1000
21
+ TIMEOUT_S = 20.0
22
+ MEMORY_LIMIT = "1GB" # per DuckDB instance, shared by every cursor on it
23
+ THREADS = 2
24
+ MAX_TEMP_DIRECTORY_SIZE = "0B" # no spilling to disk: a query over the memory limit fails instead
25
+ MAX_CONCURRENT_RUNS = 4 # per process; further calls wait (up to their timeout) for a slot
26
+ _RUN_SLOTS = threading.BoundedSemaphore(MAX_CONCURRENT_RUNS)
27
+ _LIMITS = {"memory_limit": f"'{MEMORY_LIMIT}'", "threads": str(THREADS),
28
+ "max_temp_directory_size": f"'{MAX_TEMP_DIRECTORY_SIZE}'"}
29
+
30
+
31
+ def connect(db_path):
32
+ con = duckdb.connect(db_path, read_only=True, config={"enable_external_access": False})
33
+ # a second connection to the same file in one process shares the (already locked) instance
34
+ if not con.execute("SELECT current_setting('lock_configuration')").fetchone()[0]:
35
+ for name, value in _LIMITS.items():
36
+ con.execute(f"SET {name} = {value}")
37
+ con.execute("SET lock_configuration = true")
38
+ if _settings(con) != _expected_settings():
39
+ raise RuntimeError("this database instance was locked with other resource limits; refusing to serve")
40
+ if con.execute("SELECT current_setting('enable_external_access')").fetchone()[0]:
41
+ raise RuntimeError("external access is enabled on this database instance; refusing to serve")
42
+ return con
43
+
44
+
45
+ def _settings(con):
46
+ return {n: con.execute("SELECT current_setting(?)", [n]).fetchone()[0] for n in _LIMITS}
47
+
48
+
49
+ _EXPECTED = []
50
+
51
+
52
+ def _expected_settings():
53
+ # how DuckDB reports the limits (e.g. '1GB' reads back as '953.6 MiB'), from a scratch in-memory instance
54
+ if not _EXPECTED:
55
+ scratch = duckdb.connect()
56
+ for name, value in _LIMITS.items():
57
+ scratch.execute(f"SET {name} = {value}")
58
+ _EXPECTED.append(_settings(scratch))
59
+ scratch.close()
60
+ return _EXPECTED[0]
61
+
62
+
63
+ def _cell(v):
64
+ if isinstance(v, decimal.Decimal):
65
+ return float(v)
66
+ if isinstance(v, (dt.date, dt.datetime)):
67
+ return v.isoformat()
68
+ return v
69
+
70
+
71
+ def file_sha256(path):
72
+ h = hashlib.sha256()
73
+ with open(path, "rb") as f:
74
+ for chunk in iter(lambda: f.read(1 << 20), b""):
75
+ h.update(chunk)
76
+ return h.hexdigest()
77
+
78
+
79
+ class Executor:
80
+ def __init__(self, db_path):
81
+ self.db_path = db_path
82
+ self.con = connect(db_path)
83
+ # a warehouse built by analyticsreceipts.load carries snapshot_meta; any other warehouse is
84
+ # fingerprinted by its file hash alone, and its data range comes from the semantic model
85
+ has_meta = self.con.execute("SELECT count(*) FROM information_schema.tables "
86
+ "WHERE table_name = 'snapshot_meta'").fetchone()[0]
87
+ if has_meta:
88
+ meta = self.con.execute("SELECT xlsx_sha256, max_invoice_ts FROM snapshot_meta").fetchone()
89
+ self.snapshot = {"source_xlsx_sha256": meta[0], "max_invoice_ts": meta[1].isoformat(),
90
+ "db_file_sha256": file_sha256(db_path)}
91
+ else:
92
+ self.snapshot = {"db_file_sha256": file_sha256(db_path)}
93
+ self.data_range = None # (start_date, end_date, end_label), set by the server from the model
94
+
95
+ def set_data_range(self, start, end):
96
+ label = self.snapshot.get("max_invoice_ts") or end
97
+ self.data_range = (start, end, label)
98
+
99
+ def run(self, sql, params=(), cap=ROW_CAP, timeout_s=TIMEOUT_S):
100
+ """Returns (columns, rows, truncated, error). Never raises for a bad statement.
101
+
102
+ Each call runs on its own cursor (a separate connection to the same read-only database), and
103
+ the timeout interrupts only that cursor. Concurrent calls (the MCP SDK runs sync tools on worker
104
+ threads) therefore can neither read each other's results nor cancel each other's queries. At most
105
+ MAX_CONCURRENT_RUNS calls run at once; a call that gets no slot within timeout_s returns an error.
106
+ """
107
+ if not _RUN_SLOTS.acquire(timeout=timeout_s):
108
+ return [], [], False, f"Busy: {MAX_CONCURRENT_RUNS} queries already running; no slot within {timeout_s} s"
109
+ try:
110
+ return self._run(sql, params, cap, timeout_s)
111
+ finally:
112
+ _RUN_SLOTS.release()
113
+
114
+ def _run(self, sql, params, cap, timeout_s):
115
+ cur = self.con.cursor()
116
+ timer = threading.Timer(timeout_s, cur.interrupt)
117
+ timer.start()
118
+ try:
119
+ cur.execute(sql, list(params))
120
+ cols = [d[0] for d in (cur.description or [])]
121
+ rows = cur.fetchmany(cap + 1)
122
+ except Exception as e: # the statement failed; the caller records it
123
+ return [], [], False, f"{type(e).__name__}: {str(e).splitlines()[0][:300]}"
124
+ finally:
125
+ timer.cancel()
126
+ cur.close()
127
+ truncated = len(rows) > cap
128
+ rows = [[_cell(v) for v in r] for r in rows[:cap]]
129
+ return cols, rows, truncated, None
130
+
131
+
132
+ def result_sha256(columns, rows):
133
+ return hashlib.sha256(json.dumps({"columns": columns, "rows": rows}, sort_keys=True, default=str).encode()).hexdigest()
134
+
135
+
136
+ def make_receipt(*, executor, kind, request, metric_id, metric_def_version, semantic_model_sha256, sql, params,
137
+ sql_sha256, columns, rows, truncated, error, checks, caveats, server_version, started, compiler=None):
138
+ return {
139
+ "receipt_version": RECEIPT_VERSION,
140
+ "kind": kind, # "governed_metric" (MCP query_metric), "control_metric" (planted compiler) or "raw_sql"
141
+ "compiler": compiler, # "governed", "planted:<bug>" for a control run, None for raw SQL
142
+ "request": request,
143
+ "metric_id": metric_id,
144
+ "metric_def_version": metric_def_version,
145
+ "semantic_model_sha256": semantic_model_sha256,
146
+ "compiled_sql": sql,
147
+ "params": list(params),
148
+ "compiled_sql_sha256": sql_sha256 or hashlib.sha256((sql + "\n" + json.dumps(list(params))).encode()).hexdigest(),
149
+ "engine": {"name": "duckdb", "version": duckdb.__version__},
150
+ "snapshot": executor.snapshot,
151
+ "result": {"columns": columns, "rows": rows},
152
+ "result_sha256": None if error else result_sha256(columns, rows),
153
+ "row_count": len(rows),
154
+ "truncated": truncated,
155
+ "error": error,
156
+ "checks": checks,
157
+ "uncertainty": {"caveats_applied": caveats},
158
+ "run": {"request_id": uuid.uuid4().hex, "server_version": server_version, "started_at": started,
159
+ "elapsed_s": round(time.time() - _epoch(started), 3)},
160
+ }
161
+
162
+
163
+ def _epoch(iso):
164
+ return dt.datetime.fromisoformat(iso).timestamp()
165
+
166
+
167
+ def now_iso():
168
+ return dt.datetime.now().isoformat(timespec="milliseconds")
169
+
170
+
171
+ def period_checks(data_range, time_range, comparison_range=None):
172
+ """Checks that make an incomplete or out-of-data period visible in the receipt.
173
+ data_range = (first data date, last data date, label for the last data point)."""
174
+ min_date, max_date, end_label = data_range
175
+ out = []
176
+ for name, r in (("time_range", time_range), ("comparison_range", comparison_range)):
177
+ if not r:
178
+ continue
179
+ complete = r["end"] <= max_date
180
+ out.append({"name": f"{name}_complete", "success": complete,
181
+ "expected": f"end <= last data date {max_date}", "actual": r["end"],
182
+ "note": None if complete else f"data ends {end_label}; the period is incomplete"})
183
+ out.append({"name": f"{name}_starts_in_data", "success": r["start"] >= min_date,
184
+ "expected": f"start >= {min_date}", "actual": r["start"]})
185
+ return out