structured-data-models-client 1.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.
- sdm_client/__init__.py +70 -0
- sdm_client/_version.py +5 -0
- sdm_client/adapters/__init__.py +9 -0
- sdm_client/adapters/relational.py +870 -0
- sdm_client/adapters/tabular.py +580 -0
- sdm_client/base.py +300 -0
- sdm_client/client.py +1416 -0
- sdm_client/core/__init__.py +5 -0
- sdm_client/core/databricks_serving.py +611 -0
- sdm_client/core/databricks_user_agent.py +76 -0
- sdm_client/core/dates.py +278 -0
- sdm_client/core/transport.py +898 -0
- sdm_client/core/warnings.py +51 -0
- sdm_client/decompose.py +421 -0
- sdm_client/errors.py +177 -0
- sdm_client/materializers/__init__.py +10 -0
- sdm_client/materializers/relational.py +1786 -0
- sdm_client/materializers/sources.py +331 -0
- sdm_client/materializers/tabular.py +339 -0
- sdm_client/models.py +251 -0
- sdm_client/py.typed +0 -0
- sdm_client/query.py +647 -0
- sdm_client/request.py +1127 -0
- sdm_client/result.py +328 -0
- sdm_client/schema.py +3187 -0
- sdm_client/wire/__init__.py +46 -0
- sdm_client/wire/answers.py +80 -0
- sdm_client/wire/dtypes.py +701 -0
- sdm_client/wire/keys.py +363 -0
- sdm_client/wire/tables.py +50 -0
- structured_data_models_client-1.0.0.dist-info/METADATA +99 -0
- structured_data_models_client-1.0.0.dist-info/RECORD +36 -0
- structured_data_models_client-1.0.0.dist-info/WHEEL +5 -0
- structured_data_models_client-1.0.0.dist-info/licenses/LICENSE +204 -0
- structured_data_models_client-1.0.0.dist-info/licenses/NOTICE +10 -0
- structured_data_models_client-1.0.0.dist-info/top_level.txt +1 -0
sdm_client/base.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
r"""What a model is, to this client.
|
|
6
|
+
|
|
7
|
+
A model is described by two things that are deliberately kept apart.
|
|
8
|
+
|
|
9
|
+
A **profile** says what the model may be *asked*. It comes from
|
|
10
|
+
``predictive_query`` and is data, not code: a frozen set of flags obeying one
|
|
11
|
+
normative rule: a capability may only reject. The language accepts a fixed
|
|
12
|
+
set of queries and every model accepts a subset of it, so declaring a model can
|
|
13
|
+
never widen what the language allows.
|
|
14
|
+
|
|
15
|
+
A **runtime contract** says what this *deployment* can do: which task kinds and
|
|
16
|
+
output fields it serves, whether it holds sessions, and the limits it enforces.
|
|
17
|
+
A profile cannot know whether a checkpoint for regression was packaged, so
|
|
18
|
+
those facts live beside it and are never duplicated into it.
|
|
19
|
+
|
|
20
|
+
An **adapter** does what neither can describe: shape the wire request,
|
|
21
|
+
normalize the response, choose a session strategy, release resources. It does
|
|
22
|
+
not own query-shape validation. The profile does.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from collections.abc import Iterable, Mapping
|
|
28
|
+
from dataclasses import dataclass, field, fields
|
|
29
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
30
|
+
|
|
31
|
+
from predictive_query import Capabilities, Profile
|
|
32
|
+
|
|
33
|
+
from sdm_client.errors import UnsupportedRequestError
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING: # pragma: no cover - imported for typing only.
|
|
36
|
+
from sdm_client.request import Request
|
|
37
|
+
from sdm_client.result import PredictionResult
|
|
38
|
+
|
|
39
|
+
# The capability flags that describe the *shape* of a request, as opposed to
|
|
40
|
+
# the deployment concerns in the same dataclass. Only these are derivable from
|
|
41
|
+
# a request, so only these are compared.
|
|
42
|
+
SHAPE_CAPABILITIES = (
|
|
43
|
+
'relationships',
|
|
44
|
+
'multiple_tables',
|
|
45
|
+
'link_prediction',
|
|
46
|
+
'forecasting',
|
|
47
|
+
'static',
|
|
48
|
+
'temporal',
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class RuntimeContract:
|
|
54
|
+
r"""What a deployment of the model serves.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
tasks: The task kinds the endpoint accepts. Narrower than the
|
|
58
|
+
contract's enum: a tabular model takes only the coarse
|
|
59
|
+
``'classification'``.
|
|
60
|
+
outputs: The output fields the model produces.
|
|
61
|
+
inference_options: Inference-config keys the endpoint accepts.
|
|
62
|
+
explanation_formats: Named explanation shapes, when it explains.
|
|
63
|
+
sessions: Whether the endpoint holds a pinned context.
|
|
64
|
+
limits: Sizes the endpoint enforces, so the client can refuse locally
|
|
65
|
+
rather than eat a 500.
|
|
66
|
+
unread_feature_stypes: Semantic types the endpoint accepts and the
|
|
67
|
+
model does not read. A column carrying one is answered around
|
|
68
|
+
rather than refused, so the caller is told it carried nothing.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
tasks: frozenset[str]
|
|
72
|
+
outputs: frozenset[str]
|
|
73
|
+
explanation_formats: tuple[str, ...] = ()
|
|
74
|
+
sessions: bool = True
|
|
75
|
+
limits: Mapping[str, int] = field(default_factory=dict)
|
|
76
|
+
#: Inference-config keys this deployment accepts. Empty means it takes no
|
|
77
|
+
#: inference config at all. A driver built for a different backend may
|
|
78
|
+
#: default to sending more than this, which the endpoint refuses.
|
|
79
|
+
inference_options: frozenset[str] = frozenset()
|
|
80
|
+
#: Semantic types this deployment takes and the model never reads. The
|
|
81
|
+
#: request succeeds and the column contributes nothing, which is worth
|
|
82
|
+
#: saying out loud.
|
|
83
|
+
unread_feature_stypes: frozenset[str] = frozenset()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ModelSpec:
|
|
88
|
+
r"""A model id, what it may be asked, and what its deployment serves."""
|
|
89
|
+
|
|
90
|
+
model_id: str
|
|
91
|
+
profile: Profile
|
|
92
|
+
runtime: RuntimeContract
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class ModelCapabilities:
|
|
97
|
+
r"""What :meth:`~sdm_client.SDMClient.capabilities` reports.
|
|
98
|
+
|
|
99
|
+
Describes the client side. Nothing here is read from the endpoint, and
|
|
100
|
+
``source`` says so, so a caller cannot mistake it for the endpoint's own
|
|
101
|
+
answer.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
model: str
|
|
105
|
+
tasks: tuple[str, ...]
|
|
106
|
+
outputs: tuple[str, ...]
|
|
107
|
+
single_table: bool
|
|
108
|
+
multiple_tables: bool
|
|
109
|
+
relationships: bool
|
|
110
|
+
explanation_formats: tuple[str, ...] = ()
|
|
111
|
+
sessions: bool = True
|
|
112
|
+
source: str = 'sdk'
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ModelAdapter(Protocol):
|
|
116
|
+
r"""The per-model half of a prediction.
|
|
117
|
+
|
|
118
|
+
Deliberately small. Everything a profile or a runtime contract can already
|
|
119
|
+
state is left to them; what remains is the work that genuinely differs
|
|
120
|
+
between models.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
spec: ModelSpec
|
|
124
|
+
|
|
125
|
+
def default_output(self, request: Request) -> tuple[str, ...]:
|
|
126
|
+
r"""The output fields to ask for when the caller named none."""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
def prediction_plan(
|
|
130
|
+
self, request: Request, outputs: tuple[str, ...]
|
|
131
|
+
) -> Iterable[dict[str, Any]]:
|
|
132
|
+
r"""The wire payloads for one request, in the order to send them.
|
|
133
|
+
|
|
134
|
+
More than one when the request exceeds the endpoint's per-call cap.
|
|
135
|
+
Batching lives here because it is transport, not part of the question.
|
|
136
|
+
"""
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
def session_plan(
|
|
140
|
+
self, request: Request, outputs: tuple[str, ...]
|
|
141
|
+
) -> dict[str, Any]:
|
|
142
|
+
r"""The payload that pins this request's context on the endpoint."""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
def session_prediction_plan(
|
|
146
|
+
self,
|
|
147
|
+
request: Request,
|
|
148
|
+
outputs: tuple[str, ...],
|
|
149
|
+
pinned: Mapping[str, Any] | None = None,
|
|
150
|
+
) -> Iterable[dict[str, Any]]:
|
|
151
|
+
r"""The payloads that score against an already-pinned context.
|
|
152
|
+
|
|
153
|
+
``pinned`` is the envelope the session opened with, minus the context
|
|
154
|
+
itself. The schema is not sent again on a scoring call, so the rows
|
|
155
|
+
have to be written to describe the one that was pinned.
|
|
156
|
+
"""
|
|
157
|
+
...
|
|
158
|
+
|
|
159
|
+
def normalize(
|
|
160
|
+
self,
|
|
161
|
+
response: dict[str, Any],
|
|
162
|
+
outputs: tuple[str, ...],
|
|
163
|
+
payload: dict[str, Any],
|
|
164
|
+
) -> PredictionResult:
|
|
165
|
+
r"""Turn one endpoint response into the client's result type.
|
|
166
|
+
|
|
167
|
+
``payload`` is the request that produced it. A batch's answers are
|
|
168
|
+
indexed within that batch, so mapping them back to what the caller
|
|
169
|
+
asked about needs the request beside the response.
|
|
170
|
+
"""
|
|
171
|
+
...
|
|
172
|
+
|
|
173
|
+
def explain_error(
|
|
174
|
+
self, error: Exception, payload: dict[str, Any]
|
|
175
|
+
) -> Exception:
|
|
176
|
+
r"""A chance to add what the endpoint's message left out.
|
|
177
|
+
|
|
178
|
+
A NIM sometimes reports a rule without saying which column broke it.
|
|
179
|
+
The request that was just refused knows, and the adapter is holding it,
|
|
180
|
+
so the answer is one lookup away. Returns the error to raise, the one
|
|
181
|
+
it was given, unless it has something to add.
|
|
182
|
+
"""
|
|
183
|
+
return error
|
|
184
|
+
|
|
185
|
+
def close(self) -> None:
|
|
186
|
+
r"""Release anything this adapter opened that the client cannot see."""
|
|
187
|
+
...
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def required_capabilities(request: Request) -> Capabilities:
|
|
191
|
+
r"""What answering ``request`` would require of a model.
|
|
192
|
+
|
|
193
|
+
Built with :meth:`Capabilities.nothing` and switched on, so a flag this
|
|
194
|
+
request does not need stays off and cannot cause a spurious rejection.
|
|
195
|
+
"""
|
|
196
|
+
kind = request.task.kind
|
|
197
|
+
temporal = bool(request.task.options.get('anchor_time_column')) or kind in {
|
|
198
|
+
'forecasting',
|
|
199
|
+
'temporal_link_prediction',
|
|
200
|
+
}
|
|
201
|
+
return Capabilities.nothing('this request').also(
|
|
202
|
+
relationships=bool(request.schema.relationships),
|
|
203
|
+
multiple_tables=len(request.schema) > 1,
|
|
204
|
+
link_prediction=kind == 'temporal_link_prediction',
|
|
205
|
+
forecasting=kind == 'forecasting',
|
|
206
|
+
temporal=temporal,
|
|
207
|
+
static=not temporal,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def check_request_against_model(
|
|
212
|
+
request: Request, spec: ModelSpec, outputs: tuple[str, ...]
|
|
213
|
+
) -> None:
|
|
214
|
+
r"""Refuse, locally, a request the model cannot answer.
|
|
215
|
+
|
|
216
|
+
Three checks, in the order that produces the most useful message: the
|
|
217
|
+
shape the profile rejects, then the task kind, then the output fields.
|
|
218
|
+
A shape mismatch explains the other two, so it is reported first.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
UnsupportedRequestError: With the model named, and what it does
|
|
222
|
+
support.
|
|
223
|
+
"""
|
|
224
|
+
required = required_capabilities(request)
|
|
225
|
+
supported = spec.profile.capabilities
|
|
226
|
+
unsupported = [
|
|
227
|
+
name
|
|
228
|
+
for name in SHAPE_CAPABILITIES
|
|
229
|
+
if getattr(required, name) and not getattr(supported, name)
|
|
230
|
+
]
|
|
231
|
+
if unsupported:
|
|
232
|
+
raise UnsupportedRequestError(
|
|
233
|
+
f'{spec.model_id} cannot answer a request needing '
|
|
234
|
+
f'{", ".join(sorted(unsupported))}',
|
|
235
|
+
details={
|
|
236
|
+
'model': spec.model_id,
|
|
237
|
+
'unsupported': sorted(unsupported),
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if request.task.kind not in spec.runtime.tasks:
|
|
242
|
+
raise UnsupportedRequestError(
|
|
243
|
+
f'{spec.model_id} does not serve the task kind '
|
|
244
|
+
f'{request.task.kind!r}; it serves '
|
|
245
|
+
f'{sorted(spec.runtime.tasks)}',
|
|
246
|
+
details={
|
|
247
|
+
'model': spec.model_id,
|
|
248
|
+
'task': request.task.kind,
|
|
249
|
+
'supported': sorted(spec.runtime.tasks),
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
unknown = [name for name in outputs if name not in spec.runtime.outputs]
|
|
254
|
+
if unknown:
|
|
255
|
+
raise UnsupportedRequestError(
|
|
256
|
+
f'{spec.model_id} does not produce {sorted(unknown)}; it produces '
|
|
257
|
+
f'{sorted(spec.runtime.outputs)}',
|
|
258
|
+
details={
|
|
259
|
+
'model': spec.model_id,
|
|
260
|
+
'unsupported_outputs': sorted(unknown),
|
|
261
|
+
'supported_outputs': sorted(spec.runtime.outputs),
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
cap = spec.runtime.limits.get('context_rows_max')
|
|
266
|
+
rows = request._context_rows_per_call()
|
|
267
|
+
if cap is not None and rows > cap:
|
|
268
|
+
raise UnsupportedRequestError(
|
|
269
|
+
f'this request carries {rows:,} context rows and {spec.model_id} '
|
|
270
|
+
f'reads at most {cap:,}. A request built directly is sent as it '
|
|
271
|
+
f'was given, so choose the rows you want as context, or gather it '
|
|
272
|
+
f'with materialize(), which keeps the context under the cap and '
|
|
273
|
+
f'says how',
|
|
274
|
+
details={
|
|
275
|
+
'model': spec.model_id,
|
|
276
|
+
'context_rows': rows,
|
|
277
|
+
'context_rows_max': cap,
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def describe(spec: ModelSpec) -> ModelCapabilities:
|
|
283
|
+
r"""The public description of a model, for ``capabilities()``."""
|
|
284
|
+
supported = spec.profile.capabilities
|
|
285
|
+
return ModelCapabilities(
|
|
286
|
+
model=spec.model_id,
|
|
287
|
+
tasks=tuple(sorted(spec.runtime.tasks)),
|
|
288
|
+
outputs=tuple(sorted(spec.runtime.outputs)),
|
|
289
|
+
single_table=True,
|
|
290
|
+
multiple_tables=supported.multiple_tables,
|
|
291
|
+
relationships=supported.relationships,
|
|
292
|
+
explanation_formats=spec.runtime.explanation_formats,
|
|
293
|
+
sessions=spec.runtime.sessions,
|
|
294
|
+
source='sdk',
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def capability_names() -> tuple[str, ...]:
|
|
299
|
+
r"""Every flag ``Capabilities`` carries, for tests that assert coverage."""
|
|
300
|
+
return tuple(f.name for f in fields(Capabilities) if f.name != 'model')
|