xp-libs-analytics 0.4.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.
Files changed (65) hide show
  1. xp_libs_analytics/__init__.py +18 -0
  2. xp_libs_analytics/engine/__init__.py +1 -0
  3. xp_libs_analytics/engine/backend.py +379 -0
  4. xp_libs_analytics/engine/planner.py +450 -0
  5. xp_libs_analytics/engine/worker.py +121 -0
  6. xp_libs_analytics/errors/__init__.py +43 -0
  7. xp_libs_analytics/errors/catalog.json +75 -0
  8. xp_libs_analytics/errors/codes.py +81 -0
  9. xp_libs_analytics/ingest/__init__.py +309 -0
  10. xp_libs_analytics/interfaces.py +70 -0
  11. xp_libs_analytics/matching/__init__.py +1 -0
  12. xp_libs_analytics/matching/compiler.py +344 -0
  13. xp_libs_analytics/matching/diagnostics.py +115 -0
  14. xp_libs_analytics/matching/models.py +98 -0
  15. xp_libs_analytics/matching/normalize.py +16 -0
  16. xp_libs_analytics/matching/patterns.py +71 -0
  17. xp_libs_analytics/mcp/__init__.py +1 -0
  18. xp_libs_analytics/mcp/server.py +248 -0
  19. xp_libs_analytics/models/__init__.py +654 -0
  20. xp_libs_analytics/parser/__init__.py +142 -0
  21. xp_libs_analytics/policy/__init__.py +69 -0
  22. xp_libs_analytics/py.typed +0 -0
  23. xp_libs_analytics/registry.py +204 -0
  24. xp_libs_analytics/sdk/__init__.py +259 -0
  25. xp_libs_analytics/skills/generic-analytics/SKILL.md +236 -0
  26. xp_libs_analytics/skills/generic-analytics/examples/README.md +69 -0
  27. xp_libs_analytics/skills/generic-analytics/examples/queries/01_count_all.yaml +8 -0
  28. xp_libs_analytics/skills/generic-analytics/examples/queries/02_filter_and_count.yaml +15 -0
  29. xp_libs_analytics/skills/generic-analytics/examples/queries/03_group_stats_and_topk.yaml +21 -0
  30. xp_libs_analytics/skills/generic-analytics/examples/queries/04_nested_or_predicates.yaml +17 -0
  31. xp_libs_analytics/skills/generic-analytics/examples/queries/05_is_missing.yaml +11 -0
  32. xp_libs_analytics/skills/generic-analytics/examples/queries/06_missing_vs_null.yaml +17 -0
  33. xp_libs_analytics/skills/generic-analytics/examples/queries/07_conditional_aggregation.yaml +19 -0
  34. xp_libs_analytics/skills/generic-analytics/examples/queries/08_ratio_if.yaml +16 -0
  35. xp_libs_analytics/skills/generic-analytics/examples/queries/09_tumbling_window.yaml +23 -0
  36. xp_libs_analytics/skills/generic-analytics/examples/queries/10_sliding_window.yaml +32 -0
  37. xp_libs_analytics/skills/generic-analytics/examples/queries/11_explicit_cast.yaml +16 -0
  38. xp_libs_analytics/skills/generic-analytics/examples/queries/12_text_range_distinct.yaml +23 -0
  39. xp_libs_analytics/skills/generic-analytics/examples/queries/13_summary_statistics.yaml +15 -0
  40. xp_libs_analytics/skills/generic-analytics/examples/queries/14_nested_and_not.yaml +20 -0
  41. xp_libs_analytics/skills/generic-analytics/examples/queries/15_conditional_sum.yaml +13 -0
  42. xp_libs_analytics/skills/generic-analytics/examples/queries/16_mixed_types_cast.yaml +17 -0
  43. xp_libs_analytics/skills/generic-analytics/examples/queries/17_relative_last_hour.yaml +15 -0
  44. xp_libs_analytics/skills/generic-analytics/examples/queries/18_string_exact.yaml +11 -0
  45. xp_libs_analytics/skills/generic-analytics/examples/queries/19_string_normalization.yaml +15 -0
  46. xp_libs_analytics/skills/generic-analytics/examples/queries/20_prefix_suffix.yaml +16 -0
  47. xp_libs_analytics/skills/generic-analytics/examples/queries/21_whole_word.yaml +13 -0
  48. xp_libs_analytics/skills/generic-analytics/examples/queries/22_ilike.yaml +11 -0
  49. xp_libs_analytics/skills/generic-analytics/examples/queries/23_rust_regex.yaml +11 -0
  50. xp_libs_analytics/skills/generic-analytics/examples/queries/24_fuzzy.yaml +17 -0
  51. xp_libs_analytics/skills/generic-analytics/examples/queries/25_multi_field_matching.yaml +24 -0
  52. xp_libs_analytics/skills/generic-analytics/examples/queries/26_conditional_fuzzy.yaml +20 -0
  53. xp_libs_analytics/skills/generic-analytics/examples/queries/27_temporal_fuzzy.yaml +38 -0
  54. xp_libs_analytics/skills/generic-analytics/examples/queries/28_safe_matching_diagnostics.yaml +22 -0
  55. xp_libs_analytics/skills/generic-analytics/references/errors.md +55 -0
  56. xp_libs_analytics/skills/generic-analytics/references/query-language-full.md +306 -0
  57. xp_libs_analytics/skills/generic-analytics/references/query-language.md +39 -0
  58. xp_libs_analytics/skills/generic-analytics/references/string-matching-full.md +122 -0
  59. xp_libs_analytics/skills/generic-analytics/references/string-matching.md +11 -0
  60. xp_libs_analytics-0.4.0.dist-info/METADATA +199 -0
  61. xp_libs_analytics-0.4.0.dist-info/RECORD +65 -0
  62. xp_libs_analytics-0.4.0.dist-info/WHEEL +4 -0
  63. xp_libs_analytics-0.4.0.dist-info/entry_points.txt +2 -0
  64. xp_libs_analytics-0.4.0.dist-info/licenses/LICENSE +22 -0
  65. xp_libs_analytics-0.4.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +175 -0
@@ -0,0 +1,75 @@
1
+ {
2
+ "INPUT_INVALID_JSON": "Input is not valid JSON. Fix: supply one JSON object, an array of objects, or newline-delimited JSON objects. Example: [{\"value\":12},{\"value\":15}]",
3
+ "INPUT_INVALID_NDJSON": "One or more nonblank NDJSON lines is not a valid JSON object. Fix: put exactly one object on each line. Example: {\"value\":12} followed by a newline and {\"value\":15}.",
4
+ "INPUT_DUPLICATE_KEY": "A JSON object contains a repeated property name; accepting it could change query meaning. Fix: retain only one occurrence of each key. Example: {\"value\":12}, not {\"value\":12,\"value\":15}.",
5
+ "INPUT_UNSUPPORTED_ROOT": "The input root is not an object or an array of objects. Fix: wrap each scalar inside an object. Example: [{\"value\":12},{\"value\":15}] instead of [12,15].",
6
+ "INPUT_INVALID_UTF8": "Input contains invalid UTF-8 encoding. Fix: encode the JSON text as UTF-8 without invalid byte sequences. Example: json.dumps(records, ensure_ascii=False).encode('utf-8').",
7
+ "INPUT_INVALID_NUMBER": "Input contains a non-JSON or non-finite number. Fix: use finite JSON numbers and null for absent measurements. Example: {\"value\":null} rather than NaN or Infinity.",
8
+ "INPUT_TOO_DEEP": "Input JSON nesting exceeds the configured limit. Fix: flatten or select a smaller object hierarchy before ingestion. Example: {\"meta\":{\"kind\":\"a\"}}.",
9
+ "INPUT_TOO_WIDE": "Input contains more distinct field paths than the configured limit. Fix: project needed fields before ingestion or split the dataset into authorized smaller snapshots. Example: retain /time, /region and /value.",
10
+ "INPUT_RECORD_TOO_LARGE": "A single JSON record exceeds the configured record-size limit. Fix: remove oversized free-text/binary payloads or split the record. Example: keep the numeric measurements rather than embedded document bodies.",
11
+ "INPUT_BYTES_LIMIT": "Input payload exceeds the configured byte limit. Fix: submit fewer records or provide a trusted server-side data source. Example: register 10000 records instead of a multi-gigabyte MCP argument.",
12
+ "INPUT_ROWS_LIMIT": "Input has more records than the configured admission limit. Fix: reduce the batch or use a trusted server-side adapter with approved chunking. Example: register a smaller time-bounded snapshot.",
13
+ "DATA_FIELD_UNKNOWN": "The requested JSON Pointer does not appear in the authorized discovered schema. Fix: call analytics_describe, then use an exact listed path. Example: /metrics/latency rather than /metric/latency.",
14
+ "DATA_FIELD_TYPE_CONFLICT": "A field contains incompatible JSON types and cannot be used directly by this operation. Fix: select a compatible field or add an explicit permitted cast. Example: casts:[{\"field\":\"/value\",\"to\":\"float64\",\"on_error\":\"null\"}].",
15
+ "DATA_FIELD_NOT_SCALAR": "An operation requires a scalar field but the selected path contains an object or array. Fix: choose a scalar JSON Pointer inside the object. Example: /metrics/latency, not /metrics.",
16
+ "DATA_CAST_UNSUPPORTED": "The requested type cast or timestamp format is not supported. Fix: use float64, int64, string or datetime with a documented format. Example: {\"field\":\"/ts\",\"to\":\"datetime\",\"format\":\"rfc3339\",\"on_error\":\"reject\"}.",
17
+ "DATA_CAST_FAILED": "One or more present values cannot be converted using the declared cast. Fix: correct the records or choose on_error=null if policy permits. Example: replace {\"value\":\"unknown\"} with a number before casting to float64.",
18
+ "DATA_TIMESTAMP_INVALID": "The selected time field has an invalid or offset-ambiguous timestamp. Fix: use RFC3339 with Z or an explicit UTC offset, or declare a numeric epoch unit. Example: 2026-11-06T10:00:00Z.",
19
+ "QUERY_VERSION_UNSUPPORTED": "The query language version is not supported. Fix: call analytics_capabilities and use a listed version. Example: \"version\":\"analytics.query/v1\".",
20
+ "QUERY_MALFORMED": "The query object does not match the versioned contract, contains unknown properties, or has invalid data types. Fix: compare the request with the published schema. Example: use numeric \"limit\":100 rather than \"limit\":\"100\".",
21
+ "QUERY_UNKNOWN_OPERATOR": "A filter, aggregation, or window operator is unsupported. Fix: select an operator listed by analytics_capabilities. Example: {\"op\":\"p95\",\"field\":\"/value\",\"as\":\"p95_value\"}.",
22
+ "QUERY_OPERATOR_TYPE": "The selected operator cannot be applied to the field's resolved type. Fix: select a compatible field/operator or an approved explicit cast. Example: use avg on numeric /value rather than a string /label.",
23
+ "QUERY_INVALID_POINTER": "A field path is not a valid RFC 6901 JSON Pointer. Fix: use slash-separated tokens and escape ~ as ~0 and / as ~1. Example: /metrics/latency.",
24
+ "QUERY_INVALID_LITERAL": "A comparison value or membership list has an incompatible type, size, or shape. Fix: use a single typed scalar or a bounded homogeneous values list. Example: {\"field\":\"/region\",\"op\":\"in\",\"values\":[\"west\",\"east\"]}.",
25
+ "QUERY_PREDICATE_LIMIT": "Predicate nesting or condition count exceeds V1 limits. Fix: simplify the AND/OR tree and split unrelated investigations into separate queries. Example: one AND with two comparisons.",
26
+ "QUERY_ALIAS_INVALID": "An output alias is invalid, reserved, or duplicates another alias. Fix: use a unique identifier starting with a letter and containing only letters, digits or underscores. Example: \"as\":\"avg_value\".",
27
+ "QUERY_GROUP_INVALID": "A group-by field is absent, incompatible, duplicated, or exceeds allowed grouping dimensions. Fix: group by a small number of visible scalar fields. Example: group_by:[{\"field\":\"/region\",\"as\":\"region\"}].",
28
+ "QUERY_TIME_RANGE_INVALID": "The time range is missing required boundaries, has an invalid timezone, or has start at/after end. Fix: use one relative range or offset-aware start and end. Example: {\"field\":\"/ts\",\"last_minutes\":60}.",
29
+ "QUERY_WINDOW_INVALID": "The time window has an invalid size, step, overlap, or missing time_range. Fix: use allowed fixed durations, with step not exceeding size. Example: {\"type\":\"sliding\",\"size\":\"5m\",\"step\":\"1m\"} with a valid time_range.",
30
+ "QUERY_EMPTY_WINDOW_UNSUPPORTED": "Empty-window materialization is not supported for this unbounded or unconstrained group space. Fix: disable include_empty_windows or use a constrained small group set. Example: options:{\"include_empty_windows\":false}.",
31
+ "QUERY_HAVING_INVALID": "A HAVING condition refers to an unavailable result alias or unsupported predicate. Fix: use an alias created by an aggregation or group_by. Example: having:[{\"field\":\"events\",\"op\":\"gte\",\"value\":2}].",
32
+ "QUERY_ORDER_INVALID": "Order-by refers to an unknown result field or unsupported direction. Fix: sort by a declared alias or window_start/window_end. Example: order_by:[{\"field\":\"events\",\"direction\":\"desc\"}].",
33
+ "QUERY_LIMIT_INVALID": "The requested result limit is outside the configured range. Fix: use a positive integer within server limits. Example: \"limit\":100.",
34
+ "QUERY_UNSUPPORTED_FEATURE": "The request uses a capability reserved for a future version, such as joins, unions, continuous queries, or external sinks. Fix: make a V1 single-dataset snapshot query. Example: dataset: {name: observations} without sources/joins/outputs.",
35
+ "QUERY_TOO_EXPENSIVE": "The estimated query amplification, grouping cost, or data access exceeds the allowed budget. Fix: shorten the time range, reduce grouping dimensions, or increase the window step. Example: use a tumbling 5m window instead of many overlapping 1s windows.",
36
+ "ACCESS_DENIED": "The caller is not authorized to perform this operation or to access this dataset or metadata. Fix: use a dataset registered under your authenticated session or request permission from its owner. Example: query a name discovered in your authenticated context.",
37
+ "ACCESS_FIELD_DENIED": "A requested field or field metadata is restricted for this caller. Fix: query only fields returned by your authorized analytics_describe response. Example: remove a restricted group_by or aggregation path.",
38
+ "ACCESS_UPLOAD_DISABLED": "This deployment does not permit agents to upload raw JSON through MCP. Fix: ask the trusted hosting application to register records using the Python SDK and provide a context-local logical name. Example: engine.register_records(name=\"observations\", records=records).",
39
+ "RESOURCE_GROUPS_LIMIT": "The number of distinct groups exceeds the execution budget. Fix: remove high-cardinality group_by fields or filter to a smaller scope. Example: group by /region instead of /request_id.",
40
+ "RESOURCE_WINDOWS_LIMIT": "The number of generated temporal windows exceeds the limit. Fix: choose a larger step/size or reduce the time range. Example: size=5m and step=5m instead of size=1h and step=1s.",
41
+ "RESOURCE_RESULT_LIMIT": "The serialized result exceeds the permitted byte or row budget. Fix: reduce limit, group cardinality, or requested output fields. Example: set limit=50 and rank by a numeric aggregate.",
42
+ "RESOURCE_MEMORY_LIMIT": "An execution exceeded its allocated memory budget. Fix: reduce input size or query complexity and retry once if permitted. Example: aggregate a smaller authorized snapshot.",
43
+ "RESOURCE_TIMEOUT": "An analytical query exceeded its execution deadline. Fix: use a smaller snapshot, narrower range or fewer groups, then retry. Example: remove an expensive count_distinct on a high-cardinality field.",
44
+ "RESOURCE_CONCURRENCY_LIMIT": "The caller reached the allowed concurrent query count. Fix: finish or cancel an existing request and retry after backoff. Example: run one analytics_query at a time for the same dataset.",
45
+ "EXECUTION_CANCELLED": "The execution was canceled before completion. Fix: re-run the query only if the caller still needs it and the same logical dataset binding is valid. Example: submit the original query after an explicit cancellation acknowledgment.",
46
+ "EXECUTION_BACKEND_ERROR": "The analytical backend could not complete the requested operation. Fix: verify the query with analytics_validate_query and retry only if permitted; report the query_id if the failure persists. Example: rerun with a supported aggregation such as avg.",
47
+ "MCP_INVALID_TOOL_CALL": "The MCP tool arguments or invocation shape are invalid. Fix: list the available tools and follow the published tool input schema. Example: call analytics_describe(name=\"observations\").",
48
+ "MCP_UNAVAILABLE": "The MCP adapter or selected transport is unavailable. Fix: reconnect and verify the server and transport configuration. Example: start the server using the documented xp-libs-analytics entry point.",
49
+ "INTERNAL_ERROR": "An unexpected server error occurred; no record data is included in this message. Fix: report the query_id to the operator and avoid blind repeated retries. Example: check the server's trace for that query_id.",
50
+ "QUERY_YAML_INVALID": "The YAML query contains a syntax or indentation error. Fix: submit one correctly indented YAML mapping and quote ambiguous strings. Example: aggregations:\n - op: count\n as: rows.",
51
+ "QUERY_YAML_DUPLICATE_KEY": "The YAML query repeats a mapping key, so its meaning is ambiguous. Fix: keep exactly one occurrence of the key at that mapping level. Example: limit: 100 appears once, not twice.",
52
+ "QUERY_YAML_ANCHOR_FORBIDDEN": "YAML anchors are not supported in analytical queries. Fix: write each bounded filter or aggregation explicitly without &anchor. Example: aggregations: [{op: count, as: rows}].",
53
+ "QUERY_YAML_ALIAS_FORBIDDEN": "YAML aliases are not supported because they can create recursive or amplified document graphs. Fix: expand the intended bounded mapping explicitly without *alias. Example: filters: [{field: /region, op: eq, value: west}].",
54
+ "QUERY_YAML_TAG_FORBIDDEN": "Explicit YAML tags and tag directives are not supported. Fix: use ordinary mappings, sequences and scalar values; never use ! or !! constructors. Example: limit: 100.",
55
+ "QUERY_YAML_MULTI_DOCUMENT": "Only one YAML query document may be submitted at a time. Fix: remove additional document separators and send one root mapping. Example: version: analytics.query/v1 followed by dataset and aggregations in the same document.",
56
+ "QUERY_YAML_ROOT_INVALID": "A YAML query must be a nonempty mapping with string keys. Fix: put version, dataset, and aggregations under one top-level mapping. Example: version: analytics.query/v1.",
57
+ "QUERY_YAML_COMPLEXITY_LIMIT": "The YAML query exceeds permitted bytes, nesting, nodes, keys or scalar length. Fix: simplify predicates or split the investigation into separate bounded queries. Example: a single filter with two conditions.",
58
+ "QUERY_YAML_SCALAR_INVALID": "The YAML query contains an unsupported or non-finite scalar representation. Fix: use finite decimal numbers or quote data intended as text. Example: value: 12.5 instead of value: .nan.",
59
+ "DATASET_NAME_UNKNOWN": "Dataset name is not registered in this context. Fix: discover available names with analytics_list_datasets. Example: dataset: {name: observations}.",
60
+ "DATASET_NAME_DUPLICATE": "Dataset name is already registered in this context. Fix: choose a different name or use replace_records to publish a new snapshot. Example: replace_records(\"observations\", records).",
61
+ "DATASET_NAME_INVALID": "Dataset name is invalid. Fix: use 1 to 64 ASCII letters, digits, underscores or hyphens, starting with a letter. Example: observations_2026.",
62
+ "DATASET_SNAPSHOT_STALE": "Dataset snapshot has expired or its revision no longer matches. Fix: discover current metadata and publish or reference the current snapshot. Example: describe(\"observations\").",
63
+ "RESOURCE_DATASETS_LIMIT": "Named dataset budget exceeded. Fix: unregister an unused dataset before registering another. Example: unregister_dataset(\"observations\").",
64
+ "MATCH_PATTERN_INVALID": "String pattern is invalid. Fix: escape only percent, underscore or backslash in LIKE, and use nonempty bounded search values. Example: {field: /text, op: like, value: \"Pet%\"}.",
65
+ "MATCH_REGEX_INVALID": "Regex is not supported by the Rust engine. Fix: remove lookaround/backreferences or use literal matching. Example: {field: /text, op: regex, value: \"^Pet[a-z]+$\"}.",
66
+ "MATCH_REGEX_POLICY": "Regex or wildcard pattern exceeds host policy or is an empty-string catch-all. Fix: use a bounded selective pattern without unsupported flags. Example: {field: /text, op: contains, value: Peter}.",
67
+ "MATCH_NORMALIZATION_INVALID": "String normalization is incompatible with this predicate or field. Fix: normalize strings only; regex permits case handling only, and ILIKE cannot request case-sensitive matching. Example: normalization: {case_sensitive: false, unicode: nfkc, trim: true}.",
68
+ "MATCH_FUZZY_ALGORITHM_INVALID": "Fuzzy scorer is invalid or absent. Fix: choose an approved algorithm. Example: {field: /text, op: fuzzy, value: Peter, algorithm: ratio, min_score: 85}.",
69
+ "MATCH_FUZZY_THRESHOLD_INVALID": "Fuzzy similarity threshold is invalid, absent or below host policy. Fix: supply a finite numeric min_score within the allowed range. Example: {field: /text, op: fuzzy, value: Peter, algorithm: ratio, min_score: 85}.",
70
+ "MATCH_FUZZY_QUERY_TOO_SHORT": "Normalized fuzzy search text is shorter than host policy permits. Fix: provide longer text or use exact/literal matching. Example: {field: /text, op: eq, value: Li}.",
71
+ "MATCH_CANDIDATE_BUDGET_EXCEEDED": "Exhaustive string matching exceeds the host candidate, length or byte budget. Fix: narrow authorized time/nonfuzzy filters without claiming a complete result for the original scope. Example: filters: [{field: /status, op: eq, value: error}].",
72
+ "MATCH_COMPLEXITY_BUDGET_EXCEEDED": "Matching fields, patterns or total comparisons exceed host policy. Fix: reduce repeated matching work or narrow eligible data; result limit does not reduce scanning. Example: match: {fields: [/text], patterns: [{op: word, value: Peter}], mode: any}.",
73
+ "MATCH_DIAGNOSTICS_UNAUTHORIZED": "Raw matching values are not authorized by host and field disclosure policy. Fix: request redacted diagnostics. Example: options: {matching_details: {include: true, include_raw_values: false}}.",
74
+ "MATCH_ENTITY_INFERENCE_UNSUPPORTED": "V1 string similarity does not resolve or verify identities, aliases or entity ownership. Fix: report matching observed values without merging people. Example: {field: /text, op: fuzzy, value: Peter, algorithm: ratio, min_score: 85}."
75
+ }
@@ -0,0 +1,81 @@
1
+ """Closed V1 code vocabulary; messages live only in catalog.json."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class ErrorCode(StrEnum):
7
+ INPUT_INVALID_JSON = "INPUT_INVALID_JSON"
8
+ INPUT_INVALID_NDJSON = "INPUT_INVALID_NDJSON"
9
+ INPUT_DUPLICATE_KEY = "INPUT_DUPLICATE_KEY"
10
+ INPUT_UNSUPPORTED_ROOT = "INPUT_UNSUPPORTED_ROOT"
11
+ INPUT_INVALID_UTF8 = "INPUT_INVALID_UTF8"
12
+ INPUT_INVALID_NUMBER = "INPUT_INVALID_NUMBER"
13
+ INPUT_TOO_DEEP = "INPUT_TOO_DEEP"
14
+ INPUT_TOO_WIDE = "INPUT_TOO_WIDE"
15
+ INPUT_RECORD_TOO_LARGE = "INPUT_RECORD_TOO_LARGE"
16
+ INPUT_BYTES_LIMIT = "INPUT_BYTES_LIMIT"
17
+ INPUT_ROWS_LIMIT = "INPUT_ROWS_LIMIT"
18
+ DATASET_NAME_UNKNOWN = "DATASET_NAME_UNKNOWN"
19
+ DATASET_SNAPSHOT_STALE = "DATASET_SNAPSHOT_STALE"
20
+ DATA_FIELD_UNKNOWN = "DATA_FIELD_UNKNOWN"
21
+ DATA_FIELD_TYPE_CONFLICT = "DATA_FIELD_TYPE_CONFLICT"
22
+ DATA_FIELD_NOT_SCALAR = "DATA_FIELD_NOT_SCALAR"
23
+ DATA_CAST_UNSUPPORTED = "DATA_CAST_UNSUPPORTED"
24
+ DATA_CAST_FAILED = "DATA_CAST_FAILED"
25
+ DATA_TIMESTAMP_INVALID = "DATA_TIMESTAMP_INVALID"
26
+ QUERY_VERSION_UNSUPPORTED = "QUERY_VERSION_UNSUPPORTED"
27
+ QUERY_MALFORMED = "QUERY_MALFORMED"
28
+ QUERY_UNKNOWN_OPERATOR = "QUERY_UNKNOWN_OPERATOR"
29
+ QUERY_OPERATOR_TYPE = "QUERY_OPERATOR_TYPE"
30
+ QUERY_INVALID_POINTER = "QUERY_INVALID_POINTER"
31
+ QUERY_INVALID_LITERAL = "QUERY_INVALID_LITERAL"
32
+ QUERY_PREDICATE_LIMIT = "QUERY_PREDICATE_LIMIT"
33
+ QUERY_ALIAS_INVALID = "QUERY_ALIAS_INVALID"
34
+ QUERY_GROUP_INVALID = "QUERY_GROUP_INVALID"
35
+ QUERY_TIME_RANGE_INVALID = "QUERY_TIME_RANGE_INVALID"
36
+ QUERY_WINDOW_INVALID = "QUERY_WINDOW_INVALID"
37
+ QUERY_EMPTY_WINDOW_UNSUPPORTED = "QUERY_EMPTY_WINDOW_UNSUPPORTED"
38
+ QUERY_HAVING_INVALID = "QUERY_HAVING_INVALID"
39
+ QUERY_ORDER_INVALID = "QUERY_ORDER_INVALID"
40
+ QUERY_LIMIT_INVALID = "QUERY_LIMIT_INVALID"
41
+ QUERY_UNSUPPORTED_FEATURE = "QUERY_UNSUPPORTED_FEATURE"
42
+ QUERY_TOO_EXPENSIVE = "QUERY_TOO_EXPENSIVE"
43
+ ACCESS_DENIED = "ACCESS_DENIED"
44
+ ACCESS_FIELD_DENIED = "ACCESS_FIELD_DENIED"
45
+ ACCESS_UPLOAD_DISABLED = "ACCESS_UPLOAD_DISABLED"
46
+ RESOURCE_DATASETS_LIMIT = "RESOURCE_DATASETS_LIMIT"
47
+ RESOURCE_GROUPS_LIMIT = "RESOURCE_GROUPS_LIMIT"
48
+ RESOURCE_WINDOWS_LIMIT = "RESOURCE_WINDOWS_LIMIT"
49
+ RESOURCE_RESULT_LIMIT = "RESOURCE_RESULT_LIMIT"
50
+ RESOURCE_MEMORY_LIMIT = "RESOURCE_MEMORY_LIMIT"
51
+ RESOURCE_TIMEOUT = "RESOURCE_TIMEOUT"
52
+ RESOURCE_CONCURRENCY_LIMIT = "RESOURCE_CONCURRENCY_LIMIT"
53
+ EXECUTION_CANCELLED = "EXECUTION_CANCELLED"
54
+ EXECUTION_BACKEND_ERROR = "EXECUTION_BACKEND_ERROR"
55
+ MCP_INVALID_TOOL_CALL = "MCP_INVALID_TOOL_CALL"
56
+ MCP_UNAVAILABLE = "MCP_UNAVAILABLE"
57
+ INTERNAL_ERROR = "INTERNAL_ERROR"
58
+ QUERY_YAML_INVALID = "QUERY_YAML_INVALID"
59
+ QUERY_YAML_DUPLICATE_KEY = "QUERY_YAML_DUPLICATE_KEY"
60
+ QUERY_YAML_ANCHOR_FORBIDDEN = "QUERY_YAML_ANCHOR_FORBIDDEN"
61
+ QUERY_YAML_ALIAS_FORBIDDEN = "QUERY_YAML_ALIAS_FORBIDDEN"
62
+ QUERY_YAML_TAG_FORBIDDEN = "QUERY_YAML_TAG_FORBIDDEN"
63
+ QUERY_YAML_MULTI_DOCUMENT = "QUERY_YAML_MULTI_DOCUMENT"
64
+ QUERY_YAML_ROOT_INVALID = "QUERY_YAML_ROOT_INVALID"
65
+ QUERY_YAML_COMPLEXITY_LIMIT = "QUERY_YAML_COMPLEXITY_LIMIT"
66
+ QUERY_YAML_SCALAR_INVALID = "QUERY_YAML_SCALAR_INVALID"
67
+
68
+ DATASET_NAME_DUPLICATE = "DATASET_NAME_DUPLICATE"
69
+ DATASET_NAME_INVALID = "DATASET_NAME_INVALID"
70
+
71
+ MATCH_PATTERN_INVALID = "MATCH_PATTERN_INVALID"
72
+ MATCH_REGEX_INVALID = "MATCH_REGEX_INVALID"
73
+ MATCH_REGEX_POLICY = "MATCH_REGEX_POLICY"
74
+ MATCH_NORMALIZATION_INVALID = "MATCH_NORMALIZATION_INVALID"
75
+ MATCH_FUZZY_ALGORITHM_INVALID = "MATCH_FUZZY_ALGORITHM_INVALID"
76
+ MATCH_FUZZY_THRESHOLD_INVALID = "MATCH_FUZZY_THRESHOLD_INVALID"
77
+ MATCH_FUZZY_QUERY_TOO_SHORT = "MATCH_FUZZY_QUERY_TOO_SHORT"
78
+ MATCH_CANDIDATE_BUDGET_EXCEEDED = "MATCH_CANDIDATE_BUDGET_EXCEEDED"
79
+ MATCH_COMPLEXITY_BUDGET_EXCEEDED = "MATCH_COMPLEXITY_BUDGET_EXCEEDED"
80
+ MATCH_DIAGNOSTICS_UNAUTHORIZED = "MATCH_DIAGNOSTICS_UNAUTHORIZED"
81
+ MATCH_ENTITY_INFERENCE_UNSUPPORTED = "MATCH_ENTITY_INFERENCE_UNSUPPORTED"
@@ -0,0 +1,309 @@
1
+ """Bounded JSON admission, schema discovery and owner-scoped snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import re
8
+ import secrets
9
+ import threading
10
+ from collections.abc import Iterable, Mapping
11
+ from dataclasses import dataclass
12
+ from datetime import UTC, datetime, timedelta
13
+ from typing import Any, cast
14
+
15
+ from xp_libs_analytics.errors import AnalyticsError
16
+ from xp_libs_analytics.policy import Limits, Principal, RequestBudget
17
+
18
+
19
+ def pointer_tokens(path: str, max_depth: int = 12) -> list[str]:
20
+ if not path.startswith("/") or re.search(r"~(?![01])", path):
21
+ raise AnalyticsError("QUERY_INVALID_POINTER")
22
+ tokens = path[1:].split("/")
23
+ if len(tokens) > max_depth:
24
+ raise AnalyticsError("QUERY_INVALID_POINTER")
25
+ return [t.replace("~1", "/").replace("~0", "~") for t in tokens]
26
+
27
+
28
+ def kind(value: Any) -> str:
29
+ return {
30
+ type(None): "null",
31
+ bool: "boolean",
32
+ int: "integer",
33
+ float: "float",
34
+ str: "string",
35
+ dict: "object",
36
+ list: "array",
37
+ datetime: "datetime",
38
+ }[type(value)]
39
+
40
+
41
+ def reject_constant(_: str) -> Any:
42
+ raise AnalyticsError("INPUT_INVALID_NUMBER")
43
+
44
+
45
+ def pairs_hook(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
46
+ result: dict[str, Any] = {}
47
+ for key, value in pairs:
48
+ if key in result:
49
+ raise AnalyticsError("INPUT_DUPLICATE_KEY")
50
+ result[key] = value
51
+ return result
52
+
53
+
54
+ def loads(text: str) -> Any:
55
+ return json.loads(text, object_pairs_hook=pairs_hook, parse_constant=reject_constant)
56
+
57
+
58
+ def admit(data: Any, limits: Limits, *, ndjson: bool = False) -> tuple[list[dict[str, Any]], int]:
59
+ if isinstance(data, bytes):
60
+ if len(data) > limits.max_bytes:
61
+ raise AnalyticsError("INPUT_BYTES_LIMIT")
62
+ try:
63
+ data = data.decode("utf-8")
64
+ except UnicodeError:
65
+ raise AnalyticsError("INPUT_INVALID_UTF8") from None
66
+ if isinstance(data, str):
67
+ try:
68
+ if len(data.encode("utf-8")) > limits.max_bytes:
69
+ raise AnalyticsError("INPUT_BYTES_LIMIT")
70
+ if ndjson:
71
+ data = [loads(line) for line in data.splitlines() if line.strip()]
72
+ else:
73
+ try:
74
+ data = loads(data)
75
+ except json.JSONDecodeError as exc:
76
+ if exc.msg == "Extra data" and "\n" in data:
77
+ return admit(data, limits, ndjson=True)
78
+ raise
79
+ except UnicodeError:
80
+ raise AnalyticsError("INPUT_INVALID_UTF8") from None
81
+ except (ValueError, RecursionError):
82
+ raise AnalyticsError(
83
+ "INPUT_INVALID_NDJSON" if ndjson else "INPUT_INVALID_JSON"
84
+ ) from None
85
+ if isinstance(data, Mapping):
86
+ data = [data]
87
+ if not isinstance(data, Iterable) or isinstance(data, (str, bytes)):
88
+ raise AnalyticsError("INPUT_UNSUPPORTED_ROOT")
89
+ fields: set[str] = set()
90
+ record_units = [0]
91
+
92
+ def check(value: Any, depth: int, path: str, ancestors: set[int]) -> Any:
93
+ record_units[0] += len(value) if type(value) is str else 1
94
+ if record_units[0] > limits.max_record_bytes:
95
+ raise AnalyticsError("INPUT_RECORD_TOO_LARGE")
96
+ if depth > limits.max_depth:
97
+ raise AnalyticsError("INPUT_TOO_DEEP")
98
+ if type(value) in (dict, list) or isinstance(value, Mapping):
99
+ if id(value) in ancestors:
100
+ raise AnalyticsError("INPUT_TOO_DEEP")
101
+ chain = ancestors | {id(value)}
102
+ if isinstance(value, Mapping):
103
+ result = {}
104
+ for k, v in value.items():
105
+ if type(k) is not str:
106
+ raise AnalyticsError("INPUT_UNSUPPORTED_ROOT")
107
+ p = path + "/" + k.replace("~", "~0").replace("/", "~1")
108
+ fields.add(p)
109
+ if len(fields) > limits.max_columns:
110
+ raise AnalyticsError("INPUT_TOO_WIDE")
111
+ result[k] = check(v, depth + 1, p, chain)
112
+ return result
113
+ return [check(v, depth + 1, path, chain) for v in value]
114
+ if type(value) is float and not math.isfinite(value):
115
+ raise AnalyticsError("INPUT_INVALID_NUMBER")
116
+ if type(value) is int and not -(2**63) <= value < 2**63:
117
+ raise AnalyticsError("INPUT_INVALID_NUMBER")
118
+ if type(value) is str:
119
+ try:
120
+ value.encode("utf-8")
121
+ except UnicodeError:
122
+ raise AnalyticsError("INPUT_INVALID_UTF8") from None
123
+ elif type(value) not in (int, float, bool, type(None)):
124
+ raise AnalyticsError("INPUT_UNSUPPORTED_ROOT")
125
+ return value
126
+
127
+ rows: list[dict[str, Any]] = []
128
+ total = 0
129
+ for row in data:
130
+ record_units[0] = 0
131
+ if len(rows) >= limits.max_rows:
132
+ raise AnalyticsError("INPUT_ROWS_LIMIT")
133
+ if not isinstance(row, Mapping):
134
+ raise AnalyticsError("INPUT_INVALID_NDJSON" if ndjson else "INPUT_UNSUPPORTED_ROOT")
135
+ copied = check(row, 0, "", set())
136
+ size = len(json.dumps(copied, ensure_ascii=False, allow_nan=False).encode("utf-8"))
137
+ if size > limits.max_record_bytes:
138
+ raise AnalyticsError("INPUT_RECORD_TOO_LARGE")
139
+ total += size
140
+ if total > limits.max_bytes:
141
+ raise AnalyticsError("INPUT_BYTES_LIMIT")
142
+ rows.append(copied)
143
+ return rows, total
144
+
145
+
146
+ def flatten(record: dict[str, Any], path: str = "") -> dict[str, Any]:
147
+ result = {}
148
+ for key, value in record.items():
149
+ p = path + "/" + key.replace("~", "~0").replace("/", "~1")
150
+ result[p] = value
151
+ if isinstance(value, dict):
152
+ result.update(flatten(value, p))
153
+ return result
154
+
155
+
156
+ @dataclass(frozen=True)
157
+ class DatasetHandle:
158
+ id: str
159
+ revision: int = 1
160
+
161
+
162
+ @dataclass(frozen=True)
163
+ class Snapshot:
164
+ handle: DatasetHandle
165
+ owner: str
166
+ created: datetime
167
+ expires: datetime
168
+ payload: str
169
+ schema_json: str
170
+ row_count: int
171
+ byte_count: int
172
+ source_kind: str
173
+
174
+ def records(self) -> list[dict[str, Any]]:
175
+ return cast(list[dict[str, Any]], json.loads(self.payload))
176
+
177
+ def schema(self) -> dict[str, Any]:
178
+ return cast(dict[str, Any], json.loads(self.schema_json))
179
+
180
+
181
+ class DatasetStore:
182
+ def __init__(self, limits: Limits) -> None:
183
+ self.limits = limits
184
+ self._snapshots: dict[str, Snapshot] = {}
185
+ self._budgets: dict[str, RequestBudget] = {}
186
+ self._readers: dict[str, int] = {}
187
+ self._retired: set[str] = set()
188
+ self._lock = threading.RLock()
189
+
190
+ def request_budget(self, principal: Principal) -> RequestBudget:
191
+ with self._lock:
192
+ if principal.id not in self._budgets:
193
+ self._budgets[principal.id] = RequestBudget(self.limits)
194
+ return self._budgets[principal.id]
195
+
196
+ def expire(self, now: datetime) -> None:
197
+ with self._lock:
198
+ for ident, snapshot in list(self._snapshots.items()):
199
+ if snapshot.expires <= now:
200
+ self._retire(ident)
201
+
202
+ def _retire(self, ident: str) -> None:
203
+ if self._readers.get(ident, 0):
204
+ self._retired.add(ident)
205
+ else:
206
+ self._snapshots.pop(ident, None)
207
+ self._retired.discard(ident)
208
+
209
+ def acquire(self, handle: DatasetHandle, principal: Principal, now: datetime) -> Snapshot:
210
+ with self._lock:
211
+ snapshot = self.get_snapshot(handle, principal, now)
212
+ self._readers[handle.id] = self._readers.get(handle.id, 0) + 1
213
+ return snapshot
214
+
215
+ def unpin(self, handle: DatasetHandle) -> None:
216
+ with self._lock:
217
+ count = self._readers[handle.id] - 1
218
+ if count:
219
+ self._readers[handle.id] = count
220
+ else:
221
+ del self._readers[handle.id]
222
+ if handle.id in self._retired:
223
+ self._retire(handle.id)
224
+
225
+ def register(
226
+ self,
227
+ data: Any,
228
+ principal: Principal,
229
+ now: datetime,
230
+ *,
231
+ ndjson: bool = False,
232
+ replaces: DatasetHandle | None = None,
233
+ revision: int = 1,
234
+ ) -> DatasetHandle:
235
+ rows, size = admit(data, self.limits, ndjson=ndjson)
236
+ flat = [flatten(row) for row in rows]
237
+ paths = sorted({p for row in flat for p in row})
238
+ schema = {}
239
+ for p in paths:
240
+ values = [row[p] for row in flat if p in row]
241
+ types = sorted({kind(v) for v in values})
242
+ nonnull = set(types) - {"null"}
243
+ resolved = (
244
+ "float"
245
+ if nonnull == {"integer", "float"}
246
+ else next(iter(nonnull))
247
+ if len(nonnull) == 1
248
+ else "null"
249
+ if not nonnull
250
+ else "mixed"
251
+ )
252
+ if resolved == "float" and any(type(v) is int and abs(v) > 2**53 for v in values):
253
+ raise AnalyticsError("INPUT_INVALID_NUMBER")
254
+ schema[p] = {
255
+ "types": types,
256
+ "type": resolved,
257
+ "present": len(values),
258
+ "null": sum(v is None for v in values),
259
+ "missing": len(rows) - len(values),
260
+ }
261
+ with self._lock:
262
+ self.expire(now)
263
+ old = self._snapshots.get(replaces.id) if replaces else None
264
+ reclaim = old.byte_count if old and not self._readers.get(old.handle.id) else 0
265
+ if (
266
+ size + sum(s.byte_count for s in self._snapshots.values()) - reclaim
267
+ > self.limits.max_total_bytes
268
+ ):
269
+ raise AnalyticsError("RESOURCE_MEMORY_LIMIT")
270
+ if (
271
+ sum(
272
+ s.owner == principal.id and k not in self._retired
273
+ for k, s in self._snapshots.items()
274
+ )
275
+ - int(old is not None and old.handle.id not in self._retired)
276
+ >= self.limits.max_datasets
277
+ ):
278
+ raise AnalyticsError("RESOURCE_DATASETS_LIMIT")
279
+ handle = DatasetHandle("ds_" + secrets.token_urlsafe(24), revision)
280
+ self._snapshots[handle.id] = Snapshot(
281
+ handle,
282
+ principal.id,
283
+ now,
284
+ now + timedelta(seconds=self.limits.ttl_seconds),
285
+ json.dumps(rows, ensure_ascii=False),
286
+ json.dumps(schema, sort_keys=True),
287
+ len(rows),
288
+ size,
289
+ "inline_json" if isinstance(data, (str, bytes)) else "in_memory",
290
+ )
291
+ if replaces:
292
+ self._retire(replaces.id)
293
+ return handle
294
+
295
+ def get_snapshot(
296
+ self, handle: DatasetHandle | str, principal: Principal, now: datetime
297
+ ) -> Snapshot:
298
+ ident = handle.id if isinstance(handle, DatasetHandle) else handle
299
+ with self._lock:
300
+ snapshot = self._snapshots.get(ident)
301
+ if snapshot is None or snapshot.owner != principal.id:
302
+ raise AnalyticsError("ACCESS_DENIED")
303
+ if snapshot.expires <= now:
304
+ raise AnalyticsError("DATASET_SNAPSHOT_STALE")
305
+ return snapshot
306
+
307
+
308
+ def utc_now() -> datetime:
309
+ return datetime.now(UTC)
@@ -0,0 +1,70 @@
1
+ """Versioned extension seams. V1 executes exactly one snapshot and JSON sink."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Literal, Protocol, cast
5
+
6
+ from xp_libs_analytics.engine.planner import LogicalPlan
7
+ from xp_libs_analytics.policy import Limits, Principal
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class SourceDescriptor:
12
+ source_id: str
13
+ origin: str
14
+ owner: str
15
+ schema_hash: str
16
+ redacted: bool = False
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class InputPort:
21
+ source_ref: str
22
+ mode: Literal["snapshot"] = "snapshot"
23
+ schema_revision: int = 1
24
+ watermark_capability: bool = False
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class OutputPort:
29
+ sink_ref: str = "json-response"
30
+ format: Literal["json"] = "json"
31
+ delivery_semantics: Literal["request"] = "request"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class QueryDefinition:
36
+ id: str
37
+ plan: LogicalPlan
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class QueryExecution:
42
+ id: str
43
+ definition_id: str
44
+ state: Literal["CREATED", "RUNNING", "STOPPING", "STOPPED", "FAILED"]
45
+
46
+
47
+ class DataSource(Protocol):
48
+ def read(self, request: Any, principal: Principal, budget: Limits) -> Any: ...
49
+
50
+
51
+ class ResultSink(Protocol):
52
+ def write(self, envelope: dict[str, Any], principal: Principal) -> dict[str, Any]: ...
53
+
54
+
55
+ class InlineJsonSource:
56
+ def read(self, request: Any, principal: Principal, budget: Limits) -> Any:
57
+ from xp_libs_analytics.ingest import admit
58
+
59
+ return admit(request, budget)[0]
60
+
61
+
62
+ class InMemoryObjectSource(InlineJsonSource):
63
+ pass
64
+
65
+
66
+ class JsonResultSink:
67
+ def write(self, envelope: dict[str, Any], principal: Principal) -> dict[str, Any]:
68
+ import json
69
+
70
+ return cast(dict[str, Any], json.loads(json.dumps(envelope, allow_nan=False)))
@@ -0,0 +1 @@
1
+ """Shared bounded string matching; no identity inference or cross-query cache."""