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.

Files changed (122) hide show
  1. kumoai/__init__.py +300 -0
  2. kumoai/_logging.py +29 -0
  3. kumoai/_singleton.py +25 -0
  4. kumoai/_version.py +1 -0
  5. kumoai/artifact_export/__init__.py +9 -0
  6. kumoai/artifact_export/config.py +209 -0
  7. kumoai/artifact_export/job.py +108 -0
  8. kumoai/client/__init__.py +5 -0
  9. kumoai/client/client.py +223 -0
  10. kumoai/client/connector.py +110 -0
  11. kumoai/client/endpoints.py +150 -0
  12. kumoai/client/graph.py +120 -0
  13. kumoai/client/jobs.py +471 -0
  14. kumoai/client/online.py +78 -0
  15. kumoai/client/pquery.py +207 -0
  16. kumoai/client/rfm.py +112 -0
  17. kumoai/client/source_table.py +53 -0
  18. kumoai/client/table.py +101 -0
  19. kumoai/client/utils.py +130 -0
  20. kumoai/codegen/__init__.py +19 -0
  21. kumoai/codegen/cli.py +100 -0
  22. kumoai/codegen/context.py +16 -0
  23. kumoai/codegen/edits.py +473 -0
  24. kumoai/codegen/exceptions.py +10 -0
  25. kumoai/codegen/generate.py +222 -0
  26. kumoai/codegen/handlers/__init__.py +4 -0
  27. kumoai/codegen/handlers/connector.py +118 -0
  28. kumoai/codegen/handlers/graph.py +71 -0
  29. kumoai/codegen/handlers/pquery.py +62 -0
  30. kumoai/codegen/handlers/table.py +109 -0
  31. kumoai/codegen/handlers/utils.py +42 -0
  32. kumoai/codegen/identity.py +114 -0
  33. kumoai/codegen/loader.py +93 -0
  34. kumoai/codegen/naming.py +94 -0
  35. kumoai/codegen/registry.py +121 -0
  36. kumoai/connector/__init__.py +31 -0
  37. kumoai/connector/base.py +153 -0
  38. kumoai/connector/bigquery_connector.py +200 -0
  39. kumoai/connector/databricks_connector.py +213 -0
  40. kumoai/connector/file_upload_connector.py +189 -0
  41. kumoai/connector/glue_connector.py +150 -0
  42. kumoai/connector/s3_connector.py +278 -0
  43. kumoai/connector/snowflake_connector.py +252 -0
  44. kumoai/connector/source_table.py +471 -0
  45. kumoai/connector/utils.py +1796 -0
  46. kumoai/databricks.py +14 -0
  47. kumoai/encoder/__init__.py +4 -0
  48. kumoai/exceptions.py +26 -0
  49. kumoai/experimental/__init__.py +0 -0
  50. kumoai/experimental/rfm/__init__.py +210 -0
  51. kumoai/experimental/rfm/authenticate.py +432 -0
  52. kumoai/experimental/rfm/backend/__init__.py +0 -0
  53. kumoai/experimental/rfm/backend/local/__init__.py +42 -0
  54. kumoai/experimental/rfm/backend/local/graph_store.py +297 -0
  55. kumoai/experimental/rfm/backend/local/sampler.py +312 -0
  56. kumoai/experimental/rfm/backend/local/table.py +113 -0
  57. kumoai/experimental/rfm/backend/snow/__init__.py +37 -0
  58. kumoai/experimental/rfm/backend/snow/sampler.py +297 -0
  59. kumoai/experimental/rfm/backend/snow/table.py +242 -0
  60. kumoai/experimental/rfm/backend/sqlite/__init__.py +32 -0
  61. kumoai/experimental/rfm/backend/sqlite/sampler.py +398 -0
  62. kumoai/experimental/rfm/backend/sqlite/table.py +184 -0
  63. kumoai/experimental/rfm/base/__init__.py +30 -0
  64. kumoai/experimental/rfm/base/column.py +152 -0
  65. kumoai/experimental/rfm/base/expression.py +44 -0
  66. kumoai/experimental/rfm/base/sampler.py +761 -0
  67. kumoai/experimental/rfm/base/source.py +19 -0
  68. kumoai/experimental/rfm/base/sql_sampler.py +143 -0
  69. kumoai/experimental/rfm/base/table.py +736 -0
  70. kumoai/experimental/rfm/graph.py +1237 -0
  71. kumoai/experimental/rfm/infer/__init__.py +19 -0
  72. kumoai/experimental/rfm/infer/categorical.py +40 -0
  73. kumoai/experimental/rfm/infer/dtype.py +82 -0
  74. kumoai/experimental/rfm/infer/id.py +46 -0
  75. kumoai/experimental/rfm/infer/multicategorical.py +48 -0
  76. kumoai/experimental/rfm/infer/pkey.py +128 -0
  77. kumoai/experimental/rfm/infer/stype.py +35 -0
  78. kumoai/experimental/rfm/infer/time_col.py +61 -0
  79. kumoai/experimental/rfm/infer/timestamp.py +41 -0
  80. kumoai/experimental/rfm/pquery/__init__.py +7 -0
  81. kumoai/experimental/rfm/pquery/executor.py +102 -0
  82. kumoai/experimental/rfm/pquery/pandas_executor.py +530 -0
  83. kumoai/experimental/rfm/relbench.py +76 -0
  84. kumoai/experimental/rfm/rfm.py +1184 -0
  85. kumoai/experimental/rfm/sagemaker.py +138 -0
  86. kumoai/experimental/rfm/task_table.py +231 -0
  87. kumoai/formatting.py +30 -0
  88. kumoai/futures.py +99 -0
  89. kumoai/graph/__init__.py +12 -0
  90. kumoai/graph/column.py +106 -0
  91. kumoai/graph/graph.py +948 -0
  92. kumoai/graph/table.py +838 -0
  93. kumoai/jobs.py +80 -0
  94. kumoai/kumolib.cpython-310-x86_64-linux-gnu.so +0 -0
  95. kumoai/mixin.py +28 -0
  96. kumoai/pquery/__init__.py +25 -0
  97. kumoai/pquery/prediction_table.py +287 -0
  98. kumoai/pquery/predictive_query.py +641 -0
  99. kumoai/pquery/training_table.py +424 -0
  100. kumoai/spcs.py +121 -0
  101. kumoai/testing/__init__.py +8 -0
  102. kumoai/testing/decorators.py +57 -0
  103. kumoai/testing/snow.py +50 -0
  104. kumoai/trainer/__init__.py +42 -0
  105. kumoai/trainer/baseline_trainer.py +93 -0
  106. kumoai/trainer/config.py +2 -0
  107. kumoai/trainer/distilled_trainer.py +175 -0
  108. kumoai/trainer/job.py +1192 -0
  109. kumoai/trainer/online_serving.py +258 -0
  110. kumoai/trainer/trainer.py +475 -0
  111. kumoai/trainer/util.py +103 -0
  112. kumoai/utils/__init__.py +11 -0
  113. kumoai/utils/datasets.py +83 -0
  114. kumoai/utils/display.py +51 -0
  115. kumoai/utils/forecasting.py +209 -0
  116. kumoai/utils/progress_logger.py +343 -0
  117. kumoai/utils/sql.py +3 -0
  118. kumoai-2.14.0.dev202601011731.dist-info/METADATA +71 -0
  119. kumoai-2.14.0.dev202601011731.dist-info/RECORD +122 -0
  120. kumoai-2.14.0.dev202601011731.dist-info/WHEEL +6 -0
  121. kumoai-2.14.0.dev202601011731.dist-info/licenses/LICENSE +9 -0
  122. kumoai-2.14.0.dev202601011731.dist-info/top_level.txt +1 -0
@@ -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
+ ]