UncountablePythonSDK 0.0.126__py3-none-any.whl → 0.0.142.dev0__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.
Potentially problematic release.
This version of UncountablePythonSDK might be problematic. Click here for more details.
- docs/requirements.txt +1 -1
- examples/integration-server/jobs/materials_auto/example_cron.py +1 -1
- examples/integration-server/jobs/materials_auto/example_instrument.py +68 -38
- examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
- examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
- examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +57 -16
- examples/integration-server/jobs/materials_auto/profile.yaml +18 -0
- examples/integration-server/pyproject.toml +4 -4
- pkgs/argument_parser/argument_parser.py +20 -1
- pkgs/serialization_util/serialization_helpers.py +3 -1
- pkgs/type_spec/builder.py +43 -13
- pkgs/type_spec/builder_types.py +9 -0
- pkgs/type_spec/cross_output_links.py +2 -10
- pkgs/type_spec/emit_open_api.py +0 -12
- pkgs/type_spec/emit_python.py +72 -11
- pkgs/type_spec/emit_typescript.py +2 -2
- pkgs/type_spec/emit_typescript_util.py +28 -6
- pkgs/type_spec/load_types.py +1 -1
- pkgs/type_spec/parts/base.ts.prepart +3 -0
- pkgs/type_spec/type_info/emit_type_info.py +27 -3
- pkgs/type_spec/value_spec/__main__.py +2 -2
- uncountable/core/client.py +10 -3
- uncountable/integration/cli.py +89 -2
- uncountable/integration/executors/executors.py +1 -2
- uncountable/integration/executors/generic_upload_executor.py +1 -1
- uncountable/integration/job.py +3 -3
- uncountable/integration/queue_runner/command_server/__init__.py +4 -0
- uncountable/integration/queue_runner/command_server/command_client.py +63 -0
- uncountable/integration/queue_runner/command_server/command_server.py +77 -5
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +33 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +27 -13
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +53 -1
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +135 -0
- uncountable/integration/queue_runner/command_server/types.py +44 -1
- uncountable/integration/queue_runner/datastore/datastore_sqlite.py +132 -8
- uncountable/integration/queue_runner/datastore/interface.py +3 -0
- uncountable/integration/queue_runner/datastore/model.py +8 -1
- uncountable/integration/queue_runner/job_scheduler.py +78 -3
- uncountable/integration/queue_runner/types.py +2 -0
- uncountable/integration/queue_runner/worker.py +28 -26
- uncountable/integration/scheduler.py +64 -13
- uncountable/integration/server.py +36 -6
- uncountable/integration/telemetry.py +120 -7
- uncountable/integration/webhook_server/entrypoint.py +2 -0
- uncountable/types/__init__.py +18 -0
- uncountable/types/api/entity/list_aggregate.py +79 -0
- uncountable/types/api/entity/list_entities.py +25 -0
- uncountable/types/api/entity/set_barcode.py +43 -0
- uncountable/types/api/entity/transition_entity_phase.py +2 -1
- uncountable/types/api/files/download_file.py +15 -1
- uncountable/types/api/integrations/push_notification.py +2 -0
- uncountable/types/api/integrations/register_sockets_token.py +41 -0
- uncountable/types/api/listing/__init__.py +1 -0
- uncountable/types/api/listing/fetch_listing.py +57 -0
- uncountable/types/api/notebooks/__init__.py +1 -0
- uncountable/types/api/notebooks/add_notebook_content.py +119 -0
- uncountable/types/api/outputs/get_output_organization.py +1 -1
- uncountable/types/api/recipes/edit_recipe_inputs.py +1 -1
- uncountable/types/api/recipes/get_recipes_data.py +29 -0
- uncountable/types/api/recipes/lock_recipes.py +2 -1
- uncountable/types/api/recipes/set_recipe_total.py +59 -0
- uncountable/types/api/recipes/unlock_recipes.py +2 -1
- uncountable/types/api/runsheet/export_default_runsheet.py +44 -0
- uncountable/types/api/uploader/complete_async_parse.py +4 -0
- uncountable/types/async_batch_processor.py +222 -0
- uncountable/types/async_batch_t.py +4 -0
- uncountable/types/client_base.py +367 -2
- uncountable/types/client_config.py +1 -0
- uncountable/types/client_config_t.py +10 -0
- uncountable/types/entity_t.py +3 -1
- uncountable/types/integration_server_t.py +2 -0
- uncountable/types/listing.py +46 -0
- uncountable/types/listing_t.py +533 -0
- uncountable/types/notices.py +8 -0
- uncountable/types/notices_t.py +37 -0
- uncountable/types/queued_job.py +1 -0
- uncountable/types/queued_job_t.py +9 -0
- uncountable/types/sockets.py +9 -0
- uncountable/types/sockets_t.py +99 -0
- uncountable/types/uploader_t.py +3 -2
- {uncountablepythonsdk-0.0.126.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/METADATA +4 -2
- {uncountablepythonsdk-0.0.126.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/RECORD +84 -68
- {uncountablepythonsdk-0.0.126.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/WHEEL +0 -0
- {uncountablepythonsdk-0.0.126.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/top_level.txt +0 -0
pkgs/type_spec/builder.py
CHANGED
|
@@ -13,7 +13,7 @@ from enum import Enum, StrEnum, auto
|
|
|
13
13
|
from typing import Any, Self
|
|
14
14
|
|
|
15
15
|
from . import util
|
|
16
|
-
from .
|
|
16
|
+
from .builder_types import CrossOutputPaths
|
|
17
17
|
from .non_discriminated_union_exceptions import NON_DISCRIMINATED_UNION_EXCEPTIONS
|
|
18
18
|
from .util import parse_type_str
|
|
19
19
|
|
|
@@ -308,6 +308,7 @@ class SpecTypeDefn(SpecType):
|
|
|
308
308
|
self._is_value_to_string = False
|
|
309
309
|
self._is_valid_parameter = True
|
|
310
310
|
self._is_dynamic_allowed = False
|
|
311
|
+
self._default_extant: PropertyExtant | None = None
|
|
311
312
|
self.ext_info: Any = None
|
|
312
313
|
|
|
313
314
|
def is_value_converted(self) -> bool:
|
|
@@ -340,6 +341,7 @@ class SpecTypeDefn(SpecType):
|
|
|
340
341
|
"ext_info",
|
|
341
342
|
"label",
|
|
342
343
|
"is_dynamic_allowed",
|
|
344
|
+
"default_extant",
|
|
343
345
|
]
|
|
344
346
|
+ extra_names,
|
|
345
347
|
)
|
|
@@ -351,6 +353,10 @@ class SpecTypeDefn(SpecType):
|
|
|
351
353
|
assert isinstance(is_dynamic_allowed, bool)
|
|
352
354
|
self._is_dynamic_allowed = is_dynamic_allowed
|
|
353
355
|
|
|
356
|
+
default_extant = data.get("default_extant")
|
|
357
|
+
if default_extant is not None:
|
|
358
|
+
self._default_extant = PropertyExtant(default_extant)
|
|
359
|
+
|
|
354
360
|
def _process_property(
|
|
355
361
|
self, builder: SpecBuilder, spec_name: str, data: RawDict
|
|
356
362
|
) -> SpecProperty:
|
|
@@ -369,18 +375,18 @@ class SpecTypeDefn(SpecType):
|
|
|
369
375
|
],
|
|
370
376
|
)
|
|
371
377
|
try:
|
|
372
|
-
|
|
378
|
+
extant_type_str = data.get("extant")
|
|
379
|
+
extant_type = (
|
|
380
|
+
PropertyExtant(extant_type_str) if extant_type_str is not None else None
|
|
381
|
+
)
|
|
382
|
+
extant = extant_type or self._default_extant
|
|
373
383
|
if spec_name.endswith("?"):
|
|
374
|
-
if
|
|
384
|
+
if extant is not None:
|
|
375
385
|
raise Exception("cannot specify extant with ?")
|
|
376
386
|
extant = PropertyExtant.optional
|
|
377
387
|
name = spec_name[:-1]
|
|
378
388
|
else:
|
|
379
|
-
extant =
|
|
380
|
-
PropertyExtant.required
|
|
381
|
-
if extant_type is None
|
|
382
|
-
else PropertyExtant(extant_type)
|
|
383
|
-
)
|
|
389
|
+
extant = extant or PropertyExtant.required
|
|
384
390
|
name = spec_name
|
|
385
391
|
|
|
386
392
|
property_name_case = self.name_case
|
|
@@ -417,7 +423,16 @@ class SpecTypeDefn(SpecType):
|
|
|
417
423
|
parse_require = False
|
|
418
424
|
literal = unwrap_literal_type(ptype)
|
|
419
425
|
if literal is not None:
|
|
420
|
-
|
|
426
|
+
if isinstance(
|
|
427
|
+
literal.value_type, SpecTypeDefnStringEnum
|
|
428
|
+
) and isinstance(literal.value, str):
|
|
429
|
+
resolved_value = literal.value_type.values.get(literal.value)
|
|
430
|
+
assert resolved_value is not None, (
|
|
431
|
+
f"Value {literal.value} not found in enum"
|
|
432
|
+
)
|
|
433
|
+
default = resolved_value.value
|
|
434
|
+
else:
|
|
435
|
+
default = literal.value
|
|
421
436
|
has_default = True
|
|
422
437
|
parse_require = True
|
|
423
438
|
|
|
@@ -1095,7 +1110,7 @@ def _parse_const(
|
|
|
1095
1110
|
elif const_type.defn_type.name == BaseTypeName.s_dict:
|
|
1096
1111
|
assert isinstance(value, dict)
|
|
1097
1112
|
builder.ensure(
|
|
1098
|
-
len(const_type.parameters) == 2, "constant-dict-expects-
|
|
1113
|
+
len(const_type.parameters) == 2, "constant-dict-expects-two-types"
|
|
1099
1114
|
)
|
|
1100
1115
|
key_type = const_type.parameters[0]
|
|
1101
1116
|
value_type = const_type.parameters[1]
|
|
@@ -1144,6 +1159,11 @@ def _parse_const(
|
|
|
1144
1159
|
)
|
|
1145
1160
|
return value
|
|
1146
1161
|
|
|
1162
|
+
if not const_type.is_base:
|
|
1163
|
+
# IMPROVE: validate the object type properties before emission stage
|
|
1164
|
+
builder.ensure(isinstance(value, dict), "invalid value for object constant")
|
|
1165
|
+
return value
|
|
1166
|
+
|
|
1147
1167
|
raise Exception("unsupported-const-scalar-type", const_type)
|
|
1148
1168
|
|
|
1149
1169
|
|
|
@@ -1265,7 +1285,8 @@ class SpecNamespace:
|
|
|
1265
1285
|
|
|
1266
1286
|
assert util.is_valid_type_name(name), f"{name} is not a valid type name"
|
|
1267
1287
|
assert name not in self.types, f"{name} is duplicate"
|
|
1268
|
-
defn_type = defn
|
|
1288
|
+
defn_type = defn.get("type")
|
|
1289
|
+
assert isinstance(defn_type, str), f"{name} requires a string type"
|
|
1269
1290
|
spec_type: SpecTypeDefn
|
|
1270
1291
|
if defn_type == DefnTypeName.s_alias:
|
|
1271
1292
|
spec_type = SpecTypeDefnAlias(self, name)
|
|
@@ -1392,9 +1413,13 @@ class SpecBuilder:
|
|
|
1392
1413
|
self.emit_id_source_enums: set[SpecTypeDefnStringEnum] = set()
|
|
1393
1414
|
|
|
1394
1415
|
this_dir = os.path.dirname(os.path.realpath(__file__))
|
|
1395
|
-
with open(
|
|
1416
|
+
with open(
|
|
1417
|
+
f"{this_dir}/parts/base.py.prepart", encoding="utf-8"
|
|
1418
|
+
) as py_base_part:
|
|
1396
1419
|
self.preparts["python"][base_namespace_name] = py_base_part.read()
|
|
1397
|
-
with open(
|
|
1420
|
+
with open(
|
|
1421
|
+
f"{this_dir}/parts/base.ts.prepart", encoding="utf-8"
|
|
1422
|
+
) as ts_base_part:
|
|
1398
1423
|
self.preparts["typescript"][base_namespace_name] = ts_base_part.read()
|
|
1399
1424
|
|
|
1400
1425
|
base_namespace.types["ObjectId"] = SpecTypeDefnObject(
|
|
@@ -1571,6 +1596,11 @@ class SpecBuilder:
|
|
|
1571
1596
|
f"'examples' in example files are expected to be a list, endpoint_path={path_details.resolved_path}"
|
|
1572
1597
|
)
|
|
1573
1598
|
for example in examples_data:
|
|
1599
|
+
if not isinstance(example, dict):
|
|
1600
|
+
raise Exception(
|
|
1601
|
+
f"each example in example file is expected to be a dict, endpoint_path={path_details.resolved_path}"
|
|
1602
|
+
)
|
|
1603
|
+
|
|
1574
1604
|
arguments = example["arguments"]
|
|
1575
1605
|
data_example = example["data"]
|
|
1576
1606
|
if not isinstance(arguments, dict) or not isinstance(data_example, dict):
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from dataclasses import dataclass
|
|
5
4
|
|
|
6
|
-
from
|
|
5
|
+
from . import builder
|
|
6
|
+
from .builder_types import CrossOutputPaths
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def get_python_stub_file_path(
|
|
@@ -17,14 +17,6 @@ def get_python_stub_file_path(
|
|
|
17
17
|
return api_stub_file
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
@dataclass(kw_only=True, frozen=True)
|
|
21
|
-
class CrossOutputPaths:
|
|
22
|
-
python_types_output: str
|
|
23
|
-
typescript_types_output: str
|
|
24
|
-
typescript_routes_output_by_endpoint: dict[str, str]
|
|
25
|
-
typespec_files_input: list[str]
|
|
26
|
-
|
|
27
|
-
|
|
28
20
|
def get_python_api_file_path(
|
|
29
21
|
cross_output_paths: CrossOutputPaths,
|
|
30
22
|
namespace: builder.SpecNamespace,
|
pkgs/type_spec/emit_open_api.py
CHANGED
|
@@ -610,18 +610,6 @@ def _emit_type(
|
|
|
610
610
|
ctx.types[stype.name] = final_type
|
|
611
611
|
|
|
612
612
|
|
|
613
|
-
def _emit_constant(ctx: EmitOpenAPIContext, sconst: builder.SpecConstant) -> None:
|
|
614
|
-
if sconst.value_type.is_base_type(builder.BaseTypeName.s_string):
|
|
615
|
-
value = util.encode_common_string(cast(str, sconst.value))
|
|
616
|
-
elif sconst.value_type.is_base_type(builder.BaseTypeName.s_integer):
|
|
617
|
-
value = str(sconst.value)
|
|
618
|
-
else:
|
|
619
|
-
raise Exception("invalid constant type", sconst.name)
|
|
620
|
-
|
|
621
|
-
const_name = sconst.name.upper()
|
|
622
|
-
print("_emit_constant", value, const_name)
|
|
623
|
-
|
|
624
|
-
|
|
625
613
|
def _emit_endpoint(
|
|
626
614
|
gctx: EmitOpenAPIGlobalContext,
|
|
627
615
|
ctx: EmitOpenAPIContext,
|
pkgs/type_spec/emit_python.py
CHANGED
|
@@ -35,6 +35,11 @@ QUEUED_BATCH_REQUEST_STYPE = builder.SpecTypeDefnObject(
|
|
|
35
35
|
namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="QueuedAsyncBatchRequest"
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
+
CLIENT_CONFIG_TYPE_NAMESPACE = builder.SpecNamespace(name="client_config")
|
|
39
|
+
REQUEST_OPTIONS_STYPE = builder.SpecTypeDefnObject(
|
|
40
|
+
namespace=CLIENT_CONFIG_TYPE_NAMESPACE, name="RequestOptions"
|
|
41
|
+
)
|
|
42
|
+
|
|
38
43
|
|
|
39
44
|
@dataclasses.dataclass(kw_only=True)
|
|
40
45
|
class TrackingContext:
|
|
@@ -117,26 +122,36 @@ def _check_type_match(stype: builder.SpecType, value: Any) -> bool:
|
|
|
117
122
|
raise Exception("invalid type", stype, value)
|
|
118
123
|
|
|
119
124
|
|
|
120
|
-
def _emit_value(
|
|
125
|
+
def _emit_value(
|
|
126
|
+
ctx: TrackingContext, stype: builder.SpecType, value: Any, indent: int = 0
|
|
127
|
+
) -> str:
|
|
121
128
|
literal = builder.unwrap_literal_type(stype)
|
|
122
129
|
if literal is not None:
|
|
123
130
|
return _emit_value(ctx, literal.value_type, literal.value)
|
|
124
131
|
|
|
125
132
|
if stype.is_base_type(builder.BaseTypeName.s_string):
|
|
126
|
-
assert isinstance(value, str)
|
|
133
|
+
assert isinstance(value, str), (
|
|
134
|
+
f"Expected str value for {stype.name} but got {value}"
|
|
135
|
+
)
|
|
127
136
|
return util.encode_common_string(value)
|
|
128
137
|
elif stype.is_base_type(builder.BaseTypeName.s_integer):
|
|
129
|
-
assert isinstance(value, int)
|
|
138
|
+
assert isinstance(value, int), (
|
|
139
|
+
f"Expected int value for {stype.name} but got {value}"
|
|
140
|
+
)
|
|
130
141
|
return str(value)
|
|
131
142
|
elif stype.is_base_type(builder.BaseTypeName.s_boolean):
|
|
132
|
-
assert isinstance(value, bool)
|
|
143
|
+
assert isinstance(value, bool), (
|
|
144
|
+
f"Expected bool value for {stype.name} but got {value}"
|
|
145
|
+
)
|
|
133
146
|
return "True" if value else "False"
|
|
134
147
|
elif stype.is_base_type(builder.BaseTypeName.s_decimal) or stype.is_base_type(
|
|
135
148
|
builder.BaseTypeName.s_lossy_decimal
|
|
136
149
|
):
|
|
137
150
|
# Note that decimal requires the `!decimal 123.12` style notation in the YAML
|
|
138
151
|
# file since PyYaml parses numbers as float, unfortuantely
|
|
139
|
-
assert isinstance(value, (Decimal, int))
|
|
152
|
+
assert isinstance(value, (Decimal, int)), (
|
|
153
|
+
f"Expected decimal value for {stype.name} but got {value} (type: {type(value)})"
|
|
154
|
+
)
|
|
140
155
|
if isinstance(value, int):
|
|
141
156
|
# skip quotes for integers
|
|
142
157
|
return f"Decimal({value})"
|
|
@@ -151,14 +166,14 @@ def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> st
|
|
|
151
166
|
key_type = stype.parameters[0]
|
|
152
167
|
value_type = stype.parameters[1]
|
|
153
168
|
return (
|
|
154
|
-
"{\n
|
|
155
|
-
+ ",\n
|
|
169
|
+
f"{{\n{INDENT * (indent + 1)}"
|
|
170
|
+
+ f",\n{INDENT * (indent + 1)}".join(
|
|
156
171
|
_emit_value(ctx, key_type, dkey)
|
|
157
172
|
+ ": "
|
|
158
|
-
+ _emit_value(ctx, value_type, dvalue)
|
|
173
|
+
+ _emit_value(ctx, value_type, dvalue, indent=indent + 1)
|
|
159
174
|
for dkey, dvalue in value.items()
|
|
160
175
|
)
|
|
161
|
-
+ "\n}"
|
|
176
|
+
+ f"\n{INDENT * indent}}}"
|
|
162
177
|
)
|
|
163
178
|
|
|
164
179
|
if stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
|
|
@@ -184,6 +199,34 @@ def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> st
|
|
|
184
199
|
return f"{refer_to(ctx, stype)}.{_resolve_enum_name(value, stype.name_case)}"
|
|
185
200
|
elif isinstance(stype, builder.SpecTypeDefnAlias):
|
|
186
201
|
return _emit_value(ctx, stype.alias, value)
|
|
202
|
+
elif isinstance(stype, builder.SpecTypeDefnObject):
|
|
203
|
+
assert isinstance(value, dict), (
|
|
204
|
+
f"Expected dict value for {stype.name} but got {value}"
|
|
205
|
+
)
|
|
206
|
+
if not stype.is_hashable:
|
|
207
|
+
raise Exception("invalid constant object type, non-hashable", value, stype)
|
|
208
|
+
obj_out = f"{refer_to(ctx, stype)}("
|
|
209
|
+
emitted_fields: set[str] = set()
|
|
210
|
+
for prop_name, prop in (stype.properties or {}).items():
|
|
211
|
+
if prop_name not in value:
|
|
212
|
+
continue
|
|
213
|
+
else:
|
|
214
|
+
value_to_emit = value[prop_name]
|
|
215
|
+
emitted_fields.add(prop_name)
|
|
216
|
+
py_name = python_field_name(prop.name, prop.name_case)
|
|
217
|
+
obj_out += f"\n{INDENT * (indent + 1)}{py_name}={_emit_value(ctx, prop.spec_type, value_to_emit, indent=indent + 1)},"
|
|
218
|
+
whitespace = f"\n{INDENT * indent}" if len(emitted_fields) > 0 else ""
|
|
219
|
+
obj_out += f"{whitespace})"
|
|
220
|
+
|
|
221
|
+
if emitted_fields != set(value.keys()):
|
|
222
|
+
raise Exception(
|
|
223
|
+
"invalid object type, extra fields found:",
|
|
224
|
+
value,
|
|
225
|
+
stype,
|
|
226
|
+
set(value.keys()) - emitted_fields,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return obj_out
|
|
187
230
|
|
|
188
231
|
raise Exception("invalid constant type", value, stype)
|
|
189
232
|
|
|
@@ -471,6 +514,19 @@ def _emit_endpoint_invocation_function_signature(
|
|
|
471
514
|
else []
|
|
472
515
|
) + (extra_params if extra_params is not None else [])
|
|
473
516
|
|
|
517
|
+
request_options_property = builder.SpecProperty(
|
|
518
|
+
name="_request_options",
|
|
519
|
+
label="_request_options",
|
|
520
|
+
spec_type=REQUEST_OPTIONS_STYPE,
|
|
521
|
+
extant=builder.PropertyExtant.optional,
|
|
522
|
+
convert_value=builder.PropertyConvertValue.auto,
|
|
523
|
+
name_case=builder.NameCase.convert,
|
|
524
|
+
default=None,
|
|
525
|
+
has_default=True,
|
|
526
|
+
desc=None,
|
|
527
|
+
)
|
|
528
|
+
all_arguments.append(request_options_property)
|
|
529
|
+
|
|
474
530
|
# All endpoints share a function name
|
|
475
531
|
function = endpoint.path_per_api_endpoint[endpoint.default_endpoint_key].function
|
|
476
532
|
assert function is not None
|
|
@@ -641,6 +697,7 @@ def _emit_endpoint_invocation_function(
|
|
|
641
697
|
method={refer_to(ctx=ctx, stype=endpoint_method_stype)},
|
|
642
698
|
endpoint={refer_to(ctx=ctx, stype=endpoint_path_stype)},
|
|
643
699
|
args=args,
|
|
700
|
+
request_options=_request_options,
|
|
644
701
|
)
|
|
645
702
|
return self.do_request(api_request=api_request, return_type={refer_to(ctx=ctx, stype=data_type)})"""
|
|
646
703
|
)
|
|
@@ -1396,7 +1453,7 @@ CLIENT_CLASS_IMPORTS = [
|
|
|
1396
1453
|
"import dataclasses",
|
|
1397
1454
|
]
|
|
1398
1455
|
ASYNC_BATCH_PROCESSOR_FILENAME = "async_batch_processor"
|
|
1399
|
-
|
|
1456
|
+
ASYNC_BATCH_PROCESSOR_BASE_IMPORTS = [
|
|
1400
1457
|
"import uuid",
|
|
1401
1458
|
"from abc import ABC, abstractmethod",
|
|
1402
1459
|
"from pkgs.serialization_util import serialize_for_api",
|
|
@@ -1434,8 +1491,11 @@ def _emit_async_batch_processor(
|
|
|
1434
1491
|
config=config,
|
|
1435
1492
|
)
|
|
1436
1493
|
|
|
1494
|
+
imports = ASYNC_BATCH_PROCESSOR_BASE_IMPORTS.copy()
|
|
1495
|
+
if ctx.use_dataclass:
|
|
1496
|
+
imports.append("import dataclasses")
|
|
1437
1497
|
async_batch_processor_out.write(
|
|
1438
|
-
f"""{LINE_BREAK.join(
|
|
1498
|
+
f"""{LINE_BREAK.join(imports)}
|
|
1439
1499
|
|
|
1440
1500
|
|
|
1441
1501
|
class AsyncBatchProcessorBase(ABC):
|
|
@@ -1498,6 +1558,7 @@ class APIRequest:
|
|
|
1498
1558
|
method: str
|
|
1499
1559
|
endpoint: str
|
|
1500
1560
|
args: typing.Any
|
|
1561
|
+
request_options: {refer_to(ctx=ctx, stype=REQUEST_OPTIONS_STYPE)} | None = None
|
|
1501
1562
|
|
|
1502
1563
|
|
|
1503
1564
|
class ClientMethods(ABC):
|
|
@@ -285,14 +285,14 @@ export const apiCall = {wrap_call}(
|
|
|
285
285
|
index_path = f"{config.endpoint_to_routes_output[endpoint.default_endpoint_key]}/{'/'.join(namespace.path[0:-1])}/index.tsx"
|
|
286
286
|
api_name = f"Api{ts_type_name(namespace.path[0 - 1])}"
|
|
287
287
|
if os.path.exists(index_path):
|
|
288
|
-
with open(index_path) as index:
|
|
288
|
+
with open(index_path, encoding="utf-8") as index:
|
|
289
289
|
index_data = index.read()
|
|
290
290
|
need_index = index_data.find(api_name) == -1
|
|
291
291
|
else:
|
|
292
292
|
need_index = True
|
|
293
293
|
|
|
294
294
|
if need_index:
|
|
295
|
-
with open(index_path, "a") as index:
|
|
295
|
+
with open(index_path, "a", encoding="utf-8") as index:
|
|
296
296
|
print(f"Updated API Index {index_path}")
|
|
297
297
|
index.write(f'import * as {api_name} from "./{namespace.path[-1]}"\n\n')
|
|
298
298
|
index.write(f"export {{ {api_name} }}\n")
|
|
@@ -3,7 +3,7 @@ import typing
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
|
|
5
5
|
from . import builder, util
|
|
6
|
-
from .
|
|
6
|
+
from .builder_types import CrossOutputPaths
|
|
7
7
|
|
|
8
8
|
INDENT = " "
|
|
9
9
|
|
|
@@ -51,7 +51,10 @@ def ts_name(name: str, name_case: builder.NameCase) -> str:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def emit_value_ts(
|
|
54
|
-
ctx: EmitTypescriptContext,
|
|
54
|
+
ctx: EmitTypescriptContext,
|
|
55
|
+
stype: builder.SpecType,
|
|
56
|
+
value: typing.Any,
|
|
57
|
+
indent: int = 0,
|
|
55
58
|
) -> str:
|
|
56
59
|
"""Mimics emit_python even if not all types are used in TypeScript yet"""
|
|
57
60
|
literal = builder.unwrap_literal_type(stype)
|
|
@@ -88,17 +91,17 @@ def emit_value_ts(
|
|
|
88
91
|
raise Exception("invalid dict keys -- dict keys must be string or enum")
|
|
89
92
|
|
|
90
93
|
return (
|
|
91
|
-
"{\n
|
|
92
|
-
+ ",\n
|
|
94
|
+
f"{{\n{INDENT * (indent + 1)}"
|
|
95
|
+
+ f",\n{INDENT * (indent + 1)}".join(
|
|
93
96
|
(
|
|
94
97
|
f"[{emit_value_ts(ctx, key_type, dkey)}]: "
|
|
95
98
|
if not key_type.is_base_type(builder.BaseTypeName.s_string)
|
|
96
99
|
else f"{dkey}: "
|
|
97
100
|
)
|
|
98
|
-
+ emit_value_ts(ctx, value_type, dvalue)
|
|
101
|
+
+ emit_value_ts(ctx, value_type, dvalue, indent=indent + 1)
|
|
99
102
|
for dkey, dvalue in value.items()
|
|
100
103
|
)
|
|
101
|
-
+ "\n}"
|
|
104
|
+
+ f"\n{INDENT * (indent)}}}"
|
|
102
105
|
)
|
|
103
106
|
|
|
104
107
|
if stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
|
|
@@ -109,6 +112,25 @@ def emit_value_ts(
|
|
|
109
112
|
|
|
110
113
|
elif isinstance(stype, builder.SpecTypeDefnStringEnum):
|
|
111
114
|
return f"{refer_to(ctx, stype)}.{ts_enum_name(value, stype.name_case)}"
|
|
115
|
+
elif isinstance(stype, builder.SpecTypeDefnObject):
|
|
116
|
+
assert isinstance(value, dict), (
|
|
117
|
+
f"Expected dict value for {stype.name} but got {value}"
|
|
118
|
+
)
|
|
119
|
+
obj_out = "{"
|
|
120
|
+
did_emit = False
|
|
121
|
+
for prop_name, prop in (stype.properties or {}).items():
|
|
122
|
+
if prop_name not in value and prop.has_default:
|
|
123
|
+
value_to_emit = prop.default
|
|
124
|
+
elif prop_name not in value:
|
|
125
|
+
continue
|
|
126
|
+
else:
|
|
127
|
+
value_to_emit = value[prop_name]
|
|
128
|
+
did_emit = True
|
|
129
|
+
typescript_name = ts_name(prop.name, prop.name_case)
|
|
130
|
+
obj_out += f"\n{INDENT * (indent + 1)}{typescript_name}: {emit_value_ts(ctx, prop.spec_type, value_to_emit, indent=indent + 1)},"
|
|
131
|
+
whitespace = f"\n{INDENT * indent}" if did_emit else ""
|
|
132
|
+
obj_out += f"{whitespace}}} as const"
|
|
133
|
+
return obj_out
|
|
112
134
|
|
|
113
135
|
raise Exception("invalid constant type", value, stype, type(stype))
|
|
114
136
|
|
pkgs/type_spec/load_types.py
CHANGED
|
@@ -7,8 +7,8 @@ from shelljob import fs
|
|
|
7
7
|
from pkgs.serialization import yaml
|
|
8
8
|
|
|
9
9
|
from .builder import SpecBuilder
|
|
10
|
+
from .builder_types import CrossOutputPaths
|
|
10
11
|
from .config import Config
|
|
11
|
-
from .cross_output_links import CrossOutputPaths
|
|
12
12
|
|
|
13
13
|
ext_map = {
|
|
14
14
|
".ts": "typescript",
|
|
@@ -29,3 +29,6 @@ export const IOJsonValue: IO.Type<JsonValue> = IO.recursion('JsonValue', () =>
|
|
|
29
29
|
export interface nominal<T> {
|
|
30
30
|
"nominal structural brand": T
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
// Ids matching a strict integer number are converted to integers
|
|
34
|
+
export const ID_REGEX = /^-?[1-9][0-9]{0,20}$/
|
|
@@ -41,10 +41,23 @@ def type_path_of(stype: builder.SpecType) -> object: # NamePath
|
|
|
41
41
|
parts: list[object] = ["$literal"]
|
|
42
42
|
for parameter in stype.parameters:
|
|
43
43
|
assert isinstance(parameter, builder.SpecTypeLiteralWrapper)
|
|
44
|
+
emit_value = parameter.value
|
|
45
|
+
if isinstance(parameter.value_type, builder.SpecTypeDefnObject):
|
|
46
|
+
emit_value = parameter.value
|
|
47
|
+
assert isinstance(emit_value, (str, bool)), (
|
|
48
|
+
f"invalid-literal-value:{emit_value}"
|
|
49
|
+
)
|
|
50
|
+
elif isinstance(parameter.value_type, builder.SpecTypeDefnStringEnum):
|
|
51
|
+
key = parameter.value
|
|
52
|
+
assert isinstance(key, str)
|
|
53
|
+
emit_value = parameter.value_type.values[key].value
|
|
54
|
+
else:
|
|
55
|
+
raise Exception("unhandled-literal-type")
|
|
56
|
+
|
|
44
57
|
# This allows expansion to enum literal values later
|
|
45
58
|
parts.append([
|
|
46
59
|
"$value",
|
|
47
|
-
|
|
60
|
+
emit_value,
|
|
48
61
|
type_path_of(parameter.value_type),
|
|
49
62
|
])
|
|
50
63
|
return parts
|
|
@@ -158,9 +171,16 @@ class MapTypeAlias(MapTypeBase):
|
|
|
158
171
|
discriminator: str | None
|
|
159
172
|
|
|
160
173
|
|
|
174
|
+
@dataclasses.dataclass
|
|
175
|
+
class StringEnumValue:
|
|
176
|
+
value: str
|
|
177
|
+
label: str
|
|
178
|
+
deprecated: bool = False
|
|
179
|
+
|
|
180
|
+
|
|
161
181
|
@dataclasses.dataclass
|
|
162
182
|
class MapStringEnum(MapTypeBase):
|
|
163
|
-
values: dict[str,
|
|
183
|
+
values: dict[str, StringEnumValue]
|
|
164
184
|
|
|
165
185
|
|
|
166
186
|
MapType = MapTypeObject | MapTypeAlias | MapStringEnum
|
|
@@ -423,7 +443,11 @@ def _build_map_type(
|
|
|
423
443
|
# IMPROVE: We probably want the label here, but this requires a change
|
|
424
444
|
# to the front-end type-info and form code to handle
|
|
425
445
|
values={
|
|
426
|
-
entry.value: (
|
|
446
|
+
entry.value: StringEnumValue(
|
|
447
|
+
value=entry.value,
|
|
448
|
+
label=entry.label or entry.name,
|
|
449
|
+
deprecated=entry.deprecated,
|
|
450
|
+
)
|
|
427
451
|
for entry in stype.values.values()
|
|
428
452
|
},
|
|
429
453
|
)
|
|
@@ -20,7 +20,7 @@ The accepted argument type must accept "None", it is not implied.
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
import sys
|
|
23
|
-
from typing import TypeVar, cast
|
|
23
|
+
from typing import Match, Pattern, TypeVar, cast
|
|
24
24
|
|
|
25
25
|
import regex as re
|
|
26
26
|
|
|
@@ -56,7 +56,7 @@ class Source:
|
|
|
56
56
|
def has_more(self) -> bool:
|
|
57
57
|
return self._at < len(self._text)
|
|
58
58
|
|
|
59
|
-
def match(self, expression:
|
|
59
|
+
def match(self, expression: Pattern[str]) -> Match[str] | None:
|
|
60
60
|
self.skip_space()
|
|
61
61
|
m = expression.match(self._text, self._at)
|
|
62
62
|
if m is not None:
|
uncountable/core/client.py
CHANGED
|
@@ -226,13 +226,15 @@ class Client(ClientMethods):
|
|
|
226
226
|
except JSONDecodeError as e:
|
|
227
227
|
raise SDKError("unable to process response", request_id=request_id) from e
|
|
228
228
|
|
|
229
|
-
def _send_request(
|
|
229
|
+
def _send_request(
|
|
230
|
+
self, request: requests.Request, *, timeout: float | None = None
|
|
231
|
+
) -> requests.Response:
|
|
230
232
|
if self._cfg.extra_headers is not None:
|
|
231
233
|
request.headers = {**request.headers, **self._cfg.extra_headers}
|
|
232
234
|
if self._cfg.transform_request is not None:
|
|
233
235
|
request = self._cfg.transform_request(request)
|
|
234
236
|
prepared_request = request.prepare()
|
|
235
|
-
response = self._session.send(prepared_request)
|
|
237
|
+
response = self._session.send(prepared_request, timeout=timeout)
|
|
236
238
|
return response
|
|
237
239
|
|
|
238
240
|
def do_request(self, *, api_request: APIRequest, return_type: type[DT]) -> DT:
|
|
@@ -257,7 +259,12 @@ class Client(ClientMethods):
|
|
|
257
259
|
with push_scope_optional(self._cfg.logger, "api_call", attributes=attributes):
|
|
258
260
|
if self._cfg.logger is not None:
|
|
259
261
|
self._cfg.logger.log_info(api_request.endpoint, attributes=attributes)
|
|
260
|
-
|
|
262
|
+
timeout = (
|
|
263
|
+
api_request.request_options.timeout_secs
|
|
264
|
+
if api_request.request_options is not None
|
|
265
|
+
else None
|
|
266
|
+
)
|
|
267
|
+
response = self._send_request(request, timeout=timeout)
|
|
261
268
|
response_data = self._get_response_json(response, request_id=request_id)
|
|
262
269
|
cached_parser = self._get_cached_parser(return_type)
|
|
263
270
|
try:
|