skifer 2.0.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.
- skifer/__init__.py +45 -0
- skifer/adaptive/__init__.py +62 -0
- skifer/adaptive/aggregator.py +243 -0
- skifer/adaptive/evaluator.py +450 -0
- skifer/adaptive/generator.py +282 -0
- skifer/adaptive/models.py +344 -0
- skifer/adaptive/recommender.py +713 -0
- skifer/adaptive/store.py +264 -0
- skifer/adaptive/workflow.py +411 -0
- skifer/agentic/__init__.py +38 -0
- skifer/agentic/agent.py +878 -0
- skifer/agentic/builder_agent.py +551 -0
- skifer/agentic/data_service.py +735 -0
- skifer/agentic/dictionary_agent.py +418 -0
- skifer/agentic/exporter.py +264 -0
- skifer/agentic/history.py +237 -0
- skifer/agentic/hub.py +338 -0
- skifer/agentic/lineage_agent.py +429 -0
- skifer/agentic/models.py +334 -0
- skifer/agentic/orchestrator.py +249 -0
- skifer/agentic/quality_agent.py +384 -0
- skifer/agentic/resolver.py +730 -0
- skifer/agentic/user_profile.py +118 -0
- skifer/capabilities/__init__.py +140 -0
- skifer/capabilities/autonomy.py +497 -0
- skifer/capabilities/credentials.py +259 -0
- skifer/capabilities/executor.py +646 -0
- skifer/capabilities/executors.py +164 -0
- skifer/capabilities/harness.py +302 -0
- skifer/capabilities/history.py +292 -0
- skifer/capabilities/invoker.py +276 -0
- skifer/capabilities/models.py +203 -0
- skifer/capabilities/preconditions.py +468 -0
- skifer/capabilities/registry.py +192 -0
- skifer/capabilities/validator.py +576 -0
- skifer/cli.py +874 -0
- skifer/core/__init__.py +6 -0
- skifer/core/catalog_inspector.py +177 -0
- skifer/core/config.py +283 -0
- skifer/core/constants.py +45 -0
- skifer/core/context.py +84 -0
- skifer/core/core.py +1273 -0
- skifer/core/environment.py +238 -0
- skifer/core/interpreter.py +581 -0
- skifer/core/ir.py +534 -0
- skifer/core/json_schema.py +645 -0
- skifer/core/loaders.py +124 -0
- skifer/core/op_catalog.py +393 -0
- skifer/core/patterns.py +336 -0
- skifer/core/registry.py +186 -0
- skifer/core/rule_analyzer.py +548 -0
- skifer/core/rule_executor.py +172 -0
- skifer/core/rule_planner.py +214 -0
- skifer/core/sandbox.py +141 -0
- skifer/core/schema_loader.py +1489 -0
- skifer/core/spark_backend.py +1184 -0
- skifer/core/sql_compiler.py +521 -0
- skifer/core/writer.py +174 -0
- skifer/lineage/__init__.py +19 -0
- skifer/lineage/dictionary.py +173 -0
- skifer/lineage/renderer.py +126 -0
- skifer/lineage/tracker.py +428 -0
- skifer/mcp/__init__.py +49 -0
- skifer/mcp/auth.py +336 -0
- skifer/mcp/capability_tools.py +390 -0
- skifer/mcp/config.py +237 -0
- skifer/mcp/resources.py +307 -0
- skifer/mcp/runtime.py +277 -0
- skifer/mcp/server.py +196 -0
- skifer/mcp/tools.py +346 -0
- skifer/observability/__init__.py +34 -0
- skifer/observability/alerts.py +188 -0
- skifer/observability/certification.py +92 -0
- skifer/observability/certification_store.py +313 -0
- skifer/observability/checks.py +600 -0
- skifer/observability/contracts.py +180 -0
- skifer/observability/history.py +235 -0
- skifer/observability/monitor.py +190 -0
- skifer/observability/odcs.py +74 -0
- skifer/observability/publication.py +218 -0
- skifer/observability/quarantine.py +62 -0
- skifer/observability/reporter.py +138 -0
- skifer/observability/tracing.py +770 -0
- skifer/observability/tracing_exporters.py +586 -0
- skifer/observability/uc_mirror.py +27 -0
- skifer/registry.py +7 -0
- skifer/semantic/__init__.py +47 -0
- skifer/semantic/access_policy.py +207 -0
- skifer/semantic/builder.py +696 -0
- skifer/semantic/calendar.py +100 -0
- skifer/semantic/dependencies.py +19 -0
- skifer/semantic/domain.py +107 -0
- skifer/semantic/domain_graph.py +315 -0
- skifer/semantic/draft_builder.py +529 -0
- skifer/semantic/evidence.py +349 -0
- skifer/semantic/extractor.py +132 -0
- skifer/semantic/glossary.py +141 -0
- skifer/semantic/llm_provider.py +487 -0
- skifer/semantic/output_projection.py +324 -0
- skifer/semantic/persistence.py +101 -0
- skifer/semantic/planner.py +629 -0
- skifer/semantic/semantic.py +1007 -0
- skifer/semantic/sync.py +694 -0
- skifer/semantic/validator.py +581 -0
- skifer/serving/__init__.py +20 -0
- skifer/serving/_response_serializer.py +214 -0
- skifer/serving/chat_model.py +162 -0
- skifer/sinks/__init__.py +3 -0
- skifer/sinks/jdbc.py +52 -0
- skifer/spark_factory.py +117 -0
- skifer/utils.py +130 -0
- skifer-2.0.0.dist-info/METADATA +778 -0
- skifer-2.0.0.dist-info/RECORD +117 -0
- skifer-2.0.0.dist-info/WHEEL +5 -0
- skifer-2.0.0.dist-info/entry_points.txt +2 -0
- skifer-2.0.0.dist-info/licenses/LICENSE +21 -0
- skifer-2.0.0.dist-info/top_level.txt +1 -0
skifer/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from .core.core import SkiferEngine
|
|
2
|
+
from .core.registry import RuleRegistry
|
|
3
|
+
from .core.config import ConfigurationManager
|
|
4
|
+
from .utils import safe_columns, setup_notebook, configure_logging
|
|
5
|
+
from .core.schema_loader import load_schema, parse_schema
|
|
6
|
+
from .core.rule_analyzer import RuleAnalyzer
|
|
7
|
+
from .core.rule_planner import RulePlanner, RuleCycleError
|
|
8
|
+
from .core.rule_executor import RuleExecutor
|
|
9
|
+
from .core.catalog_inspector import CatalogInspector, CatalogError
|
|
10
|
+
from .agentic.builder_agent import BuilderAgent
|
|
11
|
+
from .lineage import LineageTracker, LineageGraph, LineageEdge, DataDictionary, LineageRenderer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def __getattr__(name):
|
|
15
|
+
# Lazy export: SparkBackend requires pyspark (optional [spark] extra), so
|
|
16
|
+
# importing the package must not pull it in (CLI validate, semantic layer).
|
|
17
|
+
if name == "SparkBackend":
|
|
18
|
+
from .core.spark_backend import SparkBackend
|
|
19
|
+
return SparkBackend
|
|
20
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"SkiferEngine",
|
|
25
|
+
"SparkBackend",
|
|
26
|
+
"RuleRegistry",
|
|
27
|
+
"ConfigurationManager",
|
|
28
|
+
"safe_columns",
|
|
29
|
+
"setup_notebook",
|
|
30
|
+
"configure_logging",
|
|
31
|
+
"load_schema",
|
|
32
|
+
"parse_schema",
|
|
33
|
+
"RuleAnalyzer",
|
|
34
|
+
"RulePlanner",
|
|
35
|
+
"RuleCycleError",
|
|
36
|
+
"RuleExecutor",
|
|
37
|
+
"CatalogInspector",
|
|
38
|
+
"CatalogError",
|
|
39
|
+
"BuilderAgent",
|
|
40
|
+
"LineageTracker",
|
|
41
|
+
"LineageGraph",
|
|
42
|
+
"LineageEdge",
|
|
43
|
+
"DataDictionary",
|
|
44
|
+
"LineageRenderer",
|
|
45
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Supervised adaptive Gold foundations."""
|
|
2
|
+
|
|
3
|
+
from .aggregator import PatternAggregate, PatternAggregationResult, PatternAggregator
|
|
4
|
+
from .generator import ProposalGenerationError, ProposalGenerator
|
|
5
|
+
from .evaluator import (
|
|
6
|
+
DeliveryRecord,
|
|
7
|
+
OutcomeEvaluation,
|
|
8
|
+
OutcomeEvaluationError,
|
|
9
|
+
OutcomeEvaluator,
|
|
10
|
+
WindowMetrics,
|
|
11
|
+
)
|
|
12
|
+
from .models import (
|
|
13
|
+
OptimizationProposal,
|
|
14
|
+
SemanticUsageEvent,
|
|
15
|
+
fingerprint_query,
|
|
16
|
+
usage_event_from_evidence,
|
|
17
|
+
)
|
|
18
|
+
from .recommender import (
|
|
19
|
+
RULE_REGISTRY,
|
|
20
|
+
RecommendationContext,
|
|
21
|
+
RecommendationEngine,
|
|
22
|
+
RecommendationRefusal,
|
|
23
|
+
RecommendationResult,
|
|
24
|
+
RecommendationThresholds,
|
|
25
|
+
)
|
|
26
|
+
from .store import DeltaUsageEventStore, SqliteUsageEventStore, UsageEventStore
|
|
27
|
+
from .workflow import (
|
|
28
|
+
AdaptiveWorkflow,
|
|
29
|
+
AdaptiveWorkflowConflict,
|
|
30
|
+
AdaptiveWorkflowError,
|
|
31
|
+
StaleProposalError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"DeltaUsageEventStore",
|
|
36
|
+
"DeliveryRecord",
|
|
37
|
+
"AdaptiveWorkflow",
|
|
38
|
+
"AdaptiveWorkflowConflict",
|
|
39
|
+
"AdaptiveWorkflowError",
|
|
40
|
+
"PatternAggregate",
|
|
41
|
+
"PatternAggregationResult",
|
|
42
|
+
"PatternAggregator",
|
|
43
|
+
"OptimizationProposal",
|
|
44
|
+
"OutcomeEvaluation",
|
|
45
|
+
"OutcomeEvaluationError",
|
|
46
|
+
"OutcomeEvaluator",
|
|
47
|
+
"ProposalGenerationError",
|
|
48
|
+
"ProposalGenerator",
|
|
49
|
+
"RULE_REGISTRY",
|
|
50
|
+
"RecommendationContext",
|
|
51
|
+
"RecommendationEngine",
|
|
52
|
+
"RecommendationRefusal",
|
|
53
|
+
"RecommendationResult",
|
|
54
|
+
"RecommendationThresholds",
|
|
55
|
+
"SemanticUsageEvent",
|
|
56
|
+
"SqliteUsageEventStore",
|
|
57
|
+
"StaleProposalError",
|
|
58
|
+
"UsageEventStore",
|
|
59
|
+
"WindowMetrics",
|
|
60
|
+
"fingerprint_query",
|
|
61
|
+
"usage_event_from_evidence",
|
|
62
|
+
]
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Deterministic aggregation of privacy-safe semantic usage patterns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
import math
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Callable, Iterable
|
|
10
|
+
|
|
11
|
+
from .models import SemanticUsageEvent
|
|
12
|
+
from .store import UsageEventStore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_WINDOWS = ("7d", "30d")
|
|
16
|
+
DEFAULT_DURATION_PERCENTILE = 95.0
|
|
17
|
+
DEFAULT_PERCENTILE_MIN_POINTS = 5
|
|
18
|
+
_WINDOW_PATTERN = re.compile(r"^([1-9][0-9]*)d$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class PatternAggregate:
|
|
23
|
+
"""One status-specific pattern inside one strict usage partition."""
|
|
24
|
+
|
|
25
|
+
window: str
|
|
26
|
+
window_started_at: datetime
|
|
27
|
+
window_ended_at: datetime
|
|
28
|
+
environment: str
|
|
29
|
+
consumer_class: str
|
|
30
|
+
model_hashes: tuple[str, ...]
|
|
31
|
+
metric_ids: tuple[str, ...]
|
|
32
|
+
dimension_ids: tuple[str, ...]
|
|
33
|
+
normalized_filter_shape: tuple[str, ...]
|
|
34
|
+
query_fingerprint: str
|
|
35
|
+
status: str
|
|
36
|
+
event_count: int
|
|
37
|
+
duration_percentile: float | None
|
|
38
|
+
duration_point_count: int
|
|
39
|
+
event_ids: tuple[str, ...]
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
"""Serialize by allowlist so later fields are private by default."""
|
|
43
|
+
return {
|
|
44
|
+
"window": self.window,
|
|
45
|
+
"window_started_at": self.window_started_at.astimezone(
|
|
46
|
+
timezone.utc
|
|
47
|
+
).isoformat(),
|
|
48
|
+
"window_ended_at": self.window_ended_at.astimezone(timezone.utc).isoformat(),
|
|
49
|
+
"environment": self.environment,
|
|
50
|
+
"consumer_class": self.consumer_class,
|
|
51
|
+
"model_hashes": list(self.model_hashes),
|
|
52
|
+
"metric_ids": list(self.metric_ids),
|
|
53
|
+
"dimension_ids": list(self.dimension_ids),
|
|
54
|
+
"normalized_filter_shape": list(self.normalized_filter_shape),
|
|
55
|
+
"query_fingerprint": self.query_fingerprint,
|
|
56
|
+
"status": self.status,
|
|
57
|
+
"event_count": self.event_count,
|
|
58
|
+
"duration_percentile": self.duration_percentile,
|
|
59
|
+
"duration_point_count": self.duration_point_count,
|
|
60
|
+
"event_ids": list(self.event_ids),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class PatternAggregationResult:
|
|
66
|
+
"""Frozen, standalone aggregate output ordered canonically."""
|
|
67
|
+
|
|
68
|
+
generated_at: datetime
|
|
69
|
+
patterns: tuple[PatternAggregate, ...]
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> dict[str, Any]:
|
|
72
|
+
"""Serialize only the deliberately public aggregate fields."""
|
|
73
|
+
return {
|
|
74
|
+
"generated_at": self.generated_at.astimezone(timezone.utc).isoformat(),
|
|
75
|
+
"patterns": [pattern.to_dict() for pattern in self.patterns],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _aware_utc(value: Any, field_name: str) -> datetime:
|
|
80
|
+
if not isinstance(value, datetime):
|
|
81
|
+
raise TypeError(f"{field_name} must be a datetime.")
|
|
82
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
83
|
+
raise ValueError(f"{field_name} must be timezone-aware.")
|
|
84
|
+
return value.astimezone(timezone.utc)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _positive_int(value: Any, field_name: str) -> int:
|
|
88
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
89
|
+
raise ValueError(f"{field_name} must be a positive integer.")
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse_windows(windows: Iterable[str]) -> tuple[tuple[str, int], ...]:
|
|
94
|
+
if isinstance(windows, (str, bytes)):
|
|
95
|
+
raise TypeError("windows must be an iterable of '<days>d' strings.")
|
|
96
|
+
try:
|
|
97
|
+
supplied = tuple(windows)
|
|
98
|
+
except TypeError as exc:
|
|
99
|
+
raise TypeError("windows must be an iterable of '<days>d' strings.") from exc
|
|
100
|
+
if not supplied:
|
|
101
|
+
raise ValueError("windows must contain at least one window.")
|
|
102
|
+
|
|
103
|
+
parsed: list[tuple[str, int]] = []
|
|
104
|
+
seen_days: set[int] = set()
|
|
105
|
+
for value in supplied:
|
|
106
|
+
if not isinstance(value, str):
|
|
107
|
+
raise TypeError("Each window must be a '<days>d' string.")
|
|
108
|
+
match = _WINDOW_PATTERN.fullmatch(value)
|
|
109
|
+
if match is None:
|
|
110
|
+
raise ValueError(f"Invalid aggregation window {value!r}; expected '<days>d'.")
|
|
111
|
+
days = int(match.group(1))
|
|
112
|
+
if days in seen_days:
|
|
113
|
+
raise ValueError(f"Duplicate aggregation window {value!r}.")
|
|
114
|
+
seen_days.add(days)
|
|
115
|
+
parsed.append((value, days))
|
|
116
|
+
return tuple(sorted(parsed, key=lambda item: (item[1], item[0])))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _validate_percentile(value: Any) -> float:
|
|
120
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
121
|
+
raise TypeError("duration_percentile must be numeric.")
|
|
122
|
+
percentile = float(value)
|
|
123
|
+
if not math.isfinite(percentile) or not 0 <= percentile <= 100:
|
|
124
|
+
raise ValueError("duration_percentile must be between 0 and 100 inclusive.")
|
|
125
|
+
return percentile
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def nearest_rank_percentile(values: list[int], percentile: float) -> float:
|
|
129
|
+
"""Return the nearest-rank percentile; p=0 selects the observed minimum.
|
|
130
|
+
|
|
131
|
+
Nearest rank avoids inventing a duration between observations. For ``n`` sorted
|
|
132
|
+
points the one-based rank is ``max(1, ceil(p / 100 * n))``. Thus both bounds are
|
|
133
|
+
explicit: p0 is the minimum and p100 is the maximum.
|
|
134
|
+
"""
|
|
135
|
+
ordered = sorted(values)
|
|
136
|
+
rank = max(1, math.ceil(percentile / 100 * len(ordered)))
|
|
137
|
+
return float(ordered[rank - 1])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class PatternAggregator:
|
|
141
|
+
"""Build deterministic windowed patterns without scanning outside the window."""
|
|
142
|
+
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
store: UsageEventStore,
|
|
146
|
+
*,
|
|
147
|
+
clock: Callable[[], datetime],
|
|
148
|
+
windows: Iterable[str] = DEFAULT_WINDOWS,
|
|
149
|
+
min_count: int = 1,
|
|
150
|
+
duration_percentile: float = DEFAULT_DURATION_PERCENTILE,
|
|
151
|
+
percentile_min_points: int = DEFAULT_PERCENTILE_MIN_POINTS,
|
|
152
|
+
):
|
|
153
|
+
if not callable(clock):
|
|
154
|
+
raise TypeError("clock must be callable.")
|
|
155
|
+
if not callable(getattr(store, "list_events", None)):
|
|
156
|
+
raise TypeError("store must provide list_events().")
|
|
157
|
+
self._store = store
|
|
158
|
+
self._clock = clock
|
|
159
|
+
self._windows = _parse_windows(windows)
|
|
160
|
+
self._min_count = _positive_int(min_count, "min_count")
|
|
161
|
+
self._duration_percentile = _validate_percentile(duration_percentile)
|
|
162
|
+
self._percentile_min_points = _positive_int(
|
|
163
|
+
percentile_min_points, "percentile_min_points"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def aggregate(self) -> PatternAggregationResult:
|
|
167
|
+
"""Read the bounded event range once and aggregate every configured window."""
|
|
168
|
+
now = _aware_utc(self._clock(), "clock result")
|
|
169
|
+
earliest = now - timedelta(days=max(days for _, days in self._windows))
|
|
170
|
+
events = self._store.list_events(since=earliest, until=now)
|
|
171
|
+
for event in events:
|
|
172
|
+
if not isinstance(event, SemanticUsageEvent):
|
|
173
|
+
raise TypeError("UsageEventStore returned a non-SemanticUsageEvent value.")
|
|
174
|
+
|
|
175
|
+
patterns: list[PatternAggregate] = []
|
|
176
|
+
for window, days in self._windows:
|
|
177
|
+
window_start = now - timedelta(days=days)
|
|
178
|
+
groups: dict[tuple[Any, ...], list[SemanticUsageEvent]] = {}
|
|
179
|
+
for event in events:
|
|
180
|
+
occurred_at = _aware_utc(event.occurred_at, "event.occurred_at")
|
|
181
|
+
if not window_start <= occurred_at <= now:
|
|
182
|
+
continue
|
|
183
|
+
key = (
|
|
184
|
+
event.environment,
|
|
185
|
+
event.consumer_class,
|
|
186
|
+
tuple(sorted(event.model_hashes)),
|
|
187
|
+
tuple(sorted(event.metric_ids)),
|
|
188
|
+
tuple(sorted(event.dimension_ids)),
|
|
189
|
+
tuple(sorted(event.normalized_filter_shape)),
|
|
190
|
+
event.query_fingerprint,
|
|
191
|
+
event.status,
|
|
192
|
+
)
|
|
193
|
+
groups.setdefault(key, []).append(event)
|
|
194
|
+
|
|
195
|
+
for key in sorted(groups):
|
|
196
|
+
grouped_events = groups[key]
|
|
197
|
+
if len(grouped_events) < self._min_count:
|
|
198
|
+
continue
|
|
199
|
+
(
|
|
200
|
+
environment,
|
|
201
|
+
consumer_class,
|
|
202
|
+
model_hashes,
|
|
203
|
+
metric_ids,
|
|
204
|
+
dimension_ids,
|
|
205
|
+
filter_shape,
|
|
206
|
+
fingerprint,
|
|
207
|
+
status,
|
|
208
|
+
) = key
|
|
209
|
+
durations = (
|
|
210
|
+
sorted(
|
|
211
|
+
event.duration_ms
|
|
212
|
+
for event in grouped_events
|
|
213
|
+
if event.duration_ms is not None
|
|
214
|
+
)
|
|
215
|
+
if status == "succeeded"
|
|
216
|
+
else []
|
|
217
|
+
)
|
|
218
|
+
duration_value = None
|
|
219
|
+
if len(durations) >= self._percentile_min_points:
|
|
220
|
+
duration_value = nearest_rank_percentile(
|
|
221
|
+
durations, self._duration_percentile
|
|
222
|
+
)
|
|
223
|
+
patterns.append(
|
|
224
|
+
PatternAggregate(
|
|
225
|
+
window=window,
|
|
226
|
+
window_started_at=window_start,
|
|
227
|
+
window_ended_at=now,
|
|
228
|
+
environment=environment,
|
|
229
|
+
consumer_class=consumer_class,
|
|
230
|
+
model_hashes=model_hashes,
|
|
231
|
+
metric_ids=metric_ids,
|
|
232
|
+
dimension_ids=dimension_ids,
|
|
233
|
+
normalized_filter_shape=filter_shape,
|
|
234
|
+
query_fingerprint=fingerprint,
|
|
235
|
+
status=status,
|
|
236
|
+
event_count=len(grouped_events),
|
|
237
|
+
duration_percentile=duration_value,
|
|
238
|
+
duration_point_count=len(durations),
|
|
239
|
+
event_ids=tuple(sorted(event.event_id for event in grouped_events)),
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return PatternAggregationResult(generated_at=now, patterns=tuple(patterns))
|