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.

Files changed (84) hide show
  1. docs/requirements.txt +1 -1
  2. examples/integration-server/jobs/materials_auto/example_cron.py +1 -1
  3. examples/integration-server/jobs/materials_auto/example_instrument.py +68 -38
  4. examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
  5. examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
  6. examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +57 -16
  7. examples/integration-server/jobs/materials_auto/profile.yaml +18 -0
  8. examples/integration-server/pyproject.toml +4 -4
  9. pkgs/argument_parser/argument_parser.py +20 -1
  10. pkgs/serialization_util/serialization_helpers.py +3 -1
  11. pkgs/type_spec/builder.py +43 -13
  12. pkgs/type_spec/builder_types.py +9 -0
  13. pkgs/type_spec/cross_output_links.py +2 -10
  14. pkgs/type_spec/emit_open_api.py +0 -12
  15. pkgs/type_spec/emit_python.py +72 -11
  16. pkgs/type_spec/emit_typescript.py +2 -2
  17. pkgs/type_spec/emit_typescript_util.py +28 -6
  18. pkgs/type_spec/load_types.py +1 -1
  19. pkgs/type_spec/parts/base.ts.prepart +3 -0
  20. pkgs/type_spec/type_info/emit_type_info.py +27 -3
  21. pkgs/type_spec/value_spec/__main__.py +2 -2
  22. uncountable/core/client.py +10 -3
  23. uncountable/integration/cli.py +89 -2
  24. uncountable/integration/executors/executors.py +1 -2
  25. uncountable/integration/executors/generic_upload_executor.py +1 -1
  26. uncountable/integration/job.py +3 -3
  27. uncountable/integration/queue_runner/command_server/__init__.py +4 -0
  28. uncountable/integration/queue_runner/command_server/command_client.py +63 -0
  29. uncountable/integration/queue_runner/command_server/command_server.py +77 -5
  30. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +33 -0
  31. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +27 -13
  32. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +53 -1
  33. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +135 -0
  34. uncountable/integration/queue_runner/command_server/types.py +44 -1
  35. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +132 -8
  36. uncountable/integration/queue_runner/datastore/interface.py +3 -0
  37. uncountable/integration/queue_runner/datastore/model.py +8 -1
  38. uncountable/integration/queue_runner/job_scheduler.py +78 -3
  39. uncountable/integration/queue_runner/types.py +2 -0
  40. uncountable/integration/queue_runner/worker.py +28 -26
  41. uncountable/integration/scheduler.py +64 -13
  42. uncountable/integration/server.py +36 -6
  43. uncountable/integration/telemetry.py +120 -7
  44. uncountable/integration/webhook_server/entrypoint.py +2 -0
  45. uncountable/types/__init__.py +18 -0
  46. uncountable/types/api/entity/list_aggregate.py +79 -0
  47. uncountable/types/api/entity/list_entities.py +25 -0
  48. uncountable/types/api/entity/set_barcode.py +43 -0
  49. uncountable/types/api/entity/transition_entity_phase.py +2 -1
  50. uncountable/types/api/files/download_file.py +15 -1
  51. uncountable/types/api/integrations/push_notification.py +2 -0
  52. uncountable/types/api/integrations/register_sockets_token.py +41 -0
  53. uncountable/types/api/listing/__init__.py +1 -0
  54. uncountable/types/api/listing/fetch_listing.py +57 -0
  55. uncountable/types/api/notebooks/__init__.py +1 -0
  56. uncountable/types/api/notebooks/add_notebook_content.py +119 -0
  57. uncountable/types/api/outputs/get_output_organization.py +1 -1
  58. uncountable/types/api/recipes/edit_recipe_inputs.py +1 -1
  59. uncountable/types/api/recipes/get_recipes_data.py +29 -0
  60. uncountable/types/api/recipes/lock_recipes.py +2 -1
  61. uncountable/types/api/recipes/set_recipe_total.py +59 -0
  62. uncountable/types/api/recipes/unlock_recipes.py +2 -1
  63. uncountable/types/api/runsheet/export_default_runsheet.py +44 -0
  64. uncountable/types/api/uploader/complete_async_parse.py +4 -0
  65. uncountable/types/async_batch_processor.py +222 -0
  66. uncountable/types/async_batch_t.py +4 -0
  67. uncountable/types/client_base.py +367 -2
  68. uncountable/types/client_config.py +1 -0
  69. uncountable/types/client_config_t.py +10 -0
  70. uncountable/types/entity_t.py +3 -1
  71. uncountable/types/integration_server_t.py +2 -0
  72. uncountable/types/listing.py +46 -0
  73. uncountable/types/listing_t.py +533 -0
  74. uncountable/types/notices.py +8 -0
  75. uncountable/types/notices_t.py +37 -0
  76. uncountable/types/queued_job.py +1 -0
  77. uncountable/types/queued_job_t.py +9 -0
  78. uncountable/types/sockets.py +9 -0
  79. uncountable/types/sockets_t.py +99 -0
  80. uncountable/types/uploader_t.py +3 -2
  81. {uncountablepythonsdk-0.0.126.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/METADATA +4 -2
  82. {uncountablepythonsdk-0.0.126.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/RECORD +84 -68
  83. {uncountablepythonsdk-0.0.126.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/WHEEL +0 -0
  84. {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 .cross_output_links import CrossOutputPaths
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
- extant_type = data.get("extant")
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 extant_type is not None:
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
- default = literal.value
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-one-type"
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["type"]
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(f"{this_dir}/parts/base.py.prepart") as py_base_part:
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(f"{this_dir}/parts/base.ts.prepart") as ts_base_part:
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):
@@ -0,0 +1,9 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(kw_only=True, frozen=True)
5
+ class CrossOutputPaths:
6
+ python_types_output: str
7
+ typescript_types_output: str
8
+ typescript_routes_output_by_endpoint: dict[str, str]
9
+ typespec_files_input: list[str]
@@ -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 pkgs.type_spec import builder
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,
@@ -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,
@@ -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(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> str:
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 ".join(
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
- ASYNC_BATCH_PROCESSOR_IMPORTS = [
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(ASYNC_BATCH_PROCESSOR_IMPORTS)}
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 .cross_output_links import CrossOutputPaths
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, stype: builder.SpecType, value: typing.Any
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\t"
92
- + ",\n\t".join(
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
 
@@ -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
- parameter.value,
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, 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: (entry.label or entry.name)
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: re.Pattern) -> re.Match | None:
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:
@@ -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(self, request: requests.Request) -> requests.Response:
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
- response = self._send_request(request)
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: