kumoai 2.14.0.dev202601011731__cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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.
Potentially problematic release.
This version of kumoai might be problematic. Click here for more details.
- kumoai/__init__.py +300 -0
- kumoai/_logging.py +29 -0
- kumoai/_singleton.py +25 -0
- kumoai/_version.py +1 -0
- kumoai/artifact_export/__init__.py +9 -0
- kumoai/artifact_export/config.py +209 -0
- kumoai/artifact_export/job.py +108 -0
- kumoai/client/__init__.py +5 -0
- kumoai/client/client.py +223 -0
- kumoai/client/connector.py +110 -0
- kumoai/client/endpoints.py +150 -0
- kumoai/client/graph.py +120 -0
- kumoai/client/jobs.py +471 -0
- kumoai/client/online.py +78 -0
- kumoai/client/pquery.py +207 -0
- kumoai/client/rfm.py +112 -0
- kumoai/client/source_table.py +53 -0
- kumoai/client/table.py +101 -0
- kumoai/client/utils.py +130 -0
- kumoai/codegen/__init__.py +19 -0
- kumoai/codegen/cli.py +100 -0
- kumoai/codegen/context.py +16 -0
- kumoai/codegen/edits.py +473 -0
- kumoai/codegen/exceptions.py +10 -0
- kumoai/codegen/generate.py +222 -0
- kumoai/codegen/handlers/__init__.py +4 -0
- kumoai/codegen/handlers/connector.py +118 -0
- kumoai/codegen/handlers/graph.py +71 -0
- kumoai/codegen/handlers/pquery.py +62 -0
- kumoai/codegen/handlers/table.py +109 -0
- kumoai/codegen/handlers/utils.py +42 -0
- kumoai/codegen/identity.py +114 -0
- kumoai/codegen/loader.py +93 -0
- kumoai/codegen/naming.py +94 -0
- kumoai/codegen/registry.py +121 -0
- kumoai/connector/__init__.py +31 -0
- kumoai/connector/base.py +153 -0
- kumoai/connector/bigquery_connector.py +200 -0
- kumoai/connector/databricks_connector.py +213 -0
- kumoai/connector/file_upload_connector.py +189 -0
- kumoai/connector/glue_connector.py +150 -0
- kumoai/connector/s3_connector.py +278 -0
- kumoai/connector/snowflake_connector.py +252 -0
- kumoai/connector/source_table.py +471 -0
- kumoai/connector/utils.py +1796 -0
- kumoai/databricks.py +14 -0
- kumoai/encoder/__init__.py +4 -0
- kumoai/exceptions.py +26 -0
- kumoai/experimental/__init__.py +0 -0
- kumoai/experimental/rfm/__init__.py +210 -0
- kumoai/experimental/rfm/authenticate.py +432 -0
- kumoai/experimental/rfm/backend/__init__.py +0 -0
- kumoai/experimental/rfm/backend/local/__init__.py +42 -0
- kumoai/experimental/rfm/backend/local/graph_store.py +297 -0
- kumoai/experimental/rfm/backend/local/sampler.py +312 -0
- kumoai/experimental/rfm/backend/local/table.py +113 -0
- kumoai/experimental/rfm/backend/snow/__init__.py +37 -0
- kumoai/experimental/rfm/backend/snow/sampler.py +297 -0
- kumoai/experimental/rfm/backend/snow/table.py +242 -0
- kumoai/experimental/rfm/backend/sqlite/__init__.py +32 -0
- kumoai/experimental/rfm/backend/sqlite/sampler.py +398 -0
- kumoai/experimental/rfm/backend/sqlite/table.py +184 -0
- kumoai/experimental/rfm/base/__init__.py +30 -0
- kumoai/experimental/rfm/base/column.py +152 -0
- kumoai/experimental/rfm/base/expression.py +44 -0
- kumoai/experimental/rfm/base/sampler.py +761 -0
- kumoai/experimental/rfm/base/source.py +19 -0
- kumoai/experimental/rfm/base/sql_sampler.py +143 -0
- kumoai/experimental/rfm/base/table.py +736 -0
- kumoai/experimental/rfm/graph.py +1237 -0
- kumoai/experimental/rfm/infer/__init__.py +19 -0
- kumoai/experimental/rfm/infer/categorical.py +40 -0
- kumoai/experimental/rfm/infer/dtype.py +82 -0
- kumoai/experimental/rfm/infer/id.py +46 -0
- kumoai/experimental/rfm/infer/multicategorical.py +48 -0
- kumoai/experimental/rfm/infer/pkey.py +128 -0
- kumoai/experimental/rfm/infer/stype.py +35 -0
- kumoai/experimental/rfm/infer/time_col.py +61 -0
- kumoai/experimental/rfm/infer/timestamp.py +41 -0
- kumoai/experimental/rfm/pquery/__init__.py +7 -0
- kumoai/experimental/rfm/pquery/executor.py +102 -0
- kumoai/experimental/rfm/pquery/pandas_executor.py +530 -0
- kumoai/experimental/rfm/relbench.py +76 -0
- kumoai/experimental/rfm/rfm.py +1184 -0
- kumoai/experimental/rfm/sagemaker.py +138 -0
- kumoai/experimental/rfm/task_table.py +231 -0
- kumoai/formatting.py +30 -0
- kumoai/futures.py +99 -0
- kumoai/graph/__init__.py +12 -0
- kumoai/graph/column.py +106 -0
- kumoai/graph/graph.py +948 -0
- kumoai/graph/table.py +838 -0
- kumoai/jobs.py +80 -0
- kumoai/kumolib.cpython-310-x86_64-linux-gnu.so +0 -0
- kumoai/mixin.py +28 -0
- kumoai/pquery/__init__.py +25 -0
- kumoai/pquery/prediction_table.py +287 -0
- kumoai/pquery/predictive_query.py +641 -0
- kumoai/pquery/training_table.py +424 -0
- kumoai/spcs.py +121 -0
- kumoai/testing/__init__.py +8 -0
- kumoai/testing/decorators.py +57 -0
- kumoai/testing/snow.py +50 -0
- kumoai/trainer/__init__.py +42 -0
- kumoai/trainer/baseline_trainer.py +93 -0
- kumoai/trainer/config.py +2 -0
- kumoai/trainer/distilled_trainer.py +175 -0
- kumoai/trainer/job.py +1192 -0
- kumoai/trainer/online_serving.py +258 -0
- kumoai/trainer/trainer.py +475 -0
- kumoai/trainer/util.py +103 -0
- kumoai/utils/__init__.py +11 -0
- kumoai/utils/datasets.py +83 -0
- kumoai/utils/display.py +51 -0
- kumoai/utils/forecasting.py +209 -0
- kumoai/utils/progress_logger.py +343 -0
- kumoai/utils/sql.py +3 -0
- kumoai-2.14.0.dev202601011731.dist-info/METADATA +71 -0
- kumoai-2.14.0.dev202601011731.dist-info/RECORD +122 -0
- kumoai-2.14.0.dev202601011731.dist-info/WHEEL +6 -0
- kumoai-2.14.0.dev202601011731.dist-info/licenses/LICENSE +9 -0
- kumoai-2.14.0.dev202601011731.dist-info/top_level.txt +1 -0
kumoai/client/pquery.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from dataclasses import fields, make_dataclass
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from typing import List, Optional, Tuple, Union, get_args, get_origin
|
|
5
|
+
|
|
6
|
+
from kumoapi.common import ValidationResponse
|
|
7
|
+
from kumoapi.json_serde import to_json_dict
|
|
8
|
+
from kumoapi.model_plan import (
|
|
9
|
+
MissingType,
|
|
10
|
+
ModelPlan,
|
|
11
|
+
ModelPlanInfo,
|
|
12
|
+
PlanMixin,
|
|
13
|
+
SuggestModelPlanRequest,
|
|
14
|
+
TrainingTableGenerationPlan,
|
|
15
|
+
)
|
|
16
|
+
from kumoapi.pquery import PQueryResource, QueryType
|
|
17
|
+
from kumoapi.task import TaskType
|
|
18
|
+
from kumoapi.typing import WITH_PYDANTIC_V2
|
|
19
|
+
from pydantic.dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
from kumoai.client import KumoClient
|
|
22
|
+
from kumoai.client.endpoints import PQueryEndpoints
|
|
23
|
+
from kumoai.client.utils import (
|
|
24
|
+
Returns,
|
|
25
|
+
parse_id_response,
|
|
26
|
+
parse_response,
|
|
27
|
+
raise_on_error,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
GraphID = str
|
|
31
|
+
PQueryID = str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PQueryAPI:
|
|
35
|
+
r"""Typed API definition for Kumo Predictive Query definition."""
|
|
36
|
+
def __init__(self, client: KumoClient) -> None:
|
|
37
|
+
self._client = client
|
|
38
|
+
|
|
39
|
+
def create(self, pquery: PQueryResource) -> PQueryID:
|
|
40
|
+
r"""Creates a Predictive Query resource object in Kumo."""
|
|
41
|
+
resp = self._client._request(PQueryEndpoints.create,
|
|
42
|
+
json=to_json_dict(pquery))
|
|
43
|
+
raise_on_error(resp)
|
|
44
|
+
return parse_id_response(resp)
|
|
45
|
+
|
|
46
|
+
def get_if_exists(self, name_or_id: str) -> Optional[PQueryResource]:
|
|
47
|
+
r"""Fetches a Predictive Query given its name(id)."""
|
|
48
|
+
resp = self._client._request(PQueryEndpoints.get.with_id(name_or_id))
|
|
49
|
+
if resp.status_code == HTTPStatus.NOT_FOUND:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
raise_on_error(resp)
|
|
53
|
+
return parse_response(PQueryResource, resp)
|
|
54
|
+
|
|
55
|
+
def list(self, *,
|
|
56
|
+
name_pattern: Optional[str] = None) -> List[PQueryResource]:
|
|
57
|
+
params = {}
|
|
58
|
+
if name_pattern:
|
|
59
|
+
params['name_pattern'] = name_pattern
|
|
60
|
+
resp = self._client._request(PQueryEndpoints.list, params=params)
|
|
61
|
+
raise_on_error(resp)
|
|
62
|
+
return parse_response(List[PQueryResource], resp)
|
|
63
|
+
|
|
64
|
+
def delete(self, name: str) -> None:
|
|
65
|
+
resp = self._client._request(PQueryEndpoints.delete.with_id(name))
|
|
66
|
+
raise_on_error(resp)
|
|
67
|
+
|
|
68
|
+
def infer_task_type(
|
|
69
|
+
self,
|
|
70
|
+
pquery_string: str,
|
|
71
|
+
graph_id: GraphID,
|
|
72
|
+
) -> Tuple[TaskType, str]:
|
|
73
|
+
resp = self._client._request(
|
|
74
|
+
PQueryEndpoints.infer_task_type,
|
|
75
|
+
params={
|
|
76
|
+
'pquery_string': pquery_string,
|
|
77
|
+
'graph_id': graph_id
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
raise_on_error(resp)
|
|
81
|
+
return parse_response(Returns[Tuple[TaskType, str]], resp)
|
|
82
|
+
|
|
83
|
+
def validate(self, pquery: PQueryResource) -> ValidationResponse:
|
|
84
|
+
r"""Validates a Predictive Query resource object in Kumo."""
|
|
85
|
+
resp = self._client._request(PQueryEndpoints.validate,
|
|
86
|
+
json=to_json_dict(pquery))
|
|
87
|
+
raise_on_error(resp)
|
|
88
|
+
return parse_response(Returns[ValidationResponse], resp)
|
|
89
|
+
|
|
90
|
+
def suggest_training_table_plan(
|
|
91
|
+
self,
|
|
92
|
+
req: SuggestModelPlanRequest,
|
|
93
|
+
) -> TrainingTableGenerationPlan:
|
|
94
|
+
resp = self._client._request(
|
|
95
|
+
PQueryEndpoints.suggest_training_table_plan,
|
|
96
|
+
json=to_json_dict(req),
|
|
97
|
+
)
|
|
98
|
+
raise_on_error(resp)
|
|
99
|
+
return parse_response(TrainingTableGenerationPlan, resp)
|
|
100
|
+
|
|
101
|
+
def suggest_model_plan(
|
|
102
|
+
self,
|
|
103
|
+
req: SuggestModelPlanRequest,
|
|
104
|
+
) -> ModelPlan:
|
|
105
|
+
resp = self._client._request(
|
|
106
|
+
PQueryEndpoints.suggest_model_plan,
|
|
107
|
+
json=to_json_dict(req),
|
|
108
|
+
)
|
|
109
|
+
raise_on_error(resp)
|
|
110
|
+
plan_info = parse_response(ModelPlanInfo, resp)
|
|
111
|
+
|
|
112
|
+
return filter_model_plan(
|
|
113
|
+
plan_info.model_plan,
|
|
114
|
+
plan_info.task_type,
|
|
115
|
+
plan_info.query_type,
|
|
116
|
+
plan_info.has_train_table_weight_col,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def filter_model_plan(
|
|
121
|
+
plan: ModelPlan,
|
|
122
|
+
task_type: TaskType,
|
|
123
|
+
query_type: QueryType,
|
|
124
|
+
has_train_table_weight_col: bool,
|
|
125
|
+
) -> ModelPlan:
|
|
126
|
+
r"""Filters the model plan ``plan`` down to its valid fields only, given
|
|
127
|
+
``task_type`` and ``query_type``.
|
|
128
|
+
"""
|
|
129
|
+
new_section_fields = []
|
|
130
|
+
new_sections = []
|
|
131
|
+
for section_field in fields(plan):
|
|
132
|
+
section = getattr(plan, section_field.name)
|
|
133
|
+
|
|
134
|
+
new_opt_fields = []
|
|
135
|
+
new_opts = []
|
|
136
|
+
for field in fields(section):
|
|
137
|
+
if WITH_PYDANTIC_V2:
|
|
138
|
+
metadata = field.default.json_schema_extra # type: ignore
|
|
139
|
+
else:
|
|
140
|
+
metadata = field.default.extra['metadata'] # type: ignore
|
|
141
|
+
if not section.is_valid_option(
|
|
142
|
+
name=field.name,
|
|
143
|
+
metadata=metadata,
|
|
144
|
+
task_type=task_type,
|
|
145
|
+
query_type=query_type,
|
|
146
|
+
has_train_table_weight_col=has_train_table_weight_col,
|
|
147
|
+
):
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# Remove `MissingType` from type annotation:
|
|
151
|
+
if WITH_PYDANTIC_V2:
|
|
152
|
+
hidden = metadata["hidden"]
|
|
153
|
+
else:
|
|
154
|
+
hidden = metadata.hidden
|
|
155
|
+
if not hidden and get_origin(field.type) is Union:
|
|
156
|
+
_types = tuple(_type for _type in get_args(field.type)
|
|
157
|
+
if _type is not MissingType)
|
|
158
|
+
assert len(_types) > 0
|
|
159
|
+
_type = Union[_types] if len(_types) > 1 else _types[0]
|
|
160
|
+
else:
|
|
161
|
+
_type = field.type
|
|
162
|
+
|
|
163
|
+
default = copy.copy(field.default)
|
|
164
|
+
if not WITH_PYDANTIC_V2:
|
|
165
|
+
# Pydantic v1
|
|
166
|
+
from pydantic.fields import Undefined
|
|
167
|
+
default.default = Undefined # type: ignore
|
|
168
|
+
else:
|
|
169
|
+
try:
|
|
170
|
+
# Pydantic v2 - Undefined moved to pydantic_core
|
|
171
|
+
from pydantic_core import PydanticUndefined
|
|
172
|
+
default.default = PydanticUndefined # type: ignore
|
|
173
|
+
except ImportError:
|
|
174
|
+
# Fallback: Neither v1 nor v2 Undefined available
|
|
175
|
+
# In Pydantic v2, not setting a default is equivalent to
|
|
176
|
+
# Undefined
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
# Forward compatibility - Remove any newly introduced arguments not
|
|
180
|
+
# returned yet by the backend:
|
|
181
|
+
value = getattr(section, field.name)
|
|
182
|
+
if value != MissingType.VALUE:
|
|
183
|
+
new_opt_fields.append((field.name, _type, default))
|
|
184
|
+
new_opts.append(value)
|
|
185
|
+
|
|
186
|
+
Section = dataclass(
|
|
187
|
+
config=dict(validate_assignment=True),
|
|
188
|
+
repr=False,
|
|
189
|
+
)(make_dataclass(
|
|
190
|
+
type(section).__name__,
|
|
191
|
+
new_opt_fields,
|
|
192
|
+
bases=(PlanMixin, ),
|
|
193
|
+
repr=False,
|
|
194
|
+
))
|
|
195
|
+
new_section_fields.append((section_field.name, Section))
|
|
196
|
+
new_sections.append(Section(*new_opts))
|
|
197
|
+
|
|
198
|
+
Plan = dataclass(
|
|
199
|
+
config=dict(validate_assignment=True),
|
|
200
|
+
repr=False,
|
|
201
|
+
)(make_dataclass(
|
|
202
|
+
'ModelPlan',
|
|
203
|
+
new_section_fields,
|
|
204
|
+
bases=(ModelPlan, ),
|
|
205
|
+
repr=False,
|
|
206
|
+
))
|
|
207
|
+
return Plan(*new_sections)
|
kumoai/client/rfm.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from kumoapi.json_serde import to_json_dict
|
|
4
|
+
from kumoapi.rfm import (
|
|
5
|
+
RFMEvaluateResponse,
|
|
6
|
+
RFMExplanationResponse,
|
|
7
|
+
RFMParseQueryRequest,
|
|
8
|
+
RFMParseQueryResponse,
|
|
9
|
+
RFMPredictResponse,
|
|
10
|
+
RFMValidateQueryRequest,
|
|
11
|
+
RFMValidateQueryResponse,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from kumoai.client import KumoClient
|
|
15
|
+
from kumoai.client.endpoints import RFMEndpoints
|
|
16
|
+
from kumoai.client.utils import parse_response, raise_on_error
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RFMAPI:
|
|
20
|
+
r"""Typed API definition for Kumo RFM (Relational Foundation Model)."""
|
|
21
|
+
def __init__(self, client: KumoClient) -> None:
|
|
22
|
+
self._client = client
|
|
23
|
+
|
|
24
|
+
def predict(self, request: bytes) -> RFMPredictResponse:
|
|
25
|
+
"""Make predictions using the RFM model.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
request: The predict request as serialized protobuf.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
RFMPredictResponse containing the predictions
|
|
32
|
+
"""
|
|
33
|
+
response = self._client._request(
|
|
34
|
+
RFMEndpoints.predict,
|
|
35
|
+
data=request,
|
|
36
|
+
headers={'Content-Type': 'application/x-protobuf'},
|
|
37
|
+
)
|
|
38
|
+
raise_on_error(response)
|
|
39
|
+
return parse_response(RFMPredictResponse, response)
|
|
40
|
+
|
|
41
|
+
def explain(
|
|
42
|
+
self,
|
|
43
|
+
request: bytes,
|
|
44
|
+
skip_summary: bool = False,
|
|
45
|
+
) -> RFMExplanationResponse:
|
|
46
|
+
"""Explain the RFM model on the given context.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
request: The predict request as serialized protobuf.
|
|
50
|
+
skip_summary: Whether to skip generating a human-readable summary
|
|
51
|
+
of the explanation.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
RFMPredictResponse containing the explanations
|
|
55
|
+
"""
|
|
56
|
+
params: dict[str, Any] = {'generate_summary': not skip_summary}
|
|
57
|
+
response = self._client._request(
|
|
58
|
+
RFMEndpoints.explain, data=request, params=params,
|
|
59
|
+
headers={'Content-Type': 'application/x-protobuf'})
|
|
60
|
+
raise_on_error(response)
|
|
61
|
+
return parse_response(RFMExplanationResponse, response)
|
|
62
|
+
|
|
63
|
+
def evaluate(self, request: bytes) -> RFMEvaluateResponse:
|
|
64
|
+
"""Evaluate the RFM model on the given context.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
request: The evaluate request as serialized protobuf.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
RFMEvaluateResponse containing the computed metrics
|
|
71
|
+
"""
|
|
72
|
+
response = self._client._request(
|
|
73
|
+
RFMEndpoints.evaluate, data=request,
|
|
74
|
+
headers={'Content-Type': 'application/x-protobuf'})
|
|
75
|
+
raise_on_error(response)
|
|
76
|
+
return parse_response(RFMEvaluateResponse, response)
|
|
77
|
+
|
|
78
|
+
def validate_query(
|
|
79
|
+
self,
|
|
80
|
+
request: RFMValidateQueryRequest,
|
|
81
|
+
) -> RFMValidateQueryResponse:
|
|
82
|
+
"""Validate a predictive query against a graph.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
request: The request object containing
|
|
86
|
+
the query and graph definition
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
RFMValidateQueryResponse containing the QueryDefinition
|
|
90
|
+
"""
|
|
91
|
+
response = self._client._request(RFMEndpoints.validate_query,
|
|
92
|
+
json=to_json_dict(request))
|
|
93
|
+
raise_on_error(response)
|
|
94
|
+
return parse_response(RFMValidateQueryResponse, response)
|
|
95
|
+
|
|
96
|
+
def parse_query(
|
|
97
|
+
self,
|
|
98
|
+
request: RFMParseQueryRequest,
|
|
99
|
+
) -> RFMParseQueryResponse:
|
|
100
|
+
"""Validate a predictive query against a graph.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
request: The request object containing
|
|
104
|
+
the query and graph definition
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
RFMParseQueryResponse containing the QueryDefinition
|
|
108
|
+
"""
|
|
109
|
+
response = self._client._request(RFMEndpoints.parse_query,
|
|
110
|
+
json=to_json_dict(request))
|
|
111
|
+
raise_on_error(response)
|
|
112
|
+
return parse_response(RFMParseQueryResponse, response)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from kumoapi.json_serde import to_json_dict
|
|
4
|
+
from kumoapi.source_table import (
|
|
5
|
+
SourceTableConfigRequest,
|
|
6
|
+
SourceTableConfigResponse,
|
|
7
|
+
SourceTableDataRequest,
|
|
8
|
+
SourceTableDataResponse,
|
|
9
|
+
SourceTableListRequest,
|
|
10
|
+
SourceTableListResponse,
|
|
11
|
+
SourceTableValidateRequest,
|
|
12
|
+
SourceTableValidateResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from kumoai.client import KumoClient
|
|
16
|
+
from kumoai.client.endpoints import SourceTableEndpoints
|
|
17
|
+
from kumoai.client.utils import parse_response, raise_on_error
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SourceTableAPI:
|
|
21
|
+
r"""Typed API definition for Kumo source tables."""
|
|
22
|
+
def __init__(self, client: KumoClient) -> None:
|
|
23
|
+
self._client = client
|
|
24
|
+
|
|
25
|
+
def validate_table(
|
|
26
|
+
self, request: SourceTableValidateRequest
|
|
27
|
+
) -> SourceTableValidateResponse:
|
|
28
|
+
response = self._client._request(SourceTableEndpoints.validate_table,
|
|
29
|
+
json=to_json_dict(request))
|
|
30
|
+
raise_on_error(response)
|
|
31
|
+
return parse_response(SourceTableValidateResponse, response)
|
|
32
|
+
|
|
33
|
+
def list_tables(
|
|
34
|
+
self, request: SourceTableListRequest) -> SourceTableListResponse:
|
|
35
|
+
response = self._client._request(SourceTableEndpoints.list_tables,
|
|
36
|
+
json=to_json_dict(request))
|
|
37
|
+
raise_on_error(response)
|
|
38
|
+
return parse_response(SourceTableListResponse, response)
|
|
39
|
+
|
|
40
|
+
def get_table_data(
|
|
41
|
+
self, request: SourceTableDataRequest) -> SourceTableDataResponse:
|
|
42
|
+
response = self._client._request(SourceTableEndpoints.get_table_data,
|
|
43
|
+
json=to_json_dict(request))
|
|
44
|
+
raise_on_error(response)
|
|
45
|
+
return parse_response(List[SourceTableDataResponse], response)
|
|
46
|
+
|
|
47
|
+
def get_table_config(
|
|
48
|
+
self,
|
|
49
|
+
request: SourceTableConfigRequest) -> SourceTableConfigResponse:
|
|
50
|
+
response = self._client._request(SourceTableEndpoints.get_table_config,
|
|
51
|
+
json=to_json_dict(request))
|
|
52
|
+
raise_on_error(response)
|
|
53
|
+
return parse_response(SourceTableConfigResponse, response)
|
kumoai/client/table.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from kumoapi.common import ValidationResponse
|
|
5
|
+
from kumoapi.data_snapshot import TableSnapshotID, TableSnapshotResource
|
|
6
|
+
from kumoapi.json_serde import to_json_dict
|
|
7
|
+
from kumoapi.table import (
|
|
8
|
+
TableDefinition,
|
|
9
|
+
TableMetadataRequest,
|
|
10
|
+
TableMetadataResponse,
|
|
11
|
+
TableResource,
|
|
12
|
+
TableValidationRequest,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from kumoai.client import KumoClient
|
|
16
|
+
from kumoai.client.endpoints import TableEndpoints
|
|
17
|
+
from kumoai.client.utils import (
|
|
18
|
+
parse_id_response,
|
|
19
|
+
parse_response,
|
|
20
|
+
raise_on_error,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
TableID = str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TableAPI:
|
|
27
|
+
r"""Typed API definition for Kumo table definition."""
|
|
28
|
+
def __init__(self, client: KumoClient) -> None:
|
|
29
|
+
self._client = client
|
|
30
|
+
|
|
31
|
+
def create_table(
|
|
32
|
+
self,
|
|
33
|
+
table_def: TableDefinition,
|
|
34
|
+
*,
|
|
35
|
+
name_alias: Optional[str] = None,
|
|
36
|
+
force_rename: bool = False,
|
|
37
|
+
) -> TableID:
|
|
38
|
+
r"""Creates a Table (metadata definition) resource object in Kumo."""
|
|
39
|
+
params: Dict[str, Any] = {'force_rename': force_rename}
|
|
40
|
+
if name_alias:
|
|
41
|
+
params['name_alias'] = name_alias
|
|
42
|
+
resp = self._client._request(
|
|
43
|
+
TableEndpoints.create,
|
|
44
|
+
params=params,
|
|
45
|
+
json=to_json_dict(table_def),
|
|
46
|
+
)
|
|
47
|
+
raise_on_error(resp)
|
|
48
|
+
return parse_id_response(resp)
|
|
49
|
+
|
|
50
|
+
def get_table_if_exists(
|
|
51
|
+
self,
|
|
52
|
+
table_id_or_name: str,
|
|
53
|
+
) -> Optional[TableResource]:
|
|
54
|
+
r"""Fetches a connector given its ID."""
|
|
55
|
+
resp = self._client._request(
|
|
56
|
+
TableEndpoints.get.with_id(table_id_or_name))
|
|
57
|
+
if resp.status_code == HTTPStatus.NOT_FOUND:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
raise_on_error(resp)
|
|
61
|
+
return parse_response(TableResource, resp)
|
|
62
|
+
|
|
63
|
+
def create_snapshot(
|
|
64
|
+
self,
|
|
65
|
+
table_definition: TableDefinition,
|
|
66
|
+
*,
|
|
67
|
+
refresh_source: bool = False,
|
|
68
|
+
) -> TableSnapshotID:
|
|
69
|
+
params: Dict[str, Any] = {'refresh_source': refresh_source}
|
|
70
|
+
resp = self._client._request(TableEndpoints.create_snapshot,
|
|
71
|
+
params=params,
|
|
72
|
+
json=to_json_dict(table_definition))
|
|
73
|
+
raise_on_error(resp)
|
|
74
|
+
return parse_id_response(resp)
|
|
75
|
+
|
|
76
|
+
def get_snapshot(
|
|
77
|
+
self,
|
|
78
|
+
snapshot_id: TableSnapshotID,
|
|
79
|
+
) -> TableSnapshotResource:
|
|
80
|
+
resp = self._client._request(
|
|
81
|
+
TableEndpoints.get_snapshot.with_id(snapshot_id))
|
|
82
|
+
raise_on_error(resp)
|
|
83
|
+
return parse_response(TableSnapshotResource, resp)
|
|
84
|
+
|
|
85
|
+
def infer_metadata(
|
|
86
|
+
self,
|
|
87
|
+
request: TableMetadataRequest,
|
|
88
|
+
) -> TableMetadataResponse:
|
|
89
|
+
response = self._client._request(TableEndpoints.infer_metadata,
|
|
90
|
+
json=to_json_dict(request))
|
|
91
|
+
raise_on_error(response)
|
|
92
|
+
return parse_response(TableMetadataResponse, response)
|
|
93
|
+
|
|
94
|
+
def validate_table(
|
|
95
|
+
self,
|
|
96
|
+
request: TableValidationRequest,
|
|
97
|
+
) -> ValidationResponse:
|
|
98
|
+
response = self._client._request(TableEndpoints.validate,
|
|
99
|
+
json=to_json_dict(request))
|
|
100
|
+
raise_on_error(response)
|
|
101
|
+
return parse_response(ValidationResponse, response)
|
kumoai/client/utils.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import (
|
|
3
|
+
TYPE_CHECKING,
|
|
4
|
+
Any,
|
|
5
|
+
Dict,
|
|
6
|
+
Generic,
|
|
7
|
+
List,
|
|
8
|
+
Type,
|
|
9
|
+
TypeVar,
|
|
10
|
+
Union,
|
|
11
|
+
cast,
|
|
12
|
+
get_args,
|
|
13
|
+
get_origin,
|
|
14
|
+
overload,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
from kumoapi.json_serde import from_json
|
|
19
|
+
|
|
20
|
+
from kumoai.exceptions import HTTPException
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_DataclassT = TypeVar('_DataclassT')
|
|
25
|
+
ResponseT = TypeVar('ResponseT')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# See https://github.com/python/mypy/issues/9003
|
|
29
|
+
class Returns(Generic[ResponseT]):
|
|
30
|
+
r"""Use this class if you'd like to use Python types as annotations for
|
|
31
|
+
mypy, where `Type` is expected. See `parse_response` as an example.
|
|
32
|
+
"""
|
|
33
|
+
if not TYPE_CHECKING:
|
|
34
|
+
|
|
35
|
+
def __class_getitem__(cls, item: object) -> object:
|
|
36
|
+
"""Called when Returns is used as an Annotation at runtime.
|
|
37
|
+
We just return the type we're given
|
|
38
|
+
"""
|
|
39
|
+
return item
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_dataclass_list(
|
|
43
|
+
data_class: Type[_DataclassT],
|
|
44
|
+
response: requests.Response,
|
|
45
|
+
) -> List[_DataclassT]:
|
|
46
|
+
json_response = response.json()
|
|
47
|
+
assert isinstance(json_response, list)
|
|
48
|
+
|
|
49
|
+
def _parse_elem(v: Any) -> _DataclassT:
|
|
50
|
+
if issubclass(data_class, (str, int)):
|
|
51
|
+
return cast(_DataclassT, data_class(v))
|
|
52
|
+
if data_class == bool:
|
|
53
|
+
return cast(_DataclassT, str(v).lower() == 'true')
|
|
54
|
+
return from_json(v, data_class)
|
|
55
|
+
|
|
56
|
+
return [_parse_elem(item) for item in json_response]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@overload
|
|
60
|
+
def parse_response(response_type: Type[Returns[ResponseT]],
|
|
61
|
+
response: requests.Response) -> ResponseT:
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@overload
|
|
66
|
+
def parse_response(response_type: Type[ResponseT],
|
|
67
|
+
response: requests.Response) -> ResponseT:
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_response(response_type: Union[Type[ResponseT],
|
|
72
|
+
Type[Returns[ResponseT]]],
|
|
73
|
+
response: requests.Response) -> ResponseT:
|
|
74
|
+
"""Parse the HTTP response body into a Pydantic data structure.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
response_type: a class definition for a Pydantic dataclass, or a
|
|
78
|
+
:class:`list` of Pydantic dataclasses.
|
|
79
|
+
response: a requests.Response object containing
|
|
80
|
+
json data in the body
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The parsed response.
|
|
84
|
+
"""
|
|
85
|
+
# Case 0: no parsing needed
|
|
86
|
+
if response_type == str:
|
|
87
|
+
return response.text # type: ignore
|
|
88
|
+
if response_type == bool:
|
|
89
|
+
return response.text.lower() == 'true' # type: ignore
|
|
90
|
+
|
|
91
|
+
# Case 1: dict[str, V] type
|
|
92
|
+
if response_type == dict:
|
|
93
|
+
k_type, v_type = get_args(response_type)
|
|
94
|
+
assert issubclass(k_type, str)
|
|
95
|
+
data = response.json()
|
|
96
|
+
assert isinstance(data, dict)
|
|
97
|
+
return {
|
|
98
|
+
k: from_json(v, v_type)
|
|
99
|
+
for k, v in data.items()
|
|
100
|
+
} # type: ignore
|
|
101
|
+
|
|
102
|
+
# Case 2: response is a list of pydantic data classes
|
|
103
|
+
if get_origin(response_type) is list:
|
|
104
|
+
list_inner_type = get_args(response_type)[0]
|
|
105
|
+
result = _parse_dataclass_list(list_inner_type, response)
|
|
106
|
+
return cast(ResponseT, result)
|
|
107
|
+
|
|
108
|
+
# Case 3: response is pydantic data classes
|
|
109
|
+
try:
|
|
110
|
+
return from_json(response.text, response_type)
|
|
111
|
+
except Exception:
|
|
112
|
+
logger.error(f"Unable to parse response {response.text}")
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parse_id_response(response: requests.Response) -> str:
|
|
117
|
+
return parse_response(Dict[str, str], response)['id']
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def parse_patch_response(response: requests.Response) -> bool:
|
|
121
|
+
"""Parse PATCH response to indicate whether the resource was modified."""
|
|
122
|
+
return parse_response(Dict[str, bool], response)['resource_updated']
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def raise_on_error(response: requests.Response) -> None:
|
|
126
|
+
r"""Raises an :class:`~kumoai.exceptions.HTTPException` if a response does
|
|
127
|
+
not return with an OK status code.
|
|
128
|
+
"""
|
|
129
|
+
if not response.ok:
|
|
130
|
+
raise HTTPException(response.status_code, response.text)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Kumo SDK Code Generation Utility.
|
|
2
|
+
|
|
3
|
+
Generates Python SDK code from Kumo UI entities.
|
|
4
|
+
Supports both ID-based loading and JSON-based loading.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .generate import generate_code
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
CodegenError,
|
|
10
|
+
CyclicDependencyError,
|
|
11
|
+
UnsupportedEntityError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"generate_code",
|
|
16
|
+
"CodegenError",
|
|
17
|
+
"CyclicDependencyError",
|
|
18
|
+
"UnsupportedEntityError",
|
|
19
|
+
]
|