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/models.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
r"""The models this client can send to.
|
|
6
|
+
|
|
7
|
+
Pass one as an argument rather than calling a differently-named method:
|
|
8
|
+
|
|
9
|
+
>>> client.predict(request, model=KumoTabular())
|
|
10
|
+
>>> client.predict(request, model=KumoRelational())
|
|
11
|
+
|
|
12
|
+
Each is a thin handle over a :class:`~sdm_client.base.ModelSpec`. A handle
|
|
13
|
+
carries no state and no configuration. Gathering knobs live on the request and
|
|
14
|
+
output fields on the call, so constructing one is free and two of the same
|
|
15
|
+
kind are interchangeable.
|
|
16
|
+
|
|
17
|
+
The declarations below mirror what deployed endpoints actually answer,
|
|
18
|
+
rather than what any design document says they should.
|
|
19
|
+
|
|
20
|
+
**A runtime contract describes a deployment, and one model id may have more
|
|
21
|
+
than one.** The relational declaration below is the broader of those in
|
|
22
|
+
service: six task kinds and all seven output fields. A narrower build of the
|
|
23
|
+
same id serves only classification and regression, and produces only
|
|
24
|
+
``prediction``, ``probabilities`` and ``quantiles``. Asking that one for a
|
|
25
|
+
forecast passes the checks here and is refused by the endpoint.
|
|
26
|
+
|
|
27
|
+
Reading this from the endpoint instead would settle it, but the Universal TFM
|
|
28
|
+
contract has no capabilities route and ``/v1/models`` returns an id and nothing
|
|
29
|
+
else. So this is the broader of the two on purpose: refusing locally what one
|
|
30
|
+
deployment can answer would be worse than letting the other say no.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from predictive_query import RELATIONAL, TABULAR
|
|
36
|
+
from sdm_api import TFM_MODEL_KUMO_RELATIONAL, TFM_MODEL_KUMO_TABULAR
|
|
37
|
+
|
|
38
|
+
from sdm_client.base import ModelSpec, RuntimeContract
|
|
39
|
+
from sdm_client.errors import StructuredDataError, UnknownModelError
|
|
40
|
+
|
|
41
|
+
# Sizes the relational endpoint enforces. `predict_rows_max` is the serving
|
|
42
|
+
# layer's own `TFM_MAX_TEST_ROWS`, measured at the boundary rather than read
|
|
43
|
+
# from a document: 10,000 rows to score are answered, and 10,001 come back
|
|
44
|
+
#
|
|
45
|
+
# [422 UNSUPPORTED_SHAPE] N_test 10001 exceeds limit 10000.
|
|
46
|
+
#
|
|
47
|
+
# A deployment may be configured to hold to less, which is what the
|
|
48
|
+
# `rows_per_call` option on materialize() is for.
|
|
49
|
+
#
|
|
50
|
+
# The endpoint refuses past these, so the client refuses first with a message
|
|
51
|
+
# that names the limit. See the architecture note on inference knobs:
|
|
52
|
+
# `run_mode` is derived from these rather than being set.
|
|
53
|
+
#
|
|
54
|
+
# `predict_rows_max` and `predict_rows_temporal_link_prediction` restate the
|
|
55
|
+
# engine's own `_MAX_PRED_SIZE`, which defaults to the first and holds the
|
|
56
|
+
# second for `TaskType.TEMPORAL_LINK_PREDICTION`
|
|
57
|
+
# (sdm_relational_engine/rfm/rfm.py). The duplication predates this client,
|
|
58
|
+
# and it is load-bearing: this value is what the client asks the engine to
|
|
59
|
+
# batch at, so a drift would gather batches the endpoint refuses, or ask for
|
|
60
|
+
# smaller ones than it would take, and the engine's own refusal names a
|
|
61
|
+
# `batch_size=` argument this client's callers have not got. Change them
|
|
62
|
+
# together; `test_the_two_ceilings_are_the_engine_s_own` fails if they move
|
|
63
|
+
# apart.
|
|
64
|
+
RELATIONAL_LIMITS = {
|
|
65
|
+
'context_rows_fast': 1_000,
|
|
66
|
+
'context_rows_max': 10_000,
|
|
67
|
+
'predict_rows_max': 10_000,
|
|
68
|
+
'predict_rows_temporal_link_prediction': 200,
|
|
69
|
+
'explanation_predict_rows': 1,
|
|
70
|
+
'hops_max': 6,
|
|
71
|
+
'tables_max': 15,
|
|
72
|
+
'classes_max': 10,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# A relational regression is answered from a predicted distribution, and the
|
|
76
|
+
# endpoint reports its mean unless told otherwise; the engine reports the
|
|
77
|
+
# median, so that is asked for by default.
|
|
78
|
+
DEFAULT_PREDICTION_STATISTIC = 'median'
|
|
79
|
+
PREDICTION_STATISTICS = frozenset({'mean', 'median'})
|
|
80
|
+
SUMMARISED_TASK_KINDS = frozenset({'regression', 'forecasting'})
|
|
81
|
+
|
|
82
|
+
# The tabular endpoint refuses a context past this, exactly:
|
|
83
|
+
#
|
|
84
|
+
# [422 UNSUPPORTED_SHAPE] N_train 10001 exceeds limit 10000.
|
|
85
|
+
#
|
|
86
|
+
# Measured against the deployment rather than read from a document, at the
|
|
87
|
+
# boundary: 10,000 rows answer, 10,001 refuse. Declared so the client refuses
|
|
88
|
+
# first, because the alternative is encoding and transmitting an arbitrarily
|
|
89
|
+
# large context to be told the limit at the far end.
|
|
90
|
+
TABULAR_LIMITS = {
|
|
91
|
+
'context_rows_max': 10_000,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
KUMO_TABULAR = ModelSpec(
|
|
95
|
+
model_id=TFM_MODEL_KUMO_TABULAR,
|
|
96
|
+
profile=TABULAR,
|
|
97
|
+
runtime=RuntimeContract(
|
|
98
|
+
# The endpoint takes the coarse kind only and decides binary versus
|
|
99
|
+
# multiclass itself, so this client never narrows it here.
|
|
100
|
+
tasks=frozenset({'classification', 'regression'}),
|
|
101
|
+
outputs=frozenset({'prediction', 'probabilities', 'quantiles'}),
|
|
102
|
+
limits=TABULAR_LIMITS,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
KUMO_RELATIONAL = ModelSpec(
|
|
107
|
+
model_id=TFM_MODEL_KUMO_RELATIONAL,
|
|
108
|
+
profile=RELATIONAL,
|
|
109
|
+
runtime=RuntimeContract(
|
|
110
|
+
tasks=frozenset(
|
|
111
|
+
{
|
|
112
|
+
'classification',
|
|
113
|
+
'binary_classification',
|
|
114
|
+
'multiclass_classification',
|
|
115
|
+
'regression',
|
|
116
|
+
'forecasting',
|
|
117
|
+
'temporal_link_prediction',
|
|
118
|
+
}
|
|
119
|
+
),
|
|
120
|
+
outputs=frozenset(
|
|
121
|
+
{
|
|
122
|
+
'prediction',
|
|
123
|
+
'probabilities',
|
|
124
|
+
'scores',
|
|
125
|
+
'rankings',
|
|
126
|
+
'embeddings',
|
|
127
|
+
'quantiles',
|
|
128
|
+
'explanation',
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
# Empty because nothing here takes a format argument: the contract's
|
|
132
|
+
# `OutputSpec` is only `{fields: [...]}`, and the endpoint names the
|
|
133
|
+
# shape it returns. Two names declared here appeared in no response.
|
|
134
|
+
explanation_formats=(),
|
|
135
|
+
limits=RELATIONAL_LIMITS,
|
|
136
|
+
unread_feature_stypes=frozenset({'text'}),
|
|
137
|
+
# Inference-config keys this client forwards, rather than keys the
|
|
138
|
+
# deployment is known to serve. The engine also sends `output_type`,
|
|
139
|
+
# whose choice travels as `output.prediction_statistic` instead, and
|
|
140
|
+
# `target_transforms`, which does nothing on this model: the endpoint
|
|
141
|
+
# accepts only `quantile`, reads it nowhere, and says the model emits
|
|
142
|
+
# its quantiles natively. A deployment may still refuse a value given
|
|
143
|
+
# for a key that is forwarded, and the shipping one refuses every
|
|
144
|
+
# shuffle and every `num_estimators` but its own, so this list is not
|
|
145
|
+
# a menu.
|
|
146
|
+
inference_options=frozenset(
|
|
147
|
+
{
|
|
148
|
+
'kind',
|
|
149
|
+
'num_estimators',
|
|
150
|
+
'column_shuffle',
|
|
151
|
+
'category_shuffle',
|
|
152
|
+
'hop_shuffle',
|
|
153
|
+
'class_shuffle',
|
|
154
|
+
}
|
|
155
|
+
),
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Model:
|
|
161
|
+
r"""Base for the model handles. Not useful on its own."""
|
|
162
|
+
|
|
163
|
+
_spec: ModelSpec
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def name(self) -> str:
|
|
167
|
+
r"""The model id sent on the wire."""
|
|
168
|
+
return self._spec.model_id
|
|
169
|
+
|
|
170
|
+
def __eq__(self, other: object) -> bool:
|
|
171
|
+
return isinstance(other, Model) and other._spec is self._spec
|
|
172
|
+
|
|
173
|
+
def __hash__(self) -> int:
|
|
174
|
+
return hash(self._spec.model_id)
|
|
175
|
+
|
|
176
|
+
def __repr__(self) -> str:
|
|
177
|
+
return f'{type(self).__name__}()'
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class KumoTabular(Model):
|
|
181
|
+
r"""One table in, a prediction per row out.
|
|
182
|
+
|
|
183
|
+
Takes ``classification`` or ``regression``, and produces ``prediction``,
|
|
184
|
+
``probabilities`` and ``quantiles``. It reads no relationships, so a
|
|
185
|
+
request carrying related tables is refused before it is sent.
|
|
186
|
+
|
|
187
|
+
Needs no extra: this model does no client-side work beyond shaping the
|
|
188
|
+
request, so it ships in the base install and runs anywhere.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
_spec = KUMO_TABULAR
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class KumoRelational(Model):
|
|
195
|
+
r"""Several related tables, joined as a graph.
|
|
196
|
+
|
|
197
|
+
Takes every task kind the contract carries except ``ranking``, and produces
|
|
198
|
+
all seven output fields including ``explanation``.
|
|
199
|
+
|
|
200
|
+
Needs ``pip install structured-data-models-client[relational]``: building the graph and
|
|
201
|
+
sampling it is heavy client-side work that lives in a native package.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
_spec = KUMO_RELATIONAL
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
#: Every model this client can send to, by id.
|
|
208
|
+
MODELS = {
|
|
209
|
+
KUMO_TABULAR.model_id: KUMO_TABULAR,
|
|
210
|
+
KUMO_RELATIONAL.model_id: KUMO_RELATIONAL,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def spec_for(model: str | Model) -> ModelSpec:
|
|
215
|
+
r"""The specification behind a model handle or a model id.
|
|
216
|
+
|
|
217
|
+
Shared by the two places a model is named, gathering and sending a request,
|
|
218
|
+
so both accept the same arguments and refuse the same ones the same way.
|
|
219
|
+
|
|
220
|
+
A handle names one of the models below, not a new one. It is worth exactly
|
|
221
|
+
the spec it carries. That spec chooses the route a request is gathered for
|
|
222
|
+
and the limits it is checked against. The id
|
|
223
|
+
it names is a closed enum on the wire. One this module did not declare
|
|
224
|
+
is refused here rather than gathering work that cannot be sent. ``Model``
|
|
225
|
+
itself declares ``_spec`` without defining one, so a bare ``Model()`` gets
|
|
226
|
+
that same refusal rather than an ``AttributeError``.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
model: A model handle such as ``KumoTabular()``, or a model id.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
UnknownModelError: If ``model`` is an id or handle the contract does
|
|
233
|
+
not carry.
|
|
234
|
+
StructuredDataError: If ``model`` is neither a handle nor an id.
|
|
235
|
+
"""
|
|
236
|
+
if isinstance(model, Model):
|
|
237
|
+
spec = getattr(model, '_spec', None)
|
|
238
|
+
if isinstance(spec, ModelSpec) and MODELS.get(spec.model_id) is spec:
|
|
239
|
+
return spec
|
|
240
|
+
named = getattr(spec, 'model_id', None) or type(model).__name__
|
|
241
|
+
raise UnknownModelError(named, list(MODELS))
|
|
242
|
+
if isinstance(model, str):
|
|
243
|
+
spec = MODELS.get(model)
|
|
244
|
+
if spec is None:
|
|
245
|
+
raise UnknownModelError(model, list(MODELS))
|
|
246
|
+
return spec
|
|
247
|
+
raise StructuredDataError(
|
|
248
|
+
f'model must be a model handle or a model id, got '
|
|
249
|
+
f'{type(model).__name__}',
|
|
250
|
+
code='INVALID_REQUEST',
|
|
251
|
+
)
|
sdm_client/py.typed
ADDED
|
File without changes
|