UncountablePythonSDK 0.0.115__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 (119) hide show
  1. docs/conf.py +52 -5
  2. docs/index.md +107 -4
  3. docs/integration_examples/create_ingredient.md +43 -0
  4. docs/integration_examples/create_output.md +56 -0
  5. docs/integration_examples/index.md +6 -0
  6. docs/justfile +1 -1
  7. docs/requirements.txt +3 -2
  8. examples/basic_auth.py +7 -0
  9. examples/integration-server/jobs/materials_auto/example_cron.py +3 -0
  10. examples/integration-server/jobs/materials_auto/example_http.py +19 -7
  11. examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
  12. examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
  13. examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
  14. examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +57 -16
  15. examples/integration-server/jobs/materials_auto/profile.yaml +27 -0
  16. examples/integration-server/pyproject.toml +4 -4
  17. examples/oauth.py +7 -0
  18. pkgs/argument_parser/__init__.py +1 -0
  19. pkgs/argument_parser/_is_namedtuple.py +3 -0
  20. pkgs/argument_parser/argument_parser.py +22 -3
  21. pkgs/serialization_util/serialization_helpers.py +3 -1
  22. pkgs/type_spec/builder.py +66 -19
  23. pkgs/type_spec/builder_types.py +9 -0
  24. pkgs/type_spec/config.py +26 -5
  25. pkgs/type_spec/cross_output_links.py +10 -16
  26. pkgs/type_spec/emit_open_api.py +72 -22
  27. pkgs/type_spec/emit_open_api_util.py +1 -0
  28. pkgs/type_spec/emit_python.py +76 -12
  29. pkgs/type_spec/emit_typescript.py +48 -32
  30. pkgs/type_spec/emit_typescript_util.py +44 -6
  31. pkgs/type_spec/load_types.py +2 -2
  32. pkgs/type_spec/open_api_util.py +16 -1
  33. pkgs/type_spec/parts/base.ts.prepart +4 -0
  34. pkgs/type_spec/type_info/emit_type_info.py +37 -4
  35. pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +1 -0
  36. pkgs/type_spec/value_spec/__main__.py +2 -2
  37. pkgs/type_spec/value_spec/emit_python.py +6 -1
  38. uncountable/core/client.py +10 -3
  39. uncountable/integration/cli.py +175 -23
  40. uncountable/integration/executors/executors.py +1 -2
  41. uncountable/integration/executors/generic_upload_executor.py +1 -1
  42. uncountable/integration/http_server/types.py +3 -1
  43. uncountable/integration/job.py +35 -3
  44. uncountable/integration/queue_runner/command_server/__init__.py +4 -0
  45. uncountable/integration/queue_runner/command_server/command_client.py +89 -0
  46. uncountable/integration/queue_runner/command_server/command_server.py +117 -5
  47. uncountable/integration/queue_runner/command_server/constants.py +4 -0
  48. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +51 -0
  49. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +34 -11
  50. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +102 -1
  51. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +180 -0
  52. uncountable/integration/queue_runner/command_server/types.py +44 -1
  53. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +189 -8
  54. uncountable/integration/queue_runner/datastore/interface.py +13 -0
  55. uncountable/integration/queue_runner/datastore/model.py +8 -1
  56. uncountable/integration/queue_runner/job_scheduler.py +85 -21
  57. uncountable/integration/queue_runner/queue_runner.py +10 -2
  58. uncountable/integration/queue_runner/types.py +2 -0
  59. uncountable/integration/queue_runner/worker.py +28 -29
  60. uncountable/integration/scheduler.py +121 -23
  61. uncountable/integration/server.py +36 -6
  62. uncountable/integration/telemetry.py +129 -8
  63. uncountable/integration/webhook_server/entrypoint.py +2 -0
  64. uncountable/types/__init__.py +38 -0
  65. uncountable/types/api/entity/create_or_update_entity.py +1 -0
  66. uncountable/types/api/entity/export_entities.py +13 -0
  67. uncountable/types/api/entity/list_aggregate.py +79 -0
  68. uncountable/types/api/entity/list_entities.py +25 -0
  69. uncountable/types/api/entity/set_barcode.py +43 -0
  70. uncountable/types/api/entity/transition_entity_phase.py +2 -1
  71. uncountable/types/api/files/download_file.py +15 -1
  72. uncountable/types/api/integrations/__init__.py +1 -0
  73. uncountable/types/api/integrations/publish_realtime_data.py +41 -0
  74. uncountable/types/api/integrations/push_notification.py +49 -0
  75. uncountable/types/api/integrations/register_sockets_token.py +41 -0
  76. uncountable/types/api/listing/__init__.py +1 -0
  77. uncountable/types/api/listing/fetch_listing.py +57 -0
  78. uncountable/types/api/notebooks/__init__.py +1 -0
  79. uncountable/types/api/notebooks/add_notebook_content.py +119 -0
  80. uncountable/types/api/outputs/get_output_organization.py +173 -0
  81. uncountable/types/api/recipes/edit_recipe_inputs.py +1 -1
  82. uncountable/types/api/recipes/get_recipe_output_metadata.py +2 -2
  83. uncountable/types/api/recipes/get_recipes_data.py +29 -0
  84. uncountable/types/api/recipes/lock_recipes.py +2 -1
  85. uncountable/types/api/recipes/set_recipe_total.py +59 -0
  86. uncountable/types/api/recipes/unlock_recipes.py +2 -1
  87. uncountable/types/api/runsheet/export_default_runsheet.py +44 -0
  88. uncountable/types/api/uploader/complete_async_parse.py +46 -0
  89. uncountable/types/api/user/__init__.py +1 -0
  90. uncountable/types/api/user/get_current_user_info.py +40 -0
  91. uncountable/types/async_batch_processor.py +266 -0
  92. uncountable/types/async_batch_t.py +5 -0
  93. uncountable/types/client_base.py +432 -2
  94. uncountable/types/client_config.py +1 -0
  95. uncountable/types/client_config_t.py +10 -0
  96. uncountable/types/entity_t.py +9 -1
  97. uncountable/types/exports_t.py +1 -0
  98. uncountable/types/integration_server_t.py +2 -0
  99. uncountable/types/integration_session.py +10 -0
  100. uncountable/types/integration_session_t.py +60 -0
  101. uncountable/types/integrations.py +10 -0
  102. uncountable/types/integrations_t.py +62 -0
  103. uncountable/types/listing.py +46 -0
  104. uncountable/types/listing_t.py +533 -0
  105. uncountable/types/notices.py +8 -0
  106. uncountable/types/notices_t.py +37 -0
  107. uncountable/types/notifications.py +11 -0
  108. uncountable/types/notifications_t.py +74 -0
  109. uncountable/types/queued_job.py +2 -0
  110. uncountable/types/queued_job_t.py +20 -2
  111. uncountable/types/sockets.py +20 -0
  112. uncountable/types/sockets_t.py +169 -0
  113. uncountable/types/uploader.py +24 -0
  114. uncountable/types/uploader_t.py +222 -0
  115. {uncountablepythonsdk-0.0.115.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/METADATA +5 -2
  116. {uncountablepythonsdk-0.0.115.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/RECORD +118 -79
  117. docs/quickstart.md +0 -19
  118. {uncountablepythonsdk-0.0.115.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/WHEEL +0 -0
  119. {uncountablepythonsdk-0.0.115.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/top_level.txt +0 -0
@@ -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: 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,
@@ -35,10 +27,9 @@ def get_python_api_file_path(
35
27
  def get_typescript_api_file_path(
36
28
  cross_output_paths: CrossOutputPaths,
37
29
  namespace: builder.SpecNamespace,
30
+ endpoint_key: builder.EndpointKey,
38
31
  ) -> str:
39
- return (
40
- f"{cross_output_paths.typescript_routes_output}/{'/'.join(namespace.path)}.tsx"
41
- )
32
+ return f"{cross_output_paths.typescript_routes_output_by_endpoint[endpoint_key]}/{'/'.join(namespace.path)}.tsx"
42
33
 
43
34
 
44
35
  def get_yaml_api_file_path(
@@ -68,13 +59,16 @@ def get_path_links(
68
59
  namespace: builder.SpecNamespace,
69
60
  *,
70
61
  current_path_type: str,
62
+ endpoint: builder.SpecEndpoint,
71
63
  ) -> str:
72
64
  if cross_output_paths is None:
73
65
  return ""
74
66
 
75
67
  api_paths = {
76
68
  "Python": get_python_api_file_path(cross_output_paths, namespace),
77
- "TypeScript": get_typescript_api_file_path(cross_output_paths, namespace),
69
+ "TypeScript": get_typescript_api_file_path(
70
+ cross_output_paths, namespace, endpoint.default_endpoint_key
71
+ ),
78
72
  "YAML": get_yaml_api_file_path(cross_output_paths, namespace),
79
73
  }
80
74
 
@@ -95,11 +89,11 @@ def get_path_links(
95
89
 
96
90
  if namespace.endpoint is not None:
97
91
  for (
98
- endpoint,
92
+ endpoint_key,
99
93
  path_specific_endpoint,
100
94
  ) in namespace.endpoint.path_per_api_endpoint.items():
101
95
  path_from_root = get_python_stub_file_path(path_specific_endpoint.function)
102
96
  if path_from_root is None:
103
97
  continue
104
- paths_string += f"{comment_prefix} Implementation for {endpoint}: file://./{return_to_root_path}{path_from_root}\n"
98
+ paths_string += f"{comment_prefix} Implementation for {endpoint_key}: file://./{return_to_root_path}{path_from_root}\n"
105
99
  return paths_string
@@ -261,6 +261,10 @@ def _emit_endpoint_parameters(
261
261
  } | _emit_endpoint_parameter_examples(examples)
262
262
 
263
263
 
264
+ def _emit_endpoint_deprecated(deprecated: bool) -> DictApiSchema:
265
+ return {"deprecated": True} if deprecated else {}
266
+
267
+
264
268
  def _emit_stability_level(
265
269
  stability_level: EmitOpenAPIStabilityLevel | None,
266
270
  ) -> DictApiSchema:
@@ -275,7 +279,12 @@ def _emit_stability_level(
275
279
  case EmitOpenAPIStabilityLevel.draft:
276
280
  stability_info["x-beta"] = True
277
281
  case EmitOpenAPIStabilityLevel.beta:
278
- stability_info["x-badges"] = [{"name": "Beta", "color": "DarkOrange"}]
282
+ stability_info["x-badges"] = [
283
+ {
284
+ "name": "Beta",
285
+ "color": "DarkOrange",
286
+ }
287
+ ]
279
288
  case EmitOpenAPIStabilityLevel.stable:
280
289
  pass
281
290
  case _:
@@ -330,18 +339,57 @@ def _emit_endpoint_response_examples(
330
339
  return {"examples": response_examples}
331
340
 
332
341
 
342
+ def _create_warning_banner(api_type: str, message: str) -> str:
343
+ return (
344
+ f'<div style="background-color: #fff3cd; border: 1px solid #ffeaa7; '
345
+ f'border-radius: 4px; padding: 12px; margin-bottom: 16px;">'
346
+ f"<strong>⚠️ {api_type} API:</strong> {message}"
347
+ f"</div>"
348
+ )
349
+
350
+
351
+ def _get_stability_warning(
352
+ stability_level: EmitOpenAPIStabilityLevel | None,
353
+ ) -> str:
354
+ resolved_stability_level = (
355
+ stability_level
356
+ if stability_level is not None
357
+ else EmitOpenAPIStabilityLevel.stable
358
+ )
359
+
360
+ match resolved_stability_level:
361
+ case EmitOpenAPIStabilityLevel.draft:
362
+ return _create_warning_banner(
363
+ "Draft",
364
+ "This endpoint is in draft status and may change significantly. Not recommended for production use.",
365
+ )
366
+ case EmitOpenAPIStabilityLevel.beta:
367
+ return _create_warning_banner(
368
+ "Beta",
369
+ "This endpoint is in beta and its required parameters may change. Use with caution in production environments.",
370
+ )
371
+ case EmitOpenAPIStabilityLevel.stable:
372
+ return ""
373
+
374
+
333
375
  def _emit_endpoint_description(
334
- description: str, guides: list[EmitOpenAPIGuide]
376
+ description: str,
377
+ guides: list[EmitOpenAPIGuide],
378
+ stability_level: EmitOpenAPIStabilityLevel | None = None,
335
379
  ) -> dict[str, str]:
380
+ stability_warning = _get_stability_warning(stability_level)
381
+
336
382
  full_guides = "<br/>".join([
337
383
  _write_guide_as_html(guide, is_open=False)
338
384
  for guide in sorted(guides, key=lambda g: g.ref_name)
339
385
  ])
340
- return {
341
- "description": description
342
- if len(guides) == 0
343
- else f"{description}<br/>{full_guides}"
344
- }
386
+
387
+ full_description_parts = [
388
+ part for part in [stability_warning, description, full_guides] if part
389
+ ]
390
+ full_description = "<br/>".join(full_description_parts)
391
+
392
+ return {"description": full_description}
345
393
 
346
394
 
347
395
  def _emit_namespace(
@@ -376,7 +424,10 @@ def _emit_namespace(
376
424
  "tags": endpoint.tags,
377
425
  "summary": endpoint.summary,
378
426
  }
379
- | _emit_endpoint_description(endpoint.description, ctx.endpoint.guides)
427
+ | _emit_endpoint_deprecated(endpoint.deprecated)
428
+ | _emit_endpoint_description(
429
+ endpoint.description, ctx.endpoint.guides, endpoint.stability_level
430
+ )
380
431
  | _emit_stability_level(endpoint.stability_level)
381
432
  | _emit_endpoint_parameters(endpoint, argument_type, ctx.endpoint.examples)
382
433
  | _emit_endpoint_request_body(
@@ -474,8 +525,18 @@ def _emit_type(
474
525
  return
475
526
 
476
527
  if isinstance(stype, builder.SpecTypeDefnUnion):
477
- ctx.types[stype.name] = open_api_type(
478
- ctx, stype.get_backing_type(), config=config
528
+ converted_discriminator_map: dict[str, OpenAPIRefType] = dict()
529
+ if stype.discriminator_map is not None:
530
+ for discriminator_value, base_type in stype.discriminator_map.items():
531
+ converted_base_type = open_api_type(ctx, base_type, config=config)
532
+ assert isinstance(converted_base_type, OpenAPIRefType)
533
+ converted_discriminator_map[discriminator_value] = converted_base_type
534
+ ctx.types[stype.name] = OpenAPIUnionType(
535
+ [open_api_type(ctx, p, config=config) for p in stype.types],
536
+ discriminator=stype.discriminator,
537
+ discriminator_map=converted_discriminator_map
538
+ if stype.discriminator_map is not None
539
+ else None,
479
540
  )
480
541
  return
481
542
 
@@ -549,18 +610,6 @@ def _emit_type(
549
610
  ctx.types[stype.name] = final_type
550
611
 
551
612
 
552
- def _emit_constant(ctx: EmitOpenAPIContext, sconst: builder.SpecConstant) -> None:
553
- if sconst.value_type.is_base_type(builder.BaseTypeName.s_string):
554
- value = util.encode_common_string(cast(str, sconst.value))
555
- elif sconst.value_type.is_base_type(builder.BaseTypeName.s_integer):
556
- value = str(sconst.value)
557
- else:
558
- raise Exception("invalid constant type", sconst.name)
559
-
560
- const_name = sconst.name.upper()
561
- print("_emit_constant", value, const_name)
562
-
563
-
564
613
  def _emit_endpoint(
565
614
  gctx: EmitOpenAPIGlobalContext,
566
615
  ctx: EmitOpenAPIContext,
@@ -617,6 +666,7 @@ def _emit_endpoint(
617
666
  tags=[tag_name],
618
667
  summary=f"{'/'.join(namespace.path[path_cutoff:])}",
619
668
  description=description,
669
+ deprecated=namespace.endpoint.deprecated,
620
670
  stability_level=namespace.endpoint.stability_level,
621
671
  examples=[
622
672
  EmitOpenAPIEndpointExample(
@@ -82,6 +82,7 @@ class EmitOpenAPIEndpoint:
82
82
  tags: list[str]
83
83
  summary: str
84
84
  description: str
85
+ deprecated: bool
85
86
  stability_level: EmitOpenAPIStabilityLevel | None
86
87
  examples: list[EmitOpenAPIEndpointExample]
87
88
  guides: list[EmitOpenAPIGuide]
@@ -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
 
@@ -357,7 +400,10 @@ def _emit_namespace(ctx: Context, namespace: builder.SpecNamespace) -> None:
357
400
  endpoint = namespace.endpoint
358
401
  if endpoint is not None:
359
402
  path_links = get_path_links(
360
- ctx.builder.cross_output_paths, namespace, current_path_type="Python"
403
+ ctx.builder.cross_output_paths,
404
+ namespace,
405
+ current_path_type="Python",
406
+ endpoint=endpoint,
361
407
  )
362
408
  if path_links != "":
363
409
  ctx.out.write("\n")
@@ -468,6 +514,19 @@ def _emit_endpoint_invocation_function_signature(
468
514
  else []
469
515
  ) + (extra_params if extra_params is not None else [])
470
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
+
471
530
  # All endpoints share a function name
472
531
  function = endpoint.path_per_api_endpoint[endpoint.default_endpoint_key].function
473
532
  assert function is not None
@@ -638,6 +697,7 @@ def _emit_endpoint_invocation_function(
638
697
  method={refer_to(ctx=ctx, stype=endpoint_method_stype)},
639
698
  endpoint={refer_to(ctx=ctx, stype=endpoint_path_stype)},
640
699
  args=args,
700
+ request_options=_request_options,
641
701
  )
642
702
  return self.do_request(api_request=api_request, return_type={refer_to(ctx=ctx, stype=data_type)})"""
643
703
  )
@@ -1393,7 +1453,7 @@ CLIENT_CLASS_IMPORTS = [
1393
1453
  "import dataclasses",
1394
1454
  ]
1395
1455
  ASYNC_BATCH_PROCESSOR_FILENAME = "async_batch_processor"
1396
- ASYNC_BATCH_PROCESSOR_IMPORTS = [
1456
+ ASYNC_BATCH_PROCESSOR_BASE_IMPORTS = [
1397
1457
  "import uuid",
1398
1458
  "from abc import ABC, abstractmethod",
1399
1459
  "from pkgs.serialization_util import serialize_for_api",
@@ -1431,8 +1491,11 @@ def _emit_async_batch_processor(
1431
1491
  config=config,
1432
1492
  )
1433
1493
 
1494
+ imports = ASYNC_BATCH_PROCESSOR_BASE_IMPORTS.copy()
1495
+ if ctx.use_dataclass:
1496
+ imports.append("import dataclasses")
1434
1497
  async_batch_processor_out.write(
1435
- f"""{LINE_BREAK.join(ASYNC_BATCH_PROCESSOR_IMPORTS)}
1498
+ f"""{LINE_BREAK.join(imports)}
1436
1499
 
1437
1500
 
1438
1501
  class AsyncBatchProcessorBase(ABC):
@@ -1495,6 +1558,7 @@ class APIRequest:
1495
1558
  method: str
1496
1559
  endpoint: str
1497
1560
  args: typing.Any
1561
+ request_options: {refer_to(ctx=ctx, stype=REQUEST_OPTIONS_STYPE)} | None = None
1498
1562
 
1499
1563
 
1500
1564
  class ClientMethods(ABC):
@@ -1,17 +1,18 @@
1
1
  import io
2
2
  import os
3
+ from typing import assert_never
3
4
 
4
5
  from . import builder, util
5
- from .builder import EndpointKey, EndpointSpecificPath
6
+ from .builder import EndpointKey, EndpointSpecificPath, PathMapping
6
7
  from .config import TypeScriptConfig
7
8
  from .cross_output_links import get_path_links
8
9
  from .emit_io_ts import emit_type_io_ts
9
10
  from .emit_typescript_util import (
10
11
  MODIFY_NOTICE,
11
12
  EmitTypescriptContext,
13
+ emit_constant_ts,
12
14
  emit_namespace_imports_ts,
13
15
  emit_type_ts,
14
- emit_value_ts,
15
16
  resolve_namespace_name,
16
17
  resolve_namespace_ref,
17
18
  ts_type_name,
@@ -37,6 +38,7 @@ def _emit_types(builder: builder.SpecBuilder, config: TypeScriptConfig) -> None:
37
38
  out=io.StringIO(),
38
39
  namespace=namespace,
39
40
  cross_output_paths=builder.cross_output_paths,
41
+ api_endpoints=builder.api_endpoints,
40
42
  )
41
43
 
42
44
  _emit_namespace(ctx, config, namespace)
@@ -78,6 +80,7 @@ def _emit_types(builder: builder.SpecBuilder, config: TypeScriptConfig) -> None:
78
80
  full.write("\n")
79
81
  full.write(MODIFY_NOTICE)
80
82
  full.write(f"// === START section from {namespace.name}.ts.part ===\n")
83
+ full.write("\n")
81
84
  full.write(part)
82
85
  full.write(f"// === END section from {namespace.name}.ts.part ===\n")
83
86
 
@@ -112,7 +115,7 @@ def _emit_namespace(
112
115
  emit_type_ts(ctx, stype)
113
116
 
114
117
  for sconst in namespace.constants.values():
115
- _emit_constant(ctx, sconst)
118
+ emit_constant_ts(ctx, sconst)
116
119
 
117
120
  if namespace.endpoint is not None:
118
121
  _emit_endpoint(ctx, config, namespace, namespace.endpoint)
@@ -145,7 +148,10 @@ def _emit_endpoint(
145
148
  assert endpoint.result_type == builder.ResultType.json
146
149
 
147
150
  paths_string = get_path_links(
148
- ctx.cross_output_paths, namespace, current_path_type="TypeScript"
151
+ ctx.cross_output_paths,
152
+ namespace,
153
+ current_path_type="TypeScript",
154
+ endpoint=endpoint,
149
155
  )
150
156
 
151
157
  data_loader_head = ""
@@ -155,7 +161,7 @@ def _emit_endpoint(
155
161
  assert has_data
156
162
 
157
163
  data_loader_head = (
158
- 'import { buildApiDataLoader, argsKey } from "unc_base/data_manager"\n'
164
+ 'import { argsKey, buildApiDataLoader } from "unc_base/data_manager"\n'
159
165
  )
160
166
  data_loader_body = (
161
167
  "\nexport const data = buildApiDataLoader(argsKey(), apiCall)\n"
@@ -175,38 +181,56 @@ def _emit_endpoint(
175
181
  unc_base_api_imports = (
176
182
  f"appSpecificApiPath, {wrap_name}" if has_multiple_endpoints else wrap_name
177
183
  )
184
+ path_mapping = ctx.api_endpoints[endpoint.default_endpoint_key].path_mapping
185
+
186
+ match path_mapping:
187
+ case PathMapping.NO_MAPPING:
188
+ path_mapping_part = (
189
+ "\n { pathMapping: ApplicationT.APIPathMapping.noMapping },"
190
+ )
191
+ case PathMapping.DEFAULT_MAPPING:
192
+ path_mapping_part = ""
193
+ case _:
194
+ assert_never(path_mapping)
195
+
178
196
  unc_types_imports = (
179
- 'import { ApplicationT } from "unc_types"\n' if has_multiple_endpoints else ""
197
+ 'import { ApplicationT } from "unc_types"\n'
198
+ if has_multiple_endpoints or path_mapping_part != ""
199
+ else ""
180
200
  )
181
201
 
182
202
  type_path = f"unc_types/{'/'.join(namespace.path)}"
183
203
 
184
204
  if is_binary:
185
- tsx_response_part = f"""import {{ {unc_base_api_imports} }} from "unc_base/api"
186
- import type {{ Arguments }} from "{type_path}"
187
- {unc_types_imports}
205
+ tsx_response_head = f"""import {{ {unc_base_api_imports} }} from "unc_base/api"
206
+ """
207
+ tsx_response_part = f"""import type {{ Arguments }} from "{type_path}"
208
+
188
209
  export type {{ Arguments }}
189
210
  """
190
211
  elif has_data and endpoint.has_attachment:
191
- tsx_response_part = f"""import {{ {unc_base_api_imports}, type AttachmentResponse }} from "unc_base/api"
192
- import type {{ Arguments, Data }} from "{type_path}"
193
- {unc_types_imports}
212
+ tsx_response_head = f"""import {{ type AttachmentResponse, {unc_base_api_imports} }} from "unc_base/api"
213
+ """
214
+ tsx_response_part = f"""import type {{ Arguments, Data }} from "{type_path}"
215
+
194
216
  export type {{ Arguments, Data }}
195
217
  export type Response = AttachmentResponse<Data>
196
218
  """
197
219
  elif has_data:
198
- tsx_response_part = f"""import {{ {unc_base_api_imports}, type JsonResponse }} from "unc_base/api"
199
- import type {{ Arguments, Data }} from "{type_path}"
200
- {unc_types_imports}
220
+ tsx_response_head = f"""import {{ {unc_base_api_imports}, type JsonResponse }} from "unc_base/api"
221
+ """
222
+ tsx_response_part = f"""import type {{ Arguments, Data }} from "{type_path}"
223
+
201
224
  export type {{ Arguments, Data }}
202
225
  export type Response = JsonResponse<Data>
203
226
  """
204
227
 
205
228
  else:
206
229
  assert has_deprecated_result
207
- tsx_response_part = f"""import {{ {unc_base_api_imports} }} from "unc_base/api"
208
- import type {{ Arguments, DeprecatedResult }} from "{type_path}"
209
- {unc_types_imports}
230
+ tsx_response_head = f"""import {{ {unc_base_api_imports} }} from "unc_base/api"
231
+ """
232
+ tsx_response_part = f"""import type {{ Arguments, DeprecatedResult }} from "{type_path}"
233
+
210
234
  export type {{ Arguments }}
211
235
  export type Response = DeprecatedResult
212
236
  """
@@ -247,41 +271,33 @@ export type Response = DeprecatedResult
247
271
 
248
272
  # tsx_api = f"""{MODIFY_NOTICE}
249
273
  tsx_api = f"""{MODIFY_NOTICE}{paths_string}
250
- {data_loader_head}{tsx_response_part}
274
+ {tsx_response_head}{data_loader_head}{unc_types_imports}{tsx_response_part}
251
275
  export const apiCall = {wrap_call}(
252
- {endpoint_path_part}
276
+ {endpoint_path_part}{path_mapping_part}
253
277
  )
254
278
  {data_loader_body}"""
255
279
 
256
- output = f"{config.routes_output}/{'/'.join(namespace.path)}.tsx"
280
+ output = f"{config.endpoint_to_routes_output[endpoint.default_endpoint_key]}/{'/'.join(namespace.path)}.tsx"
257
281
  util.rewrite_file(output, tsx_api)
258
282
 
259
283
  # Hacky index support, until enough is migrated to regen entirely
260
284
  # Emits the import into the UI API index file
261
- index_path = f"{config.routes_output}/{'/'.join(namespace.path[0:-1])}/index.tsx"
285
+ index_path = f"{config.endpoint_to_routes_output[endpoint.default_endpoint_key]}/{'/'.join(namespace.path[0:-1])}/index.tsx"
262
286
  api_name = f"Api{ts_type_name(namespace.path[0 - 1])}"
263
287
  if os.path.exists(index_path):
264
- with open(index_path) as index:
288
+ with open(index_path, encoding="utf-8") as index:
265
289
  index_data = index.read()
266
290
  need_index = index_data.find(api_name) == -1
267
291
  else:
268
292
  need_index = True
269
293
 
270
294
  if need_index:
271
- with open(index_path, "a") as index:
295
+ with open(index_path, "a", encoding="utf-8") as index:
272
296
  print(f"Updated API Index {index_path}")
273
297
  index.write(f'import * as {api_name} from "./{namespace.path[-1]}"\n\n')
274
298
  index.write(f"export {{ {api_name} }}\n")
275
299
 
276
300
 
277
- def _emit_constant(ctx: EmitTypescriptContext, sconst: builder.SpecConstant) -> None:
278
- ctx.out.write("\n\n")
279
- ctx.out.write(MODIFY_NOTICE)
280
- value = emit_value_ts(ctx, sconst.value_type, sconst.value)
281
- const_name = sconst.name.upper()
282
- ctx.out.write(f"export const {const_name} = {value}\n")
283
-
284
-
285
301
  def _emit_id_source(builder: builder.SpecBuilder, config: TypeScriptConfig) -> None:
286
302
  id_source_output = config.id_source_output
287
303
  if id_source_output is None: