welt-client 0.7.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.
welt/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """Welt Python SDK."""
2
+
3
+ from .client import AsyncClient, AsyncJob, Client, Job
4
+ from .credentials import Credential
5
+ from .errors import (
6
+ AuthenticationError,
7
+ CapacityError,
8
+ ExecutionError,
9
+ InvalidInputError,
10
+ ModelUnavailableError,
11
+ WeltError,
12
+ ConflictError, JobCancelledError, JobTimeoutError, NotFoundError,
13
+ PermissionDeniedError, PredictionPendingError, RateLimitError,
14
+ ResultExpiredError, ResultDeletedError, TransportError,
15
+ )
16
+ from .estimators import Classifier, Regressor
17
+ from .causal import CausalDiscovery, CausalResult
18
+ from .errors import InvalidCausalResultError, OptionalDependencyError, UnsupportedGraphConversionError
19
+
20
+ __all__ = [
21
+ "AsyncClient", "AsyncJob", "Credential", "ConflictError", "JobCancelledError",
22
+ "JobTimeoutError", "NotFoundError", "PermissionDeniedError", "PredictionPendingError",
23
+ "RateLimitError", "ResultExpiredError", "ResultDeletedError", "TransportError",
24
+ "AuthenticationError",
25
+ "CapacityError",
26
+ "Classifier",
27
+ "Client",
28
+ "ExecutionError",
29
+ "InvalidInputError",
30
+ "Job",
31
+ "ModelUnavailableError",
32
+ "Regressor",
33
+ "WeltError",
34
+ "CausalDiscovery", "CausalResult", "InvalidCausalResultError",
35
+ "OptionalDependencyError", "UnsupportedGraphConversionError",
36
+ ]
37
+ __version__ = "0.7.0"
welt/causal.py ADDED
@@ -0,0 +1,353 @@
1
+ """Owned discovery results and explicit local graph conversion.
2
+
3
+ No model runtime or optional causal dependency is imported during SDK import.
4
+ """
5
+
6
+ from copy import deepcopy
7
+ from dataclasses import dataclass, field
8
+ from importlib.metadata import PackageNotFoundError, version
9
+ from types import MappingProxyType
10
+
11
+ from sklearn.base import BaseEstimator
12
+
13
+ from .credentials import credential
14
+ from .errors import (
15
+ InvalidCausalResultError,
16
+ OptionalDependencyError,
17
+ UnsupportedGraphConversionError,
18
+ )
19
+
20
+
21
+ def _freeze(value):
22
+ if isinstance(value, dict):
23
+ return MappingProxyType({k: _freeze(v) for k, v in value.items()})
24
+ if isinstance(value, (list, tuple)):
25
+ return tuple(_freeze(v) for v in value)
26
+ return value
27
+
28
+
29
+ def _thaw(value):
30
+ if isinstance(value, MappingProxyType):
31
+ return {k: _thaw(v) for k, v in value.items()}
32
+ if isinstance(value, tuple):
33
+ return [_thaw(v) for v in value]
34
+ return value
35
+
36
+
37
+ def _graph(payload):
38
+ if not isinstance(payload, dict):
39
+ raise ValueError("Result must be a structured causal object.")
40
+ names = payload["variables"]
41
+ if (
42
+ not isinstance(names, (list, tuple))
43
+ or not names
44
+ or any(not isinstance(n, str) or not n or len(n) > 128 for n in names)
45
+ or len(names) != len(set(names))
46
+ ):
47
+ raise ValueError("Causal variables must retain unique ordered names.")
48
+ if payload["native_graph_type"] not in ("dag", "directed"):
49
+ raise ValueError("This SDK slice supports native directed graphs only.")
50
+ edges, pairs = [], set()
51
+ for value in payload["edge_marks"]:
52
+ if not isinstance(value, dict) or set(value) != {"u", "v", "u_mark", "v_mark"}:
53
+ raise ValueError("Edges must have explicit native endpoint marks.")
54
+ u, v = value["u"], value["v"]
55
+ marks = value["u_mark"], value["v_mark"]
56
+ if (
57
+ u not in names
58
+ or v not in names
59
+ or u == v
60
+ or marks not in (("tail", "arrow"), ("arrow", "tail"))
61
+ ):
62
+ raise ValueError("Unsupported or invalid native directed edge.")
63
+ pair = (u, v) if marks == ("tail", "arrow") else (v, u)
64
+ if pair in pairs:
65
+ raise ValueError("Duplicate directed edge.")
66
+ pairs.add(pair)
67
+ edges.append(deepcopy(value))
68
+ if payload["native_graph_type"] == "dag":
69
+ children = {n: [] for n in names}
70
+ indegree = dict.fromkeys(names, 0)
71
+ for source, target in pairs:
72
+ children[source].append(target)
73
+ indegree[target] += 1
74
+ ready = [n for n in names if indegree[n] == 0]
75
+ visited = 0
76
+ while ready:
77
+ source = ready.pop()
78
+ visited += 1
79
+ for target in children[source]:
80
+ indegree[target] -= 1
81
+ if indegree[target] == 0:
82
+ ready.append(target)
83
+ if visited != len(names):
84
+ raise ValueError("Native DAG contains a directed cycle.")
85
+ return tuple(names), edges
86
+
87
+
88
+ @dataclass(frozen=True, init=False)
89
+ class CausalResult:
90
+ """Immutable native result; score fetches use explicit client/job ownership."""
91
+
92
+ _payload: object = field(repr=False)
93
+
94
+ def __init__(self, payload):
95
+ try:
96
+ _graph(payload)
97
+ for name in (
98
+ "id",
99
+ "workspace_id",
100
+ "dataset_id",
101
+ "model_version",
102
+ "configuration_version",
103
+ "score_reference",
104
+ ):
105
+ if not isinstance(payload[name], str) or not payload[name]:
106
+ raise ValueError("Missing result provenance.")
107
+ if payload["score_semantics"] != "native_directed_edge_probability":
108
+ raise ValueError("Unsupported causal score semantics.")
109
+ if not isinstance(payload["diagnostics"], dict) or not isinstance(
110
+ payload["decoder"], dict
111
+ ):
112
+ raise ValueError("Result must preserve decoder/provenance metadata.")
113
+ job_id = payload["diagnostics"].get("job_id")
114
+ if not isinstance(job_id, str) or not job_id or len(job_id) > 128:
115
+ raise ValueError(
116
+ "Result must preserve its owned discovery job identity."
117
+ )
118
+ if not isinstance(payload["assumptions"], list) or any(
119
+ not isinstance(a, str) for a in payload["assumptions"]
120
+ ):
121
+ raise ValueError("Assumptions must be explicit strings.")
122
+ if not isinstance(payload["constraints_applied"], dict):
123
+ raise ValueError("Constraints must be explicit.")
124
+ except (KeyError, TypeError, ValueError):
125
+ raise InvalidCausalResultError(
126
+ "Invalid native causal result; no graph repair was applied.",
127
+ code="invalid_causal_result",
128
+ ) from None
129
+ object.__setattr__(self, "_payload", _freeze(deepcopy(payload)))
130
+
131
+ def to_dict(self):
132
+ return _thaw(self._payload)
133
+
134
+ @property
135
+ def variables(self):
136
+ return self._payload["variables"]
137
+
138
+ @property
139
+ def id(self):
140
+ return self._payload["id"]
141
+
142
+ @property
143
+ def score_reference(self):
144
+ return self._payload["score_reference"]
145
+
146
+ @property
147
+ def model_version(self):
148
+ return self._payload["model_version"]
149
+
150
+ @property
151
+ def dataset_id(self):
152
+ return self._payload["dataset_id"]
153
+
154
+ @property
155
+ def job_id(self):
156
+ return self._payload["diagnostics"]["job_id"]
157
+
158
+ @property
159
+ def configuration_version(self):
160
+ return self._payload["configuration_version"]
161
+
162
+ @property
163
+ def diagnostics(self):
164
+ return self._payload["diagnostics"]
165
+
166
+ @property
167
+ def decoder(self):
168
+ return self._payload["decoder"]
169
+
170
+ @property
171
+ def assumptions(self):
172
+ return self._payload["assumptions"]
173
+
174
+ @property
175
+ def edge_marks(self):
176
+ return self._payload["edge_marks"]
177
+
178
+ @property
179
+ def native_graph_type(self):
180
+ return self._payload["native_graph_type"]
181
+
182
+ def to_ergodic(self):
183
+ """Convert a declared native DAG locally, preserving marks and all nodes."""
184
+ if self.native_graph_type != "dag":
185
+ raise UnsupportedGraphConversionError(
186
+ "Only native DAG conversion is qualified; no projection was applied.",
187
+ code="unsupported_graph_conversion",
188
+ )
189
+ try:
190
+ if version("ergodic") != "0.1.1":
191
+ raise OptionalDependencyError(
192
+ "Install the approved Ergodic co-release artifact; the public dependency is not yet available.",
193
+ code="optional_dependency_unavailable",
194
+ )
195
+ import ergodic
196
+
197
+ if not hasattr(ergodic, "MixedGraph"):
198
+ raise OptionalDependencyError(
199
+ "The installed Ergodic package lacks the approved graph API.",
200
+ code="optional_dependency_unavailable",
201
+ )
202
+ except (ImportError, PackageNotFoundError):
203
+ raise OptionalDependencyError(
204
+ "Install the approved Ergodic co-release artifact; Welt does not download it automatically.",
205
+ code="optional_dependency_unavailable",
206
+ ) from None
207
+ payload = dict(
208
+ kind="dag",
209
+ nodes=list(self.variables),
210
+ edges=[_thaw(edge) for edge in self.edge_marks],
211
+ )
212
+ try:
213
+ graph = ergodic.MixedGraph.from_dict(payload)
214
+ except (ValueError, TypeError, AttributeError):
215
+ raise InvalidCausalResultError(
216
+ "Ergodic rejected native DAG conversion; no graph repair was applied.",
217
+ code="invalid_causal_result",
218
+ ) from None
219
+ # MixedGraph stores node sets; ensure actual conversion retains all names.
220
+ if graph.nodes != frozenset(self.variables):
221
+ raise InvalidCausalResultError(
222
+ "Ergodic conversion changed native variables.",
223
+ code="invalid_causal_result",
224
+ )
225
+ return graph
226
+
227
+
228
+ def validated_scores(result, payload):
229
+ """Bounded original-axis scores; never decode or threshold another graph."""
230
+ import numpy as np
231
+
232
+ try:
233
+ if (
234
+ not isinstance(payload, dict)
235
+ or payload["result_id"] != result.id
236
+ or payload["score_reference"] != result.score_reference
237
+ ):
238
+ raise ValueError("Scores are not bound to this result.")
239
+ if (
240
+ tuple(payload["variables"]) != result.variables
241
+ or payload["score_axes"] != "source_row_target_column"
242
+ or payload["score_semantics"] != "native_directed_edge_probability"
243
+ ):
244
+ raise ValueError("Score semantics/order differ from the native result.")
245
+ values = payload["values"]
246
+ if any(
247
+ isinstance(value, bool) or not isinstance(value, (int, float))
248
+ for row in values
249
+ for value in row
250
+ ):
251
+ raise ValueError("Scores must be numeric.")
252
+ array = np.asarray(values, dtype=float)
253
+ if (
254
+ array.shape != (len(result.variables), len(result.variables))
255
+ or not np.isfinite(array).all()
256
+ or not ((array >= 0) & (array <= 1)).all()
257
+ or not (array.diagonal() == 0).all()
258
+ ):
259
+ raise ValueError(
260
+ "Scores require native square finite probability semantics."
261
+ )
262
+ except (KeyError, TypeError, ValueError, OverflowError):
263
+ raise InvalidCausalResultError(
264
+ "Invalid result-bound causal scores; no transpose or repair was applied.",
265
+ code="invalid_causal_result",
266
+ ) from None
267
+ return deepcopy(payload)
268
+
269
+
270
+ class CausalDiscovery(BaseEstimator):
271
+ """Dataset-based discovery facade with reusable sklearn-style parameters."""
272
+
273
+ def __init__(
274
+ self,
275
+ model="arrow",
276
+ configuration="default",
277
+ base_url=None,
278
+ api_key=None,
279
+ random_state=0,
280
+ timeout=600,
281
+ ):
282
+ self.model, self.configuration, self.base_url = model, configuration, base_url
283
+ self.api_key = credential(api_key)
284
+ self.random_state, self.timeout = random_state, timeout
285
+
286
+ def _client(self):
287
+ from .client import Client
288
+
289
+ return Client(base_url=self.base_url, api_key=self.api_key)
290
+
291
+ def set_params(self, **params):
292
+ if "api_key" in params:
293
+ params["api_key"] = credential(params["api_key"])
294
+ return super().set_params(**params)
295
+
296
+ def submit_discover(
297
+ self,
298
+ X=None,
299
+ *,
300
+ dataset_id=None,
301
+ constraints=None,
302
+ model_version=None,
303
+ idempotency_key=None,
304
+ ):
305
+ if (X is None) == (dataset_id is None):
306
+ raise ValueError("Supply exactly one observational table or dataset_id.")
307
+ if (
308
+ type(self.random_state) is not int
309
+ or not 0 <= self.random_state <= 2**32 - 1
310
+ ):
311
+ raise ValueError("random_state must be an unsigned32-bit integer.")
312
+ client = self._client()
313
+ try:
314
+ if dataset_id is None:
315
+ from .estimators import table
316
+
317
+ columns, rows, _ = table(X)
318
+ dataset_id = client.upload(columns=columns, rows=rows, target=None)[
319
+ "id"
320
+ ]
321
+ return client.submit_discover(
322
+ dataset_id,
323
+ model=self.model,
324
+ configuration=self.configuration,
325
+ seed=self.random_state,
326
+ constraints=constraints,
327
+ model_version=model_version,
328
+ idempotency_key=idempotency_key,
329
+ )
330
+ except BaseException:
331
+ client.close()
332
+ raise
333
+
334
+ def discover(
335
+ self,
336
+ X=None,
337
+ *,
338
+ dataset_id=None,
339
+ constraints=None,
340
+ model_version=None,
341
+ idempotency_key=None,
342
+ ):
343
+ job = self.submit_discover(
344
+ X,
345
+ dataset_id=dataset_id,
346
+ constraints=constraints,
347
+ model_version=model_version,
348
+ idempotency_key=idempotency_key,
349
+ )
350
+ try:
351
+ return job.result(timeout=self.timeout)
352
+ finally:
353
+ job.client.close()