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.
Files changed (117) hide show
  1. skifer/__init__.py +45 -0
  2. skifer/adaptive/__init__.py +62 -0
  3. skifer/adaptive/aggregator.py +243 -0
  4. skifer/adaptive/evaluator.py +450 -0
  5. skifer/adaptive/generator.py +282 -0
  6. skifer/adaptive/models.py +344 -0
  7. skifer/adaptive/recommender.py +713 -0
  8. skifer/adaptive/store.py +264 -0
  9. skifer/adaptive/workflow.py +411 -0
  10. skifer/agentic/__init__.py +38 -0
  11. skifer/agentic/agent.py +878 -0
  12. skifer/agentic/builder_agent.py +551 -0
  13. skifer/agentic/data_service.py +735 -0
  14. skifer/agentic/dictionary_agent.py +418 -0
  15. skifer/agentic/exporter.py +264 -0
  16. skifer/agentic/history.py +237 -0
  17. skifer/agentic/hub.py +338 -0
  18. skifer/agentic/lineage_agent.py +429 -0
  19. skifer/agentic/models.py +334 -0
  20. skifer/agentic/orchestrator.py +249 -0
  21. skifer/agentic/quality_agent.py +384 -0
  22. skifer/agentic/resolver.py +730 -0
  23. skifer/agentic/user_profile.py +118 -0
  24. skifer/capabilities/__init__.py +140 -0
  25. skifer/capabilities/autonomy.py +497 -0
  26. skifer/capabilities/credentials.py +259 -0
  27. skifer/capabilities/executor.py +646 -0
  28. skifer/capabilities/executors.py +164 -0
  29. skifer/capabilities/harness.py +302 -0
  30. skifer/capabilities/history.py +292 -0
  31. skifer/capabilities/invoker.py +276 -0
  32. skifer/capabilities/models.py +203 -0
  33. skifer/capabilities/preconditions.py +468 -0
  34. skifer/capabilities/registry.py +192 -0
  35. skifer/capabilities/validator.py +576 -0
  36. skifer/cli.py +874 -0
  37. skifer/core/__init__.py +6 -0
  38. skifer/core/catalog_inspector.py +177 -0
  39. skifer/core/config.py +283 -0
  40. skifer/core/constants.py +45 -0
  41. skifer/core/context.py +84 -0
  42. skifer/core/core.py +1273 -0
  43. skifer/core/environment.py +238 -0
  44. skifer/core/interpreter.py +581 -0
  45. skifer/core/ir.py +534 -0
  46. skifer/core/json_schema.py +645 -0
  47. skifer/core/loaders.py +124 -0
  48. skifer/core/op_catalog.py +393 -0
  49. skifer/core/patterns.py +336 -0
  50. skifer/core/registry.py +186 -0
  51. skifer/core/rule_analyzer.py +548 -0
  52. skifer/core/rule_executor.py +172 -0
  53. skifer/core/rule_planner.py +214 -0
  54. skifer/core/sandbox.py +141 -0
  55. skifer/core/schema_loader.py +1489 -0
  56. skifer/core/spark_backend.py +1184 -0
  57. skifer/core/sql_compiler.py +521 -0
  58. skifer/core/writer.py +174 -0
  59. skifer/lineage/__init__.py +19 -0
  60. skifer/lineage/dictionary.py +173 -0
  61. skifer/lineage/renderer.py +126 -0
  62. skifer/lineage/tracker.py +428 -0
  63. skifer/mcp/__init__.py +49 -0
  64. skifer/mcp/auth.py +336 -0
  65. skifer/mcp/capability_tools.py +390 -0
  66. skifer/mcp/config.py +237 -0
  67. skifer/mcp/resources.py +307 -0
  68. skifer/mcp/runtime.py +277 -0
  69. skifer/mcp/server.py +196 -0
  70. skifer/mcp/tools.py +346 -0
  71. skifer/observability/__init__.py +34 -0
  72. skifer/observability/alerts.py +188 -0
  73. skifer/observability/certification.py +92 -0
  74. skifer/observability/certification_store.py +313 -0
  75. skifer/observability/checks.py +600 -0
  76. skifer/observability/contracts.py +180 -0
  77. skifer/observability/history.py +235 -0
  78. skifer/observability/monitor.py +190 -0
  79. skifer/observability/odcs.py +74 -0
  80. skifer/observability/publication.py +218 -0
  81. skifer/observability/quarantine.py +62 -0
  82. skifer/observability/reporter.py +138 -0
  83. skifer/observability/tracing.py +770 -0
  84. skifer/observability/tracing_exporters.py +586 -0
  85. skifer/observability/uc_mirror.py +27 -0
  86. skifer/registry.py +7 -0
  87. skifer/semantic/__init__.py +47 -0
  88. skifer/semantic/access_policy.py +207 -0
  89. skifer/semantic/builder.py +696 -0
  90. skifer/semantic/calendar.py +100 -0
  91. skifer/semantic/dependencies.py +19 -0
  92. skifer/semantic/domain.py +107 -0
  93. skifer/semantic/domain_graph.py +315 -0
  94. skifer/semantic/draft_builder.py +529 -0
  95. skifer/semantic/evidence.py +349 -0
  96. skifer/semantic/extractor.py +132 -0
  97. skifer/semantic/glossary.py +141 -0
  98. skifer/semantic/llm_provider.py +487 -0
  99. skifer/semantic/output_projection.py +324 -0
  100. skifer/semantic/persistence.py +101 -0
  101. skifer/semantic/planner.py +629 -0
  102. skifer/semantic/semantic.py +1007 -0
  103. skifer/semantic/sync.py +694 -0
  104. skifer/semantic/validator.py +581 -0
  105. skifer/serving/__init__.py +20 -0
  106. skifer/serving/_response_serializer.py +214 -0
  107. skifer/serving/chat_model.py +162 -0
  108. skifer/sinks/__init__.py +3 -0
  109. skifer/sinks/jdbc.py +52 -0
  110. skifer/spark_factory.py +117 -0
  111. skifer/utils.py +130 -0
  112. skifer-2.0.0.dist-info/METADATA +778 -0
  113. skifer-2.0.0.dist-info/RECORD +117 -0
  114. skifer-2.0.0.dist-info/WHEEL +5 -0
  115. skifer-2.0.0.dist-info/entry_points.txt +2 -0
  116. skifer-2.0.0.dist-info/licenses/LICENSE +21 -0
  117. 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))