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/wire/keys.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
r"""Which rows an encoded request is about.
|
|
6
|
+
|
|
7
|
+
The keys are in the envelope rather than in anything the caller still holds,
|
|
8
|
+
and the two models write them differently: a relational envelope carries an
|
|
9
|
+
opaque entity reference, a tabular one the caller's own key column. Reading
|
|
10
|
+
that is a question about the wire, so it is answered here rather than by the
|
|
11
|
+
client, which should have to ask only whether two sets of rows overlap.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import itertools
|
|
17
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
18
|
+
from decimal import Decimal
|
|
19
|
+
from numbers import Real
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
import pandas as pd
|
|
23
|
+
|
|
24
|
+
from sdm_client.schema import _as_key_tuple
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from sdm_client.request import Request
|
|
28
|
+
|
|
29
|
+
#: What a relational envelope calls the moment, when nothing of the caller's
|
|
30
|
+
#: is called that already.
|
|
31
|
+
_ANCHOR = '__kumo_anchor_time'
|
|
32
|
+
|
|
33
|
+
#: How a relational envelope names the entity a row is about.
|
|
34
|
+
_ENTITY_REF = '__kumo_entity_ref'
|
|
35
|
+
|
|
36
|
+
#: What ties a related row to the instance it hangs off.
|
|
37
|
+
INSTANCE_ID = 'instance_id'
|
|
38
|
+
|
|
39
|
+
#: Counts the rows that name no entity, so that no two of them are one.
|
|
40
|
+
_UNIDENTIFIED = itertools.count()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _anchor_column(payload: dict[str, Any] | None) -> str:
|
|
44
|
+
r"""What this envelope calls the moment.
|
|
45
|
+
|
|
46
|
+
Read from the task rather than assumed. The engine suffixes the name when
|
|
47
|
+
a column of the caller's own is called that, so looking only for the bare
|
|
48
|
+
name goes blind on exactly the graphs that have one.
|
|
49
|
+
"""
|
|
50
|
+
task = (payload or {}).get('task')
|
|
51
|
+
named = (
|
|
52
|
+
task.get('anchor_time_column') if isinstance(task, Mapping) else None
|
|
53
|
+
)
|
|
54
|
+
return str(named) if named else _ANCHOR
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def rows_in(
|
|
58
|
+
payload: dict[str, Any] | None, half: str, *, with_anchor: bool = False
|
|
59
|
+
) -> list[Any]:
|
|
60
|
+
r"""The key of every row one half of a wire envelope holds, one per row.
|
|
61
|
+
|
|
62
|
+
The row-wise reading. :func:`keys_in` is this without the duplicates, and
|
|
63
|
+
is what a question about overlap wants; a sentence that counts rows wants
|
|
64
|
+
this, because two rows about one entity are two rows.
|
|
65
|
+
"""
|
|
66
|
+
if not isinstance(payload, dict):
|
|
67
|
+
return []
|
|
68
|
+
table = (payload.get(half) or {}).get('instance_table') or {}
|
|
69
|
+
columns = table.get('columns')
|
|
70
|
+
rows = table.get('rows')
|
|
71
|
+
if not isinstance(columns, list) or not isinstance(rows, list):
|
|
72
|
+
return []
|
|
73
|
+
named: Any = next(
|
|
74
|
+
(name for name in columns if str(name).startswith(_ENTITY_REF)),
|
|
75
|
+
None,
|
|
76
|
+
)
|
|
77
|
+
if named is None:
|
|
78
|
+
named = ((payload.get('schema') or {}).get('instance_table') or {}).get(
|
|
79
|
+
'primary_key'
|
|
80
|
+
)
|
|
81
|
+
if named is None:
|
|
82
|
+
return []
|
|
83
|
+
wanted = named if isinstance(named, list) else [named]
|
|
84
|
+
if any(name not in columns for name in wanted):
|
|
85
|
+
return []
|
|
86
|
+
at = [columns.index(name) for name in wanted]
|
|
87
|
+
if with_anchor:
|
|
88
|
+
anchor = _anchor_column(payload)
|
|
89
|
+
if anchor in columns:
|
|
90
|
+
at = [*at, columns.index(anchor)]
|
|
91
|
+
return [
|
|
92
|
+
_named_by(row[position] for position in at)
|
|
93
|
+
for row in rows
|
|
94
|
+
if isinstance(row, list) and len(row) > max(at)
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def keys_in(
|
|
99
|
+
payload: dict[str, Any] | None, half: str, *, with_anchor: bool = False
|
|
100
|
+
) -> frozenset[Any]:
|
|
101
|
+
r"""The entity keys one half of a wire envelope holds.
|
|
102
|
+
|
|
103
|
+
Kept when a session is opened, and only the keys: the context itself is
|
|
104
|
+
deliberately not kept, since holding a second encoded copy of a whole
|
|
105
|
+
dataset for the life of the handle is what `pinned` exists to avoid. A set
|
|
106
|
+
of keys is small enough to keep and is the only thing needed to notice that
|
|
107
|
+
a later call scores rows the context already answers.
|
|
108
|
+
|
|
109
|
+
The key is named in the schema rather than in the rows, and the two models
|
|
110
|
+
name it differently: a relational envelope carries an opaque entity
|
|
111
|
+
reference, a tabular one carries the caller's own key column.
|
|
112
|
+
|
|
113
|
+
`with_anchor` puts the moment into the key. An entity asked about over a
|
|
114
|
+
window is a different question at a different moment, so the context has
|
|
115
|
+
only answered the one it was asked at; an entity whose answer is a
|
|
116
|
+
standing value has been given away at any moment at all.
|
|
117
|
+
"""
|
|
118
|
+
return frozenset(rows_in(payload, half, with_anchor=with_anchor))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _named_by(values: Iterable[Any]) -> str:
|
|
122
|
+
r"""One way of writing a key, whichever side of the wire it came from.
|
|
123
|
+
|
|
124
|
+
A pinned key is read from the encoded payload and a scored key from the
|
|
125
|
+
frame, and the two disagree about the same value: an integer past the
|
|
126
|
+
JS-safe range travels as a base-10 string by contract, so
|
|
127
|
+
`9007199254740993` on one side and `'9007199254740993'` on the other would
|
|
128
|
+
never meet, and the warning would go quiet on precisely the warehouse keys
|
|
129
|
+
it matters most for.
|
|
130
|
+
|
|
131
|
+
A whole number written as a float is the same entity as the integer: a
|
|
132
|
+
context typed `int64` pins entity `1` and a scored frame of floats sends
|
|
133
|
+
`1.0`, and the endpoint answers the second with the first's id. Comparing
|
|
134
|
+
the spellings called that two entities, and a session scored rows its
|
|
135
|
+
context answers without saying so.
|
|
136
|
+
|
|
137
|
+
A composite key becomes a tuple of its parts rather than nothing at all,
|
|
138
|
+
which is what reading only single-column keys amounted to.
|
|
139
|
+
"""
|
|
140
|
+
named = tuple(values)
|
|
141
|
+
if any(_is_missing(value) for value in named):
|
|
142
|
+
# A row with no identifier is not the same row as another with no
|
|
143
|
+
# identifier. Named alike, two unidentified rows were reported as one
|
|
144
|
+
# entity leaking into the other, and `holdout=[None]` took every
|
|
145
|
+
# unidentified row out of the context at once.
|
|
146
|
+
return f'<unidentified {next(_UNIDENTIFIED)}>'
|
|
147
|
+
return repr(tuple(_one_spelling(value) for value in named))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _is_missing(value: Any) -> bool:
|
|
151
|
+
r"""Whether this value names no entity at all."""
|
|
152
|
+
if value is None:
|
|
153
|
+
return True
|
|
154
|
+
try:
|
|
155
|
+
return bool(pd.isna(value))
|
|
156
|
+
except (ArithmeticError, TypeError, ValueError):
|
|
157
|
+
# A signalling NaN raises rather than comparing. It names no entity
|
|
158
|
+
# either, and asking whether it does must not be the thing that fails.
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _one_spelling(value: Any) -> str:
|
|
163
|
+
r"""A value as the name of an entity, not as the type that carried it.
|
|
164
|
+
|
|
165
|
+
Every real number is read as the number it is, rather than a list of the
|
|
166
|
+
types that can hold one: ``1``, ``1.0``, a numpy float of any width and
|
|
167
|
+
``Decimal("1.00")`` all name entity 1, and ``Decimal("1.50")`` names the
|
|
168
|
+
same entity as ``1.5``. Written by type instead, a holdout given as a
|
|
169
|
+
``float32`` removed nothing and a trailing zero made two names for one row.
|
|
170
|
+
|
|
171
|
+
Anything that is not a number is left as it is written. A key column of
|
|
172
|
+
text has ``"1.50"`` and ``"1.5"`` as two different entities, and reading
|
|
173
|
+
them as numbers would merge two rows the schema says are distinct.
|
|
174
|
+
"""
|
|
175
|
+
if isinstance(value, bool) or not isinstance(value, (Real, Decimal)):
|
|
176
|
+
return str(value)
|
|
177
|
+
try:
|
|
178
|
+
exact = Decimal(str(value))
|
|
179
|
+
# A name is not arithmetic. An infinity or a NaN is not an entity and
|
|
180
|
+
# cannot be read as one, and asking anyway raised `OverflowError` and
|
|
181
|
+
# `InvalidOperation` out of a function whose whole job is to write a
|
|
182
|
+
# name. Thus, a holdout naming one failed the call before it was sent.
|
|
183
|
+
if not exact.is_finite():
|
|
184
|
+
return str(value)
|
|
185
|
+
if exact == exact.to_integral_value():
|
|
186
|
+
return str(int(exact))
|
|
187
|
+
return str(exact.normalize())
|
|
188
|
+
except (ArithmeticError, ValueError):
|
|
189
|
+
return str(value)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def key_dtypes(
|
|
193
|
+
table: Any, key: Sequence[str], widened_by: Any = None
|
|
194
|
+
) -> dict[str, str] | None:
|
|
195
|
+
r"""One wire dtype per key column, settled the way the adapter settles it.
|
|
196
|
+
|
|
197
|
+
One per column, not one per half. Settled separately for each frame, the
|
|
198
|
+
same entity came out ``1.0`` on the side read from the context and ``1``
|
|
199
|
+
on the side read from the rows being scored, and the comparison between
|
|
200
|
+
them reported overlaps the encoded request does not have.
|
|
201
|
+
|
|
202
|
+
``None`` where no dtype can be settled. Every caller is a warning, and a
|
|
203
|
+
warning is never worth a failure.
|
|
204
|
+
"""
|
|
205
|
+
from sdm_client.adapters.tabular import _raw_dtype_of
|
|
206
|
+
from sdm_client.wire import UNTYPED, widen_tfm_dtype
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
dtypes = {}
|
|
210
|
+
for name in key:
|
|
211
|
+
column = table.columns[name]
|
|
212
|
+
dtype = UNTYPED if column.untyped else column.dtype
|
|
213
|
+
if widened_by is not None and name in widened_by.columns:
|
|
214
|
+
dtype = widen_tfm_dtype(
|
|
215
|
+
(dtype, _raw_dtype_of(widened_by[name], dtype))
|
|
216
|
+
)
|
|
217
|
+
dtypes[name] = dtype or 'string'
|
|
218
|
+
return dtypes
|
|
219
|
+
except Exception: # noqa: BLE001 - a warning is never worth a failure
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def keys_from_a_frame(
|
|
224
|
+
frame: Any, key: Sequence[str], dtypes: Mapping[str, str] | None
|
|
225
|
+
) -> list[Any]:
|
|
226
|
+
r"""``frame``'s keys, written the way the wire will write them.
|
|
227
|
+
|
|
228
|
+
The other side of every comparison here is read from an encoded envelope,
|
|
229
|
+
so a frame read raw does not meet it: an entity held as ``1.0`` in a frame
|
|
230
|
+
is ``1`` on the wire, and the two would never match. This is how a
|
|
231
|
+
session came to score rows its pinned context answers without saying so.
|
|
232
|
+
|
|
233
|
+
Through the encoder that writes the request, given the dtypes
|
|
234
|
+
:func:`key_dtypes` settled for the request as a whole. Empty where there
|
|
235
|
+
are none or the encoding fails: every caller is a warning, and a warning
|
|
236
|
+
is never worth a failure.
|
|
237
|
+
"""
|
|
238
|
+
from sdm_client.wire.tables import encode_table
|
|
239
|
+
|
|
240
|
+
if dtypes is None:
|
|
241
|
+
return []
|
|
242
|
+
try:
|
|
243
|
+
encoded = encode_table(
|
|
244
|
+
frame[list(key)], {name: dtypes[name] for name in key}
|
|
245
|
+
)
|
|
246
|
+
except Exception: # noqa: BLE001 - a warning is never worth a failure
|
|
247
|
+
return []
|
|
248
|
+
return [_named_by(row) for row in encoded['rows']]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def scored_rows(
|
|
252
|
+
request: Request,
|
|
253
|
+
*,
|
|
254
|
+
with_anchor: bool = False,
|
|
255
|
+
dtypes: Mapping[str, str] | None = None,
|
|
256
|
+
) -> list[Any]:
|
|
257
|
+
r"""The key of every row a request asks about, one per row.
|
|
258
|
+
|
|
259
|
+
A sentence that counts rows has to be given rows: two rows asking about
|
|
260
|
+
one entity are two rows, and a set of keys had already lost one of them.
|
|
261
|
+
|
|
262
|
+
``dtypes`` come from the other side of the comparison, namely a session's
|
|
263
|
+
pinned schema. Settled here instead, the same entity has
|
|
264
|
+
one name on each side and neither meets the other.
|
|
265
|
+
"""
|
|
266
|
+
if request.payloads:
|
|
267
|
+
return [
|
|
268
|
+
named
|
|
269
|
+
for payload in request.payloads
|
|
270
|
+
for named in rows_in(payload, 'predict', with_anchor=with_anchor)
|
|
271
|
+
]
|
|
272
|
+
frame = getattr(request.predict, 'instance_table', None)
|
|
273
|
+
table = request.schema[request.entity_table] if request.schema else None
|
|
274
|
+
names = _as_key_tuple(getattr(table, 'primary_key', None))
|
|
275
|
+
if frame is None or not names or any(n not in frame for n in names):
|
|
276
|
+
return []
|
|
277
|
+
if dtypes is None:
|
|
278
|
+
dtypes = key_dtypes(table, names, frame)
|
|
279
|
+
return keys_from_a_frame(frame, names, dtypes)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def scored_keys(
|
|
283
|
+
request: Request, *, with_anchor: bool = False
|
|
284
|
+
) -> frozenset[Any]:
|
|
285
|
+
r"""The keys a request is asking about.
|
|
286
|
+
|
|
287
|
+
Read from the envelopes where there are any, and from the frame where
|
|
288
|
+
there are not. Only the first can carry a moment: a request holding
|
|
289
|
+
frames is a single-table one, and a single-table model is not asked for a
|
|
290
|
+
window over time. The query is refused before it reaches here, so there
|
|
291
|
+
is no moment in the frame to put in the key and none to look for.
|
|
292
|
+
"""
|
|
293
|
+
return frozenset(scored_rows(request, with_anchor=with_anchor))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def without_the_keys(payload: dict[str, Any], keys: frozenset[Any]) -> int:
|
|
297
|
+
r"""Take out of ``payload``'s context every row naming one of ``keys``.
|
|
298
|
+
|
|
299
|
+
Returns how many instance rows went. Related rows that hang off them go
|
|
300
|
+
too, by the ``instance_id`` they were tied with, because a related row
|
|
301
|
+
whose instance has gone describes nothing.
|
|
302
|
+
|
|
303
|
+
The key is found the way `keys_in` finds it, so what is removed here is
|
|
304
|
+
exactly what would have been reported as an overlap there.
|
|
305
|
+
"""
|
|
306
|
+
context = payload.get('context')
|
|
307
|
+
if not isinstance(context, Mapping) or not keys:
|
|
308
|
+
return 0
|
|
309
|
+
table = context.get('instance_table')
|
|
310
|
+
columns = (table or {}).get('columns')
|
|
311
|
+
rows = (table or {}).get('rows')
|
|
312
|
+
if not isinstance(columns, list) or not isinstance(rows, list):
|
|
313
|
+
return 0
|
|
314
|
+
|
|
315
|
+
named = next(
|
|
316
|
+
(name for name in columns if str(name).startswith(_ENTITY_REF)), None
|
|
317
|
+
)
|
|
318
|
+
if named is None:
|
|
319
|
+
named = ((payload.get('schema') or {}).get('instance_table') or {}).get(
|
|
320
|
+
'primary_key'
|
|
321
|
+
)
|
|
322
|
+
wanted = named if isinstance(named, list) else [named]
|
|
323
|
+
if named is None or any(name not in columns for name in wanted):
|
|
324
|
+
return 0
|
|
325
|
+
|
|
326
|
+
at = [columns.index(name) for name in wanted]
|
|
327
|
+
instance = columns.index(INSTANCE_ID) if INSTANCE_ID in columns else None
|
|
328
|
+
going, leaving = [], set()
|
|
329
|
+
for row in rows:
|
|
330
|
+
if not isinstance(row, list) or len(row) <= max(at):
|
|
331
|
+
going.append(row)
|
|
332
|
+
continue
|
|
333
|
+
if _named_by(row[position] for position in at) in keys:
|
|
334
|
+
if instance is not None and len(row) > instance:
|
|
335
|
+
leaving.add(row[instance])
|
|
336
|
+
continue
|
|
337
|
+
going.append(row)
|
|
338
|
+
|
|
339
|
+
removed = len(rows) - len(going)
|
|
340
|
+
if not removed:
|
|
341
|
+
return 0
|
|
342
|
+
table['rows'] = going
|
|
343
|
+
for related in (context.get('related_tables') or {}).values():
|
|
344
|
+
_drop_instances(related, leaving)
|
|
345
|
+
return removed
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _drop_instances(table: Any, instances: set[Any]) -> None:
|
|
349
|
+
r"""Remove related rows tied to removed instances."""
|
|
350
|
+
columns = (table or {}).get('columns')
|
|
351
|
+
rows = (table or {}).get('rows')
|
|
352
|
+
if not isinstance(columns, list) or not isinstance(rows, list):
|
|
353
|
+
return
|
|
354
|
+
if INSTANCE_ID not in columns or not instances:
|
|
355
|
+
return
|
|
356
|
+
at = columns.index(INSTANCE_ID)
|
|
357
|
+
table['rows'] = [
|
|
358
|
+
row
|
|
359
|
+
for row in rows
|
|
360
|
+
if not (
|
|
361
|
+
isinstance(row, list) and len(row) > at and row[at] in instances
|
|
362
|
+
)
|
|
363
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
r"""Turning a DataFrame into the wire's ``arrays`` table."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from sdm_client.wire.dtypes import serialize_column
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def encode_table(frame: pd.DataFrame, dtypes: dict[str, str]) -> dict[str, Any]:
|
|
17
|
+
r"""Encode ``frame`` as an ``arrays`` table.
|
|
18
|
+
|
|
19
|
+
``arrays`` is the only table format the contract defines today: an ordered
|
|
20
|
+
list of column names and a list of row arrays positionally matching it.
|
|
21
|
+
Arrow IPC is a reserved extension point everywhere in this system and is
|
|
22
|
+
not implemented.
|
|
23
|
+
|
|
24
|
+
Serialization is per column rather than per cell, because a column has one
|
|
25
|
+
dtype and the rule for writing a value follows from it. The rows are then
|
|
26
|
+
transposed out of the serialized columns, which is what keeps a column's
|
|
27
|
+
values consistently written even where a row-major walk would see mixed
|
|
28
|
+
types.
|
|
29
|
+
|
|
30
|
+
A frame with no columns still carries its row count, so an empty-column
|
|
31
|
+
table is a list of empty rows rather than an empty table. The two mean
|
|
32
|
+
different things to the server.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
frame: The rows to encode.
|
|
36
|
+
dtypes: Wire dtype per column name. Must cover every column of
|
|
37
|
+
``frame``.
|
|
38
|
+
"""
|
|
39
|
+
columns = list(frame.columns)
|
|
40
|
+
if not columns:
|
|
41
|
+
return {
|
|
42
|
+
'format': 'arrays',
|
|
43
|
+
'columns': [],
|
|
44
|
+
'rows': [[] for _ in range(len(frame))],
|
|
45
|
+
}
|
|
46
|
+
serialized_columns = [
|
|
47
|
+
serialize_column(frame[column], dtypes[column]) for column in columns
|
|
48
|
+
]
|
|
49
|
+
rows = [list(row) for row in zip(*serialized_columns, strict=True)]
|
|
50
|
+
return {'format': 'arrays', 'columns': columns, 'rows': rows}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: structured-data-models-client
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: One Python client for NVIDIA's structured-data foundation model NIMs
|
|
5
|
+
Author: NVIDIA
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/NVIDIA/structured-data-models-client
|
|
8
|
+
Project-URL: Source, https://github.com/NVIDIA/structured-data-models-client
|
|
9
|
+
Project-URL: Issues, https://github.com/NVIDIA/structured-data-models-client/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/NVIDIA/structured-data-models-client/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: structured-data,foundation-model,tabular,relational
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
License-File: NOTICE
|
|
28
|
+
Requires-Dist: structured-data-models-api==1.0.0
|
|
29
|
+
Requires-Dist: structured-data-models-pql==1.0.0
|
|
30
|
+
Requires-Dist: pandas>=2.2.2
|
|
31
|
+
Requires-Dist: numpy>=1.23
|
|
32
|
+
Requires-Dist: requests>=2.28
|
|
33
|
+
Requires-Dist: urllib3>=1.26
|
|
34
|
+
Provides-Extra: relational
|
|
35
|
+
Requires-Dist: structured-data-models-relational-engine==1.0.0; extra == "relational"
|
|
36
|
+
Provides-Extra: sqlite
|
|
37
|
+
Requires-Dist: structured-data-models-connectors[sqlite]==1.0.0; extra == "sqlite"
|
|
38
|
+
Provides-Extra: duckdb
|
|
39
|
+
Requires-Dist: structured-data-models-connectors[duckdb]==1.0.0; extra == "duckdb"
|
|
40
|
+
Provides-Extra: snowflake
|
|
41
|
+
Requires-Dist: structured-data-models-connectors[snowflake]==1.0.0; extra == "snowflake"
|
|
42
|
+
Provides-Extra: databricks
|
|
43
|
+
Requires-Dist: structured-data-models-connectors[databricks]==1.0.0; extra == "databricks"
|
|
44
|
+
Provides-Extra: postgres
|
|
45
|
+
Requires-Dist: structured-data-models-connectors[postgres]==1.0.0; extra == "postgres"
|
|
46
|
+
Provides-Extra: lakebase
|
|
47
|
+
Requires-Dist: structured-data-models-connectors[lakebase]==1.0.0; extra == "lakebase"
|
|
48
|
+
Provides-Extra: s3
|
|
49
|
+
Requires-Dist: structured-data-models-connectors[s3]==1.0.0; extra == "s3"
|
|
50
|
+
Provides-Extra: databricks-serving
|
|
51
|
+
Requires-Dist: databricks-sdk<1.0,>=0.68.0; extra == "databricks-serving"
|
|
52
|
+
Provides-Extra: all
|
|
53
|
+
Requires-Dist: structured-data-models-client[databricks,duckdb,lakebase,postgres,relational,s3,snowflake,sqlite]; extra == "all"
|
|
54
|
+
Provides-Extra: test
|
|
55
|
+
Requires-Dist: pytest; extra == "test"
|
|
56
|
+
Requires-Dist: requests-mock; extra == "test"
|
|
57
|
+
Dynamic: license-file
|
|
58
|
+
|
|
59
|
+
# structured-data-models-client
|
|
60
|
+
|
|
61
|
+
One Python client for NVIDIA's structured-data foundation models. Ask a
|
|
62
|
+
predictive question of one table, or of a whole relational schema, without
|
|
63
|
+
training a model for each dataset.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install structured-data-models-client
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Inference runs in a NIM, so this package downloads no weights and needs no GPU.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import pandas as pd
|
|
73
|
+
from sdm_client import KumoTabular, Request, SDMClient
|
|
74
|
+
|
|
75
|
+
labelled = pd.DataFrame(
|
|
76
|
+
{
|
|
77
|
+
'age': [31.0, 44.5, 28.0, 51.0, 39.0],
|
|
78
|
+
'spend': [120.0, 430.0, 90.0, 510.0, 260.0],
|
|
79
|
+
'churn': [0, 1, 0, 1, 0],
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
to_score = pd.DataFrame({'age': [33.0, 47.0], 'spend': [150.0, 480.0]})
|
|
83
|
+
|
|
84
|
+
request = Request(
|
|
85
|
+
context=labelled, predict=to_score, target='churn', task='classification'
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
with SDMClient(url='http://localhost:8000') as client:
|
|
89
|
+
print(client.predict(request, model=KumoTabular()).to_pandas())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The model is an argument, not a method name. For several related tables, ask in
|
|
93
|
+
the Predictive Query Language and pass `KumoRelational()` instead; that needs
|
|
94
|
+
`pip install 'structured-data-models-client[relational]'`.
|
|
95
|
+
|
|
96
|
+
Documentation, the other four packages and the examples are in the repository:
|
|
97
|
+
<https://github.com/NVIDIA/structured-data-models-client>
|
|
98
|
+
|
|
99
|
+
Apache-2.0. Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
sdm_client/__init__.py,sha256=80jgFI3qqu2vM6cPg2D_1_4wjGiOodQpMI1Lqh20ygA,2067
|
|
2
|
+
sdm_client/_version.py,sha256=poqaNUKu0_Ul5ZFqMGv8evj9j4qgAD5x8P_Il9KxWOg,163
|
|
3
|
+
sdm_client/base.py,sha256=YDrTAoLgxQQG4WrsdI08tt40fx7wkuqhEWT52X6BKgU,10959
|
|
4
|
+
sdm_client/client.py,sha256=B4tVNTXOlrfBZVx1X814jt_BKymRP3ajEkapRZDvM-A,57548
|
|
5
|
+
sdm_client/decompose.py,sha256=EfHHgQ9VEqkI1HHJD-je7kKFPpGQ9C8ZRdpLyoaZKOY,16976
|
|
6
|
+
sdm_client/errors.py,sha256=-XsFUAnVuhNuG5vr21JNU72Esmn5SQSOU6H7WkAobJU,6474
|
|
7
|
+
sdm_client/models.py,sha256=BbCi04Cw4f8uo2_CXAs93enaHnIxAFVQF_BZrFvisoA,9708
|
|
8
|
+
sdm_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
sdm_client/query.py,sha256=8ArlBRQkN-HVodnZSkl7LFbgC6mtK5JNPt_zo_aqbKk,28438
|
|
10
|
+
sdm_client/request.py,sha256=R5C5OUnpYeLN0aQu6xdy4JyJ6xXDeaggiLezCYjuv1Y,47310
|
|
11
|
+
sdm_client/result.py,sha256=UQKeUQitF7Xql02CC_DRp-Y1SRgOfZO9j-R9FecAUkM,13917
|
|
12
|
+
sdm_client/schema.py,sha256=v6b2YOMTrXJdnYpX2ofXUnXMZwuYKBykW8bckIbHDWM,134074
|
|
13
|
+
sdm_client/adapters/__init__.py,sha256=2fRta-sa2gyep6ZWzsa56SczPXwusBKSzLBZBIgu1ro,275
|
|
14
|
+
sdm_client/adapters/relational.py,sha256=aHW0DjhNzOk1NekRcoSNmGKTw8hgJ_3n1xIwDp_FMZc,35381
|
|
15
|
+
sdm_client/adapters/tabular.py,sha256=KhqW7OOoik39-urT3jMWNkcJMKXR-IT5Jf_7ORqoDLs,23460
|
|
16
|
+
sdm_client/core/__init__.py,sha256=dzFONzK2amcBi2mjeOZLTtsGl9v9zm5BlbbdiGFYWQI,201
|
|
17
|
+
sdm_client/core/databricks_serving.py,sha256=rb7xxudzMZ-iAdNrSm7JXwTki-k-Ri9Z4xRoClf8BoA,23884
|
|
18
|
+
sdm_client/core/databricks_user_agent.py,sha256=L2hfkeZKydT8aN9zYWzx17cIbwWcTpvjU4DkqKyxwfA,2969
|
|
19
|
+
sdm_client/core/dates.py,sha256=0lrCfXS4PLQGi-rV7LoMXx9Ljg7zVGcInnCofZsFgIk,11393
|
|
20
|
+
sdm_client/core/transport.py,sha256=JMnOvEOjjUNDmAUwxi1UvB0z455gVq-MbmltbcnNu7s,34658
|
|
21
|
+
sdm_client/core/warnings.py,sha256=vWfjbTI3H7BsGJZCwKI3sVpgbfyROi8rVes6B_kiUx8,1693
|
|
22
|
+
sdm_client/materializers/__init__.py,sha256=0LRVeZyFxJGvN71w01NKOo4IQ5PvU08P_APD9XlQR4Y,407
|
|
23
|
+
sdm_client/materializers/relational.py,sha256=lJzFDBzFnUflZQWpdGhI3nwHFWMIIZ3giUgff8H93Jg,75385
|
|
24
|
+
sdm_client/materializers/sources.py,sha256=oRcExP2eL4cNUTn2a8hMYpsP3CZUGrfPAOUCMwmWt2M,11492
|
|
25
|
+
sdm_client/materializers/tabular.py,sha256=ExhK4HR6xdcTU4j1ysSY4RN_1h07IsYDcPL1CxpIItE,12463
|
|
26
|
+
sdm_client/wire/__init__.py,sha256=6eIZ3kuYq-XKMvKiQWFgIhFXxMOWkQrMQaJusWayXyE,1333
|
|
27
|
+
sdm_client/wire/answers.py,sha256=6v_91HuHB7OaAuHrbz77_Xmk94N83t-l1azp3SQ2rjU,3156
|
|
28
|
+
sdm_client/wire/dtypes.py,sha256=hFhhjmlY-UDRoXgNiNHf-8xlAk0Tm_gSPiaxxveqX2I,28791
|
|
29
|
+
sdm_client/wire/keys.py,sha256=mkuHX-Hfm6wiilwDTfxmjRjrN8x-8Dc7nLYqNkHK95o,14315
|
|
30
|
+
sdm_client/wire/tables.py,sha256=YFBURW3v00X2rzmyeG5ebTTEnwwEv8WKKwjayWO1jEU,1822
|
|
31
|
+
structured_data_models_client-1.0.0.dist-info/licenses/LICENSE,sha256=fM-2HkOnqT3X8X_O17f2oQGYvlXIdaMvdUYepurE32I,11432
|
|
32
|
+
structured_data_models_client-1.0.0.dist-info/licenses/NOTICE,sha256=n-tTBUaxjlPv7qKQfp4g1JFKcB7nYalVQv4NjirI8b0,405
|
|
33
|
+
structured_data_models_client-1.0.0.dist-info/METADATA,sha256=EPxCwaTW4zwAOGCxxvPt59puzYXck10T7TYQhlIWxIA,4111
|
|
34
|
+
structured_data_models_client-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
35
|
+
structured_data_models_client-1.0.0.dist-info/top_level.txt,sha256=adIMKX4LMOb2GTCVOMWoHptnvE2vsHMY2glsW1zCphM,11
|
|
36
|
+
structured_data_models_client-1.0.0.dist-info/RECORD,,
|