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,654 @@
1
+ """Strict immutable, versioned normalized AST."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Literal, TypeAlias
6
+
7
+ from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag, ValidationError
8
+
9
+ from xp_libs_analytics.errors import AnalyticsError
10
+ from xp_libs_analytics.matching.models import (
11
+ FuzzyPredicate,
12
+ MatchFieldsPredicate,
13
+ MatchingDetails,
14
+ Normalization,
15
+ StringPredicate,
16
+ )
17
+ from xp_libs_analytics.parser import parse_yaml
18
+ from xp_libs_analytics.policy import Limits
19
+
20
+ Scalar = bool | int | float | str | None
21
+ LEAF_OPS = (
22
+ "eq ne gt gte lt lte in not_in between is_null is_not_null "
23
+ "is_missing is_present contains starts_with ends_with"
24
+ ).split()
25
+ LEGACY_FILTER_OPS = LEAF_OPS + ["and", "or", "not"]
26
+ FILTER_OPS = LEGACY_FILTER_OPS + [
27
+ "not_contains",
28
+ "word",
29
+ "like",
30
+ "ilike",
31
+ "regex",
32
+ "fuzzy",
33
+ "match",
34
+ ]
35
+ AGG_OPS = (
36
+ "count count_non_null null_count missing_count count_distinct sum avg min max "
37
+ "median p50 p90 p95 p99 stddev variance count_if sum_if avg_if ratio_if"
38
+ ).split()
39
+ HAVING_OPS = "eq ne gt gte lt lte between is_null is_not_null and or not".split()
40
+
41
+
42
+ class StrictModel(BaseModel):
43
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True, populate_by_name=False)
44
+
45
+
46
+ class PredicateBase(StrictModel):
47
+ pass
48
+
49
+
50
+ class Eq(PredicateBase):
51
+ op: Literal["eq"]
52
+ field: str
53
+ value: Scalar
54
+ normalization: Normalization | None = None
55
+
56
+
57
+ class Ne(PredicateBase):
58
+ op: Literal["ne"]
59
+ field: str
60
+ value: Scalar
61
+ normalization: Normalization | None = None
62
+
63
+
64
+ class Gt(PredicateBase):
65
+ op: Literal["gt"]
66
+ field: str
67
+ value: Scalar
68
+
69
+
70
+ class Gte(PredicateBase):
71
+ op: Literal["gte"]
72
+ field: str
73
+ value: Scalar
74
+
75
+
76
+ class Lt(PredicateBase):
77
+ op: Literal["lt"]
78
+ field: str
79
+ value: Scalar
80
+
81
+
82
+ class Lte(PredicateBase):
83
+ op: Literal["lte"]
84
+ field: str
85
+ value: Scalar
86
+
87
+
88
+ class In(PredicateBase):
89
+ op: Literal["in"]
90
+ field: str
91
+ values: list[Scalar] = Field(min_length=1, max_length=20)
92
+
93
+
94
+ class NotIn(PredicateBase):
95
+ op: Literal["not_in"]
96
+ field: str
97
+ values: list[Scalar] = Field(min_length=1, max_length=20)
98
+
99
+
100
+ class Between(PredicateBase):
101
+ op: Literal["between"]
102
+ field: str
103
+ lower: Scalar
104
+ upper: Scalar
105
+
106
+
107
+ class IsNull(PredicateBase):
108
+ op: Literal["is_null"]
109
+ field: str
110
+
111
+
112
+ class IsNotNull(PredicateBase):
113
+ op: Literal["is_not_null"]
114
+ field: str
115
+
116
+
117
+ class IsMissing(PredicateBase):
118
+ op: Literal["is_missing"]
119
+ field: str
120
+
121
+
122
+ class IsPresent(PredicateBase):
123
+ op: Literal["is_present"]
124
+ field: str
125
+
126
+
127
+ class Contains(PredicateBase):
128
+ op: Literal["contains"]
129
+ field: str
130
+ value: Scalar
131
+ normalization: Normalization | None = None
132
+
133
+
134
+ class StartsWith(PredicateBase):
135
+ op: Literal["starts_with"]
136
+ field: str
137
+ value: Scalar
138
+ normalization: Normalization | None = None
139
+
140
+
141
+ class EndsWith(PredicateBase):
142
+ op: Literal["ends_with"]
143
+ field: str
144
+ value: Scalar
145
+ normalization: Normalization | None = None
146
+
147
+
148
+ class And(PredicateBase):
149
+ op: Literal["and"]
150
+ conditions: list[Predicate] = Field(min_length=2, max_length=32)
151
+
152
+
153
+ class Or(PredicateBase):
154
+ op: Literal["or"]
155
+ conditions: list[Predicate] = Field(min_length=2, max_length=32)
156
+
157
+
158
+ class Not(PredicateBase):
159
+ op: Literal["not"]
160
+ condition: Predicate
161
+
162
+
163
+ SinglePredicate: TypeAlias = Annotated[
164
+ Eq
165
+ | Ne
166
+ | Gt
167
+ | Gte
168
+ | Lt
169
+ | Lte
170
+ | In
171
+ | NotIn
172
+ | Between
173
+ | IsNull
174
+ | IsNotNull
175
+ | IsMissing
176
+ | IsPresent
177
+ | Contains
178
+ | StartsWith
179
+ | EndsWith
180
+ | StringPredicate
181
+ | FuzzyPredicate
182
+ | And
183
+ | Or
184
+ | Not,
185
+ Field(discriminator="op"),
186
+ ]
187
+
188
+
189
+ def predicate_tag(value: object) -> str:
190
+ return (
191
+ "match"
192
+ if isinstance(value, MatchFieldsPredicate) or isinstance(value, dict) and "match" in value
193
+ else "single"
194
+ )
195
+
196
+
197
+ Predicate: TypeAlias = Annotated[
198
+ Annotated[SinglePredicate, Tag("single")] | Annotated[MatchFieldsPredicate, Tag("match")],
199
+ Discriminator(predicate_tag),
200
+ ]
201
+
202
+
203
+ Eq.model_rebuild()
204
+ Ne.model_rebuild()
205
+ Gt.model_rebuild()
206
+ Gte.model_rebuild()
207
+ Lt.model_rebuild()
208
+ Lte.model_rebuild()
209
+ In.model_rebuild()
210
+ NotIn.model_rebuild()
211
+ Between.model_rebuild()
212
+ IsNull.model_rebuild()
213
+ IsNotNull.model_rebuild()
214
+ IsMissing.model_rebuild()
215
+ IsPresent.model_rebuild()
216
+ Contains.model_rebuild()
217
+ StartsWith.model_rebuild()
218
+ EndsWith.model_rebuild()
219
+ And.model_rebuild()
220
+ Or.model_rebuild()
221
+ Not.model_rebuild()
222
+
223
+
224
+ class HavingPredicateBase(StrictModel):
225
+ pass
226
+
227
+
228
+ class HavingEq(HavingPredicateBase):
229
+ op: Literal["eq"]
230
+ field: str
231
+ value: Scalar
232
+
233
+
234
+ class HavingNe(HavingPredicateBase):
235
+ op: Literal["ne"]
236
+ field: str
237
+ value: Scalar
238
+
239
+
240
+ class HavingGt(HavingPredicateBase):
241
+ op: Literal["gt"]
242
+ field: str
243
+ value: Scalar
244
+
245
+
246
+ class HavingGte(HavingPredicateBase):
247
+ op: Literal["gte"]
248
+ field: str
249
+ value: Scalar
250
+
251
+
252
+ class HavingLt(HavingPredicateBase):
253
+ op: Literal["lt"]
254
+ field: str
255
+ value: Scalar
256
+
257
+
258
+ class HavingLte(HavingPredicateBase):
259
+ op: Literal["lte"]
260
+ field: str
261
+ value: Scalar
262
+
263
+
264
+ class HavingBetween(HavingPredicateBase):
265
+ op: Literal["between"]
266
+ field: str
267
+ lower: Scalar
268
+ upper: Scalar
269
+
270
+
271
+ class HavingIsNull(HavingPredicateBase):
272
+ op: Literal["is_null"]
273
+ field: str
274
+
275
+
276
+ class HavingIsNotNull(HavingPredicateBase):
277
+ op: Literal["is_not_null"]
278
+ field: str
279
+
280
+
281
+ class HavingAnd(HavingPredicateBase):
282
+ op: Literal["and"]
283
+ conditions: list[HavingPredicate] = Field(min_length=2, max_length=32)
284
+
285
+
286
+ class HavingOr(HavingPredicateBase):
287
+ op: Literal["or"]
288
+ conditions: list[HavingPredicate] = Field(min_length=2, max_length=32)
289
+
290
+
291
+ class HavingNot(HavingPredicateBase):
292
+ op: Literal["not"]
293
+ condition: HavingPredicate
294
+
295
+
296
+ HavingPredicate: TypeAlias = Annotated[
297
+ HavingEq
298
+ | HavingNe
299
+ | HavingGt
300
+ | HavingGte
301
+ | HavingLt
302
+ | HavingLte
303
+ | HavingBetween
304
+ | HavingIsNull
305
+ | HavingIsNotNull
306
+ | HavingAnd
307
+ | HavingOr
308
+ | HavingNot,
309
+ Field(discriminator="op"),
310
+ ]
311
+
312
+ HavingEq.model_rebuild()
313
+ HavingNe.model_rebuild()
314
+ HavingGt.model_rebuild()
315
+ HavingGte.model_rebuild()
316
+ HavingLt.model_rebuild()
317
+ HavingLte.model_rebuild()
318
+ HavingBetween.model_rebuild()
319
+ HavingIsNull.model_rebuild()
320
+ HavingIsNotNull.model_rebuild()
321
+ HavingAnd.model_rebuild()
322
+ HavingOr.model_rebuild()
323
+ HavingNot.model_rebuild()
324
+
325
+
326
+ class CountAggregate(StrictModel):
327
+ op: Literal["count"]
328
+ alias: str = Field(alias="as")
329
+
330
+
331
+ class CountNonNullAggregate(StrictModel):
332
+ op: Literal["count_non_null"]
333
+ alias: str = Field(alias="as")
334
+ field: str
335
+
336
+
337
+ class NullCountAggregate(StrictModel):
338
+ op: Literal["null_count"]
339
+ alias: str = Field(alias="as")
340
+ field: str
341
+
342
+
343
+ class MissingCountAggregate(StrictModel):
344
+ op: Literal["missing_count"]
345
+ alias: str = Field(alias="as")
346
+ field: str
347
+
348
+
349
+ class CountDistinctAggregate(StrictModel):
350
+ op: Literal["count_distinct"]
351
+ alias: str = Field(alias="as")
352
+ field: str
353
+
354
+
355
+ class SumAggregate(StrictModel):
356
+ op: Literal["sum"]
357
+ alias: str = Field(alias="as")
358
+ field: str
359
+
360
+
361
+ class AvgAggregate(StrictModel):
362
+ op: Literal["avg"]
363
+ alias: str = Field(alias="as")
364
+ field: str
365
+
366
+
367
+ class MinAggregate(StrictModel):
368
+ op: Literal["min"]
369
+ alias: str = Field(alias="as")
370
+ field: str
371
+
372
+
373
+ class MaxAggregate(StrictModel):
374
+ op: Literal["max"]
375
+ alias: str = Field(alias="as")
376
+ field: str
377
+
378
+
379
+ class MedianAggregate(StrictModel):
380
+ op: Literal["median"]
381
+ alias: str = Field(alias="as")
382
+ field: str
383
+
384
+
385
+ class P50Aggregate(StrictModel):
386
+ op: Literal["p50"]
387
+ alias: str = Field(alias="as")
388
+ field: str
389
+
390
+
391
+ class P90Aggregate(StrictModel):
392
+ op: Literal["p90"]
393
+ alias: str = Field(alias="as")
394
+ field: str
395
+
396
+
397
+ class P95Aggregate(StrictModel):
398
+ op: Literal["p95"]
399
+ alias: str = Field(alias="as")
400
+ field: str
401
+
402
+
403
+ class P99Aggregate(StrictModel):
404
+ op: Literal["p99"]
405
+ alias: str = Field(alias="as")
406
+ field: str
407
+
408
+
409
+ class StddevAggregate(StrictModel):
410
+ op: Literal["stddev"]
411
+ alias: str = Field(alias="as")
412
+ field: str
413
+
414
+
415
+ class VarianceAggregate(StrictModel):
416
+ op: Literal["variance"]
417
+ alias: str = Field(alias="as")
418
+ field: str
419
+
420
+
421
+ class CountIfAggregate(StrictModel):
422
+ op: Literal["count_if"]
423
+ alias: str = Field(alias="as")
424
+ where: Predicate
425
+
426
+
427
+ class SumIfAggregate(StrictModel):
428
+ op: Literal["sum_if"]
429
+ alias: str = Field(alias="as")
430
+ field: str
431
+ where: Predicate
432
+
433
+
434
+ class AvgIfAggregate(StrictModel):
435
+ op: Literal["avg_if"]
436
+ alias: str = Field(alias="as")
437
+ field: str
438
+ where: Predicate
439
+
440
+
441
+ class RatioIfAggregate(StrictModel):
442
+ op: Literal["ratio_if"]
443
+ alias: str = Field(alias="as")
444
+ field: str
445
+ where: Predicate
446
+
447
+
448
+ Aggregate: TypeAlias = Annotated[
449
+ CountAggregate
450
+ | CountNonNullAggregate
451
+ | NullCountAggregate
452
+ | MissingCountAggregate
453
+ | CountDistinctAggregate
454
+ | SumAggregate
455
+ | AvgAggregate
456
+ | MinAggregate
457
+ | MaxAggregate
458
+ | MedianAggregate
459
+ | P50Aggregate
460
+ | P90Aggregate
461
+ | P95Aggregate
462
+ | P99Aggregate
463
+ | StddevAggregate
464
+ | VarianceAggregate
465
+ | CountIfAggregate
466
+ | SumIfAggregate
467
+ | AvgIfAggregate
468
+ | RatioIfAggregate,
469
+ Field(discriminator="op"),
470
+ ]
471
+
472
+
473
+ class Dataset(StrictModel):
474
+ name: str = Field(min_length=1, max_length=64, pattern=r"^[A-Za-z][A-Za-z0-9_-]*$")
475
+
476
+
477
+ class Cast(StrictModel):
478
+ field: str
479
+ to: Literal["float64", "int64", "string", "datetime"]
480
+ on_error: Literal["reject", "null"]
481
+ format: Literal["rfc3339", "epoch_s", "epoch_ms", "epoch_us"] | None = None
482
+
483
+
484
+ class AbsoluteTime(StrictModel):
485
+ field: str
486
+ start: str
487
+ end: str
488
+
489
+
490
+ class RelativeTime(StrictModel):
491
+ field: str
492
+ last_minutes: int = Field(ge=1, le=1440)
493
+
494
+
495
+ class Tumbling(StrictModel):
496
+ type: Literal["tumbling"]
497
+ size: str
498
+
499
+
500
+ class Sliding(StrictModel):
501
+ type: Literal["sliding"]
502
+ size: str
503
+ step: str
504
+
505
+
506
+ class Group(StrictModel):
507
+ field: str
508
+ alias: str = Field(alias="as")
509
+
510
+
511
+ class Order(StrictModel):
512
+ field: str
513
+ direction: Literal["asc", "desc"] = "asc"
514
+ nulls: Literal["first", "last"] = "last"
515
+
516
+
517
+ class Options(StrictModel):
518
+ include_empty_windows: bool = False
519
+ matching_details: MatchingDetails = Field(default_factory=MatchingDetails)
520
+
521
+
522
+ class AnalyticsQuery(StrictModel):
523
+ version: Literal["analytics.query/v1"]
524
+ dataset: Dataset
525
+ casts: list[Cast] = Field(default_factory=list, max_length=8)
526
+ time_range: AbsoluteTime | RelativeTime | None = None
527
+ filters: list[Predicate] = Field(default_factory=list, max_length=20)
528
+ window: Annotated[Tumbling | Sliding, Field(discriminator="type")] | None = None
529
+ group_by: list[Group] = Field(default_factory=list, max_length=4)
530
+ aggregations: list[Aggregate] = Field(min_length=1, max_length=20)
531
+ having: list[HavingPredicate] = Field(default_factory=list, max_length=10)
532
+ order_by: list[Order] = Field(default_factory=list, max_length=4)
533
+ limit: int = Field(default=100, ge=1, le=500)
534
+ options: Options = Field(default_factory=Options)
535
+
536
+
537
+ def parse_query(text: str, limits: Limits) -> AnalyticsQuery:
538
+ marks: dict[str, tuple[int, int]] = {}
539
+ data = parse_yaml(text, limits, source_marks=marks)
540
+ if data.get("version") != "analytics.query/v1":
541
+ raise AnalyticsError("QUERY_VERSION_UNSUPPORTED", "$.version")
542
+ if set(data) & {
543
+ "entity_inference",
544
+ "entity_resolution",
545
+ "resolve_entities",
546
+ "identities",
547
+ "aliases",
548
+ }:
549
+ raise AnalyticsError("MATCH_ENTITY_INFERENCE_UNSUPPORTED")
550
+ if set(data) & {"sources", "joins", "unions", "outputs", "continuous", "sinks"}:
551
+ raise AnalyticsError("QUERY_UNSUPPORTED_FEATURE")
552
+ try:
553
+ return AnalyticsQuery.model_validate(data)
554
+ except ValidationError as exc:
555
+ issue = exc.errors(include_input=False)[0]
556
+ loc = issue["loc"]
557
+ code = "QUERY_MALFORMED"
558
+ if loc and loc[0] == "having":
559
+ code = "QUERY_HAVING_INVALID"
560
+ elif "algorithm" in loc:
561
+ code = "MATCH_FUZZY_ALGORITHM_INVALID"
562
+ elif "min_score" in loc:
563
+ code = "MATCH_FUZZY_THRESHOLD_INVALID"
564
+ elif "normalization" in loc:
565
+ code = "MATCH_NORMALIZATION_INVALID"
566
+ elif issue["type"] == "string_too_short" and "value" in loc:
567
+ code = "MATCH_FUZZY_QUERY_TOO_SHORT" if "fuzzy" in loc else "MATCH_PATTERN_INVALID"
568
+ elif issue["type"] == "union_tag_invalid":
569
+ code = "QUERY_UNKNOWN_OPERATOR"
570
+ elif loc and loc[0] in {"time_range", "window", "order_by", "group_by", "having", "limit"}:
571
+ code = {
572
+ "time_range": "QUERY_TIME_RANGE_INVALID",
573
+ "window": "QUERY_WINDOW_INVALID",
574
+ "order_by": "QUERY_ORDER_INVALID",
575
+ "group_by": "QUERY_GROUP_INVALID",
576
+ "having": "QUERY_HAVING_INVALID",
577
+ "limit": "QUERY_LIMIT_INVALID",
578
+ }[loc[0]]
579
+ model_keys = {
580
+ "version",
581
+ "dataset",
582
+ "name",
583
+ "casts",
584
+ "field",
585
+ "to",
586
+ "format",
587
+ "on_error",
588
+ "time_range",
589
+ "start",
590
+ "end",
591
+ "last_minutes",
592
+ "filters",
593
+ "op",
594
+ "value",
595
+ "values",
596
+ "lower",
597
+ "upper",
598
+ "conditions",
599
+ "condition",
600
+ "window",
601
+ "type",
602
+ "size",
603
+ "step",
604
+ "group_by",
605
+ "as",
606
+ "aggregations",
607
+ "where",
608
+ "having",
609
+ "order_by",
610
+ "direction",
611
+ "nulls",
612
+ "limit",
613
+ "options",
614
+ "match",
615
+ "fields",
616
+ "patterns",
617
+ "mode",
618
+ "normalization",
619
+ "case_sensitive",
620
+ "unicode",
621
+ "trim",
622
+ "algorithm",
623
+ "min_score",
624
+ "matching_details",
625
+ "include",
626
+ "top_values",
627
+ "include_raw_values",
628
+ "include_empty_windows",
629
+ }
630
+ segments = [
631
+ v
632
+ for v in loc
633
+ if v
634
+ not in set(
635
+ FILTER_OPS
636
+ + AGG_OPS
637
+ + [
638
+ "single",
639
+ "LiteralRule",
640
+ "FuzzyRule",
641
+ "AbsoluteTime",
642
+ "RelativeTime",
643
+ "tumbling",
644
+ "sliding",
645
+ ]
646
+ )
647
+ ]
648
+ actual = "$" + "".join(f"[{v}]" if isinstance(v, int) else "." + str(v) for v in segments)
649
+ path = "$" + "".join(
650
+ f"[{v}]" if isinstance(v, int) else "." + (v if v in model_keys else "?")
651
+ for v in segments
652
+ )
653
+ line, column = marks.get(actual, marks.get("$", (1, 1)))
654
+ raise AnalyticsError(code, path, line=line, column=column) from None