UncountablePythonSDK 0.0.114__py3-none-any.whl → 0.0.116__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 (34) hide show
  1. examples/integration-server/jobs/materials_auto/example_http.py +35 -0
  2. examples/integration-server/jobs/materials_auto/example_instrument.py +38 -0
  3. examples/integration-server/jobs/materials_auto/profile.yaml +15 -0
  4. pkgs/type_spec/builder.py +18 -5
  5. pkgs/type_spec/config.py +26 -5
  6. pkgs/type_spec/cross_output_links.py +9 -7
  7. pkgs/type_spec/emit_open_api.py +9 -2
  8. pkgs/type_spec/emit_open_api_util.py +1 -0
  9. pkgs/type_spec/emit_python.py +4 -1
  10. pkgs/type_spec/emit_typescript.py +46 -30
  11. pkgs/type_spec/emit_typescript_util.py +16 -0
  12. pkgs/type_spec/load_types.py +1 -1
  13. pkgs/type_spec/open_api_util.py +9 -1
  14. pkgs/type_spec/parts/base.ts.prepart +1 -0
  15. pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +19 -5
  16. uncountable/core/environment.py +1 -1
  17. uncountable/integration/http_server/__init__.py +5 -0
  18. uncountable/integration/http_server/types.py +67 -0
  19. uncountable/integration/job.py +129 -5
  20. uncountable/integration/server.py +2 -2
  21. uncountable/integration/telemetry.py +1 -1
  22. uncountable/integration/webhook_server/entrypoint.py +37 -112
  23. uncountable/types/api/entity/create_or_update_entity.py +1 -0
  24. uncountable/types/api/entity/lookup_entity.py +15 -1
  25. uncountable/types/async_batch_processor.py +3 -0
  26. uncountable/types/client_base.py +52 -0
  27. uncountable/types/entity_t.py +8 -0
  28. uncountable/types/integration_server_t.py +2 -0
  29. uncountable/types/job_definition.py +2 -0
  30. uncountable/types/job_definition_t.py +25 -2
  31. {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/METADATA +1 -1
  32. {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/RECORD +34 -30
  33. {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/WHEEL +0 -0
  34. {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,35 @@
1
+ from dataclasses import dataclass
2
+
3
+ from uncountable.integration.http_server import (
4
+ GenericHttpRequest,
5
+ GenericHttpResponse,
6
+ )
7
+ from uncountable.integration.job import CustomHttpJob, register_job
8
+ from uncountable.types import job_definition_t
9
+
10
+
11
+ @dataclass(kw_only=True)
12
+ class ExampleWebhookPayload:
13
+ id: int
14
+ message: str
15
+
16
+
17
+ @register_job
18
+ class HttpExample(CustomHttpJob):
19
+ @staticmethod
20
+ def validate_request(
21
+ *,
22
+ request: GenericHttpRequest, # noqa: ARG004
23
+ job_definition: job_definition_t.HttpJobDefinitionBase, # noqa: ARG004
24
+ profile_meta: job_definition_t.ProfileMetadata, # noqa: ARG004
25
+ ) -> None:
26
+ return None
27
+
28
+ @staticmethod
29
+ def handle_request(
30
+ *,
31
+ request: GenericHttpRequest, # noqa: ARG004
32
+ job_definition: job_definition_t.HttpJobDefinitionBase, # noqa: ARG004
33
+ profile_meta: job_definition_t.ProfileMetadata, # noqa: ARG004
34
+ ) -> GenericHttpResponse:
35
+ return GenericHttpResponse(response="OK", status_code=200)
@@ -0,0 +1,38 @@
1
+ from dataclasses import dataclass
2
+
3
+ from uncountable.integration.job import JobArguments, WebhookJob, register_job
4
+ from uncountable.types import base_t, entity_t, job_definition_t
5
+
6
+
7
+ @dataclass(kw_only=True)
8
+ class InstrumentPayload:
9
+ equipment_id: base_t.ObjectId
10
+
11
+
12
+ @register_job
13
+ class InstrumentExample(WebhookJob[InstrumentPayload]):
14
+ def run(
15
+ self, args: JobArguments, payload: InstrumentPayload
16
+ ) -> job_definition_t.JobResult:
17
+ equipment_data = args.client.get_entities_data(
18
+ entity_type=entity_t.EntityType.EQUIPMENT,
19
+ entity_ids=[payload.equipment_id],
20
+ ).entity_details[0]
21
+
22
+ # Load the instrument's connection details from the entity
23
+ instrument_id = None
24
+ for field in equipment_data.field_values:
25
+ if field.field_ref_name == "ins_instrument_id":
26
+ instrument_id = field.value
27
+
28
+ if instrument_id is None:
29
+ args.logger.log_error("Could not find instrument ID")
30
+ return job_definition_t.JobResult(success=False)
31
+
32
+ args.logger.log_info(f"Instrument ID: {instrument_id}")
33
+
34
+ return job_definition_t.JobResult(success=True)
35
+
36
+ @property
37
+ def webhook_payload_type(self) -> type:
38
+ return InstrumentPayload
@@ -60,6 +60,12 @@ jobs:
60
60
  executor:
61
61
  type: script
62
62
  import_path: example_wh
63
+ - id: example_http
64
+ type: custom_http
65
+ name: Custom HTTP
66
+ executor:
67
+ type: script
68
+ import_path: example_http
63
69
  - id: example_runsheet_wh
64
70
  type: webhook
65
71
  name: Runsheet Webhook
@@ -69,3 +75,12 @@ jobs:
69
75
  executor:
70
76
  type: script
71
77
  import_path: example_runsheet_wh
78
+ - id: example_instrument
79
+ type: webhook
80
+ name: Webhook Instrument Connection
81
+ signature_key_secret:
82
+ type: env
83
+ env_key: WH_INSTRUMENT_SIGNATURE_KEY
84
+ executor:
85
+ type: script
86
+ import_path: example_instrument
pkgs/type_spec/builder.py CHANGED
@@ -15,12 +15,23 @@ from typing import Any, Self
15
15
  from . import util
16
16
  from .cross_output_links import CrossOutputPaths
17
17
  from .non_discriminated_union_exceptions import NON_DISCRIMINATED_UNION_EXCEPTIONS
18
- from .util import parse_type_str, unused
18
+ from .util import parse_type_str
19
19
 
20
20
  RawDict = dict[Any, Any]
21
21
  EndpointKey = str
22
22
 
23
23
 
24
+ class PathMapping(StrEnum):
25
+ NO_MAPPING = "no_mapping"
26
+ DEFAULT_MAPPING = "default_mapping"
27
+
28
+
29
+ @dataclass(kw_only=True)
30
+ class APIEndpointInfo:
31
+ root_path: str
32
+ path_mapping: PathMapping
33
+
34
+
24
35
  class StabilityLevel(StrEnum):
25
36
  """These are currently used for open api,
26
37
  see: https://github.com/Tufin/oasdiff/blob/main/docs/STABILITY.md
@@ -835,7 +846,7 @@ class _EndpointPathDetails:
835
846
 
836
847
 
837
848
  def _resolve_endpoint_path(
838
- path: str, api_endpoints: dict[EndpointKey, str]
849
+ path: str, api_endpoints: dict[EndpointKey, APIEndpointInfo]
839
850
  ) -> _EndpointPathDetails:
840
851
  root_path_source = path.split("/")[0]
841
852
  root_match = RE_ENDPOINT_ROOT.fullmatch(root_path_source)
@@ -843,7 +854,7 @@ def _resolve_endpoint_path(
843
854
  raise Exception(f"invalid-api-path-root:{root_path_source}")
844
855
 
845
856
  root_var = root_match.group(1)
846
- root_path = api_endpoints[root_var]
857
+ root_path = api_endpoints[root_var].root_path
847
858
 
848
859
  _, *rest_path = path.split("/", 1)
849
860
  resolved_path = "/".join([root_path] + rest_path)
@@ -911,6 +922,7 @@ class SpecEndpoint:
911
922
  stability_level: StabilityLevel | None
912
923
  # Don't emit TypeScript endpoint code
913
924
  suppress_ts: bool
925
+ deprecated: bool = False
914
926
  async_batch_path: str | None = None
915
927
  result_type: ResultType = ResultType.json
916
928
  has_attachment: bool = False
@@ -928,13 +940,13 @@ class SpecEndpoint:
928
940
  pass
929
941
 
930
942
  def process(self, builder: SpecBuilder, data: RawDict) -> None:
931
- unused(builder)
932
943
  util.check_fields(
933
944
  data,
934
945
  [
935
946
  "method",
936
947
  "path",
937
948
  "data_loader",
949
+ "deprecated",
938
950
  "is_sdk",
939
951
  "stability_level",
940
952
  "async_batch_path",
@@ -954,6 +966,7 @@ class SpecEndpoint:
954
966
  data_loader = data.get("data_loader", False)
955
967
  assert isinstance(data_loader, bool)
956
968
  self.data_loader = data_loader
969
+ self.deprecated = data.get("deprecated", False)
957
970
 
958
971
  is_sdk = data.get("is_sdk", EndpointEmitType.EMIT_NOTHING)
959
972
 
@@ -1346,7 +1359,7 @@ class SpecBuilder:
1346
1359
  def __init__(
1347
1360
  self,
1348
1361
  *,
1349
- api_endpoints: dict[EndpointKey, str],
1362
+ api_endpoints: dict[EndpointKey, APIEndpointInfo],
1350
1363
  top_namespace: str,
1351
1364
  cross_output_paths: CrossOutputPaths | None,
1352
1365
  ) -> None:
pkgs/type_spec/config.py CHANGED
@@ -4,6 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import Self, TypeVar
5
5
 
6
6
  from pkgs.serialization import yaml
7
+ from pkgs.type_spec.builder import APIEndpointInfo, EndpointKey
7
8
 
8
9
  ConfigValueType = str | None | Mapping[str, str | None] | list[str]
9
10
 
@@ -19,6 +20,22 @@ def _parse_string_lookup(
19
20
  }
20
21
 
21
22
 
23
+ VT = TypeVar("VT")
24
+
25
+
26
+ def _parse_data_lookup(
27
+ key: str,
28
+ raw_value: ConfigValueType,
29
+ conv_func: type[VT],
30
+ ) -> dict[str, VT]:
31
+ assert isinstance(raw_value, dict), f"{key} must be key/values"
32
+ return {
33
+ k: conv_func(**v)
34
+ for k, v in raw_value.items()
35
+ if v is not None and isinstance(v, dict)
36
+ }
37
+
38
+
22
39
  @dataclass(kw_only=True)
23
40
  class BaseLanguageConfig:
24
41
  types_output: (
@@ -31,7 +48,9 @@ class BaseLanguageConfig:
31
48
 
32
49
  @dataclass(kw_only=True)
33
50
  class TypeScriptConfig(BaseLanguageConfig):
34
- routes_output: str # folder for generate route files will be located.
51
+ endpoint_to_routes_output: dict[
52
+ EndpointKey, str
53
+ ] # folder for generate route files will be located.
35
54
  type_info_output: str # folder for generated type info files
36
55
  id_source_output: str | None = None # folder for emitted id source maps.
37
56
  endpoint_to_frontend_app_type: dict[
@@ -39,7 +58,7 @@ class TypeScriptConfig(BaseLanguageConfig):
39
58
  ] # map from api_endpoint to frontend app type
40
59
 
41
60
  def __post_init__(self: Self) -> None:
42
- self.routes_output = self.routes_output
61
+ self.endpoint_to_routes_output = self.endpoint_to_routes_output
43
62
  self.type_info_output = os.path.abspath(self.type_info_output)
44
63
  self.id_source_output = (
45
64
  os.path.abspath(self.id_source_output)
@@ -100,7 +119,7 @@ class OpenAPIConfig(BaseLanguageConfig):
100
119
  class Config:
101
120
  top_namespace: str
102
121
  type_spec_types: list[str] # folders containing the yaml type spec definitions
103
- api_endpoint: dict[str, str]
122
+ api_endpoint: dict[str, APIEndpointInfo]
104
123
  # languages
105
124
  typescript: TypeScriptConfig | None
106
125
  python: PythonConfig
@@ -125,8 +144,10 @@ def parse_yaml_config(config_file: str) -> Config:
125
144
  )
126
145
  type_spec_types = [os.path.abspath(folder) for folder in raw_type_spec_types]
127
146
 
128
- api_endpoint = _parse_string_lookup(
129
- "api_endpoint", raw_config.get("api_endpoint", {}), lambda x: x
147
+ api_endpoint = _parse_data_lookup(
148
+ "api_endpoint",
149
+ raw_config.get("api_endpoint", {}),
150
+ APIEndpointInfo,
130
151
  )
131
152
 
132
153
  raw_typescript = raw_config.get("typescript")
@@ -21,7 +21,7 @@ def get_python_stub_file_path(
21
21
  class CrossOutputPaths:
22
22
  python_types_output: str
23
23
  typescript_types_output: str
24
- typescript_routes_output: str
24
+ typescript_routes_output_by_endpoint: dict[str, str]
25
25
  typespec_files_input: list[str]
26
26
 
27
27
 
@@ -35,10 +35,9 @@ def get_python_api_file_path(
35
35
  def get_typescript_api_file_path(
36
36
  cross_output_paths: CrossOutputPaths,
37
37
  namespace: builder.SpecNamespace,
38
+ endpoint_key: builder.EndpointKey,
38
39
  ) -> str:
39
- return (
40
- f"{cross_output_paths.typescript_routes_output}/{'/'.join(namespace.path)}.tsx"
41
- )
40
+ return f"{cross_output_paths.typescript_routes_output_by_endpoint[endpoint_key]}/{'/'.join(namespace.path)}.tsx"
42
41
 
43
42
 
44
43
  def get_yaml_api_file_path(
@@ -68,13 +67,16 @@ def get_path_links(
68
67
  namespace: builder.SpecNamespace,
69
68
  *,
70
69
  current_path_type: str,
70
+ endpoint: builder.SpecEndpoint,
71
71
  ) -> str:
72
72
  if cross_output_paths is None:
73
73
  return ""
74
74
 
75
75
  api_paths = {
76
76
  "Python": get_python_api_file_path(cross_output_paths, namespace),
77
- "TypeScript": get_typescript_api_file_path(cross_output_paths, namespace),
77
+ "TypeScript": get_typescript_api_file_path(
78
+ cross_output_paths, namespace, endpoint.default_endpoint_key
79
+ ),
78
80
  "YAML": get_yaml_api_file_path(cross_output_paths, namespace),
79
81
  }
80
82
 
@@ -95,11 +97,11 @@ def get_path_links(
95
97
 
96
98
  if namespace.endpoint is not None:
97
99
  for (
98
- endpoint,
100
+ endpoint_key,
99
101
  path_specific_endpoint,
100
102
  ) in namespace.endpoint.path_per_api_endpoint.items():
101
103
  path_from_root = get_python_stub_file_path(path_specific_endpoint.function)
102
104
  if path_from_root is None:
103
105
  continue
104
- paths_string += f"{comment_prefix} Implementation for {endpoint}: file://./{return_to_root_path}{path_from_root}\n"
106
+ paths_string += f"{comment_prefix} Implementation for {endpoint_key}: file://./{return_to_root_path}{path_from_root}\n"
105
107
  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:
@@ -376,6 +380,7 @@ def _emit_namespace(
376
380
  "tags": endpoint.tags,
377
381
  "summary": endpoint.summary,
378
382
  }
383
+ | _emit_endpoint_deprecated(endpoint.deprecated)
379
384
  | _emit_endpoint_description(endpoint.description, ctx.endpoint.guides)
380
385
  | _emit_stability_level(endpoint.stability_level)
381
386
  | _emit_endpoint_parameters(endpoint, argument_type, ctx.endpoint.examples)
@@ -474,8 +479,9 @@ def _emit_type(
474
479
  return
475
480
 
476
481
  if isinstance(stype, builder.SpecTypeDefnUnion):
477
- ctx.types[stype.name] = open_api_type(
478
- ctx, stype.get_backing_type(), config=config
482
+ ctx.types[stype.name] = OpenAPIUnionType(
483
+ [open_api_type(ctx, p, config=config) for p in stype.types],
484
+ discrimnator=stype.discriminator,
479
485
  )
480
486
  return
481
487
 
@@ -617,6 +623,7 @@ def _emit_endpoint(
617
623
  tags=[tag_name],
618
624
  summary=f"{'/'.join(namespace.path[path_cutoff:])}",
619
625
  description=description,
626
+ deprecated=namespace.endpoint.deprecated,
620
627
  stability_level=namespace.endpoint.stability_level,
621
628
  examples=[
622
629
  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]
@@ -357,7 +357,10 @@ def _emit_namespace(ctx: Context, namespace: builder.SpecNamespace) -> None:
357
357
  endpoint = namespace.endpoint
358
358
  if endpoint is not None:
359
359
  path_links = get_path_links(
360
- ctx.builder.cross_output_paths, namespace, current_path_type="Python"
360
+ ctx.builder.cross_output_paths,
361
+ namespace,
362
+ current_path_type="Python",
363
+ endpoint=endpoint,
361
364
  )
362
365
  if path_links != "":
363
366
  ctx.out.write("\n")
@@ -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,18 +271,18 @@ 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
288
  with open(index_path) as index:
@@ -274,14 +298,6 @@ export const apiCall = {wrap_call}(
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:
@@ -32,6 +32,7 @@ class EmitTypescriptContext:
32
32
  namespace: builder.SpecNamespace
33
33
  namespaces: set[builder.SpecNamespace] = field(default_factory=set)
34
34
  cross_output_paths: CrossOutputPaths | None = None
35
+ api_endpoints: dict[builder.EndpointKey, builder.APIEndpointInfo]
35
36
 
36
37
 
37
38
  def ts_type_name(name: str) -> str:
@@ -80,6 +81,12 @@ def emit_value_ts(
80
81
  if stype.defn_type.is_base_type(builder.BaseTypeName.s_dict):
81
82
  key_type = stype.parameters[0]
82
83
  value_type = stype.parameters[1]
84
+
85
+ if not key_type.is_base_type(
86
+ builder.BaseTypeName.s_string
87
+ ) and not isinstance(key_type, builder.SpecTypeDefnStringEnum):
88
+ raise Exception("invalid dict keys -- dict keys must be string or enum")
89
+
83
90
  return (
84
91
  "{\n\t"
85
92
  + ",\n\t".join(
@@ -106,6 +113,14 @@ def emit_value_ts(
106
113
  raise Exception("invalid constant type", value, stype, type(stype))
107
114
 
108
115
 
116
+ def emit_constant_ts(ctx: EmitTypescriptContext, sconst: builder.SpecConstant) -> None:
117
+ ctx.out.write("\n\n")
118
+ ctx.out.write(MODIFY_NOTICE)
119
+ value = emit_value_ts(ctx, sconst.value_type, sconst.value)
120
+ const_name = sconst.name.upper()
121
+ ctx.out.write(f"export const {const_name} = {value}\n")
122
+
123
+
109
124
  def emit_type_ts(ctx: EmitTypescriptContext, stype: builder.SpecType) -> None:
110
125
  if not isinstance(stype, builder.SpecTypeDefn):
111
126
  return
@@ -118,6 +133,7 @@ def emit_type_ts(ctx: EmitTypescriptContext, stype: builder.SpecType) -> None:
118
133
 
119
134
  if isinstance(stype, builder.SpecTypeDefnExternal):
120
135
  assert not stype.is_exported, "expecting private names"
136
+ ctx.out.write("\n")
121
137
  ctx.out.write(stype.external_map["ts"])
122
138
  ctx.out.write("\n")
123
139
  return
@@ -46,7 +46,7 @@ def load_types(config: Config) -> SpecBuilder | None:
46
46
  cross_output_paths = CrossOutputPaths(
47
47
  python_types_output=config.python.types_output,
48
48
  typescript_types_output=config.typescript.types_output,
49
- typescript_routes_output=config.typescript.routes_output,
49
+ typescript_routes_output_by_endpoint=config.typescript.endpoint_to_routes_output,
50
50
  typespec_files_input=config.type_spec_types,
51
51
  # IMPROVE not sure how to know which one is the correct one in emit_typescript
52
52
  )
@@ -223,13 +223,21 @@ class OpenAPIUnionType(OpenAPIType):
223
223
  base_types: list[OpenAPIType],
224
224
  description: str | None = None,
225
225
  nullable: bool = False,
226
+ discrimnator: str | None = None,
227
+ discriminator_map: dict[str, OpenAPIType] | None = None,
226
228
  ) -> None:
227
229
  self.base_types = base_types
230
+ self._discriminator = discrimnator
228
231
  super().__init__(description=description, nullable=nullable)
229
232
 
230
233
  def asdict(self) -> dict[str, object]:
231
234
  # TODO: use parents description and nullable
232
- return {"oneOf": [base_type.asdict() for base_type in self.base_types]}
235
+ return {
236
+ "oneOf": [base_type.asdict() for base_type in self.base_types],
237
+ "discriminator": {"propertyName": self._discriminator}
238
+ if self._discriminator is not None
239
+ else None,
240
+ }
233
241
 
234
242
 
235
243
  class OpenAPIIntersectionType(OpenAPIType):
@@ -3,6 +3,7 @@
3
3
  // doesn't allow referring explicitly to global names (thus cannot override here)
4
4
  // IMPROVE: invert relationship for global.d.ts looks here instead
5
5
  import * as IO from 'io-ts';
6
+
6
7
  type localJsonScalar = JsonScalar
7
8
  type localJsonValue = JsonValue
8
9
  type localObjectId = ObjectId