UncountablePythonSDK 0.0.190__py3-none-any.whl → 0.0.192__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.
Files changed (45) hide show
  1. pkgs/argument_parser/parse_fallback.py +26 -0
  2. pkgs/argument_parser/parser_inner.py +116 -54
  3. pkgs/serialization/__init__.py +1 -0
  4. pkgs/serialization/serial_class.py +26 -1
  5. pkgs/type_spec/builder.py +61 -18
  6. pkgs/type_spec/builder_types.py +6 -0
  7. pkgs/type_spec/emit_io_ts.py +1 -1
  8. pkgs/type_spec/emit_open_api.py +29 -32
  9. pkgs/type_spec/emit_open_api_examples.py +221 -0
  10. pkgs/type_spec/emit_python.py +17 -81
  11. pkgs/type_spec/emit_typescript_util.py +27 -8
  12. pkgs/type_spec/parts/base.ts.prepart +6 -1
  13. uncountable/core/client.py +4 -1
  14. uncountable/core/file_upload.py +5 -3
  15. uncountable/core/tls.py +34 -0
  16. uncountable/integration/cli.py +2 -0
  17. uncountable/integration/job.py +47 -30
  18. uncountable/integration/job_queue.py +14 -0
  19. uncountable/integration/ownership_lock.py +77 -0
  20. uncountable/integration/queue_runner/command_server/command_client.py +2 -0
  21. uncountable/integration/queue_runner/command_server/command_server.py +2 -0
  22. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +1 -0
  23. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +4 -4
  24. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +2 -0
  25. uncountable/integration/queue_runner/command_server/types.py +1 -0
  26. uncountable/integration/queue_runner/datastore/datastore_sql_base.py +14 -7
  27. uncountable/integration/queue_runner/datastore/interface.py +6 -1
  28. uncountable/integration/queue_runner/job_scheduler.py +66 -23
  29. uncountable/integration/queue_runner/worker.py +8 -0
  30. uncountable/integration/scheduler.py +86 -57
  31. uncountable/integration/telemetry.py +13 -3
  32. uncountable/integration/webhook_server/entrypoint.py +17 -5
  33. uncountable/types/__init__.py +2 -0
  34. uncountable/types/api/listing/fetch_listing.py +2 -1
  35. uncountable/types/api/notebooks/get_notebook_content.py +15 -1
  36. uncountable/types/client_base.py +5 -2
  37. uncountable/types/entity_t.py +2 -0
  38. uncountable/types/listing.py +2 -0
  39. uncountable/types/listing_t.py +18 -0
  40. uncountable/types/notebook_files.py +10 -0
  41. uncountable/types/notebook_files_t.py +64 -0
  42. {uncountablepythonsdk-0.0.190.dist-info → uncountablepythonsdk-0.0.192.dist-info}/METADATA +1 -1
  43. {uncountablepythonsdk-0.0.190.dist-info → uncountablepythonsdk-0.0.192.dist-info}/RECORD +45 -38
  44. {uncountablepythonsdk-0.0.190.dist-info → uncountablepythonsdk-0.0.192.dist-info}/WHEEL +0 -0
  45. {uncountablepythonsdk-0.0.190.dist-info → uncountablepythonsdk-0.0.192.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,26 @@
1
+ import dataclasses
2
+ import inspect
3
+ import typing
4
+ from collections.abc import Callable, Mapping
5
+
6
+ from .parser_function_type import ParserFunction
7
+
8
+
9
+ @dataclasses.dataclass(kw_only=True, frozen=True)
10
+ class DataclassParseFallback:
11
+ legacy_key: str
12
+ legacy_parser: ParserFunction[object]
13
+ convert: Callable[[typing.Any], object]
14
+
15
+ def applies(self, *, value: Mapping[str, object], field_raw_value: object) -> bool:
16
+ return (
17
+ field_raw_value is dataclasses.MISSING or field_raw_value is None
18
+ ) and value.get(self.legacy_key) is not None
19
+
20
+ def parse(self, value: Mapping[str, object]) -> object:
21
+ return self.convert(self.legacy_parser(value[self.legacy_key]))
22
+
23
+
24
+ def parse_fallback_legacy_type(convert: Callable[..., object]) -> typing.Any:
25
+ legacy_parameter = next(iter(inspect.signature(convert).parameters))
26
+ return typing.get_type_hints(convert, include_extras=True)[legacy_parameter]
@@ -15,6 +15,7 @@ import dateutil.parser
15
15
  from pkgs.serialization import (
16
16
  MissingSentryType,
17
17
  OpaqueKey,
18
+ SerialClassDataInspector,
18
19
  UnknownBrand,
19
20
  get_serial_class_data,
20
21
  get_serial_union_data,
@@ -22,6 +23,7 @@ from pkgs.serialization import (
22
23
 
23
24
  from .case_convert import camel_to_snake_case, snake_to_camel_case
24
25
  from .field_name_resolution import resolve_serialized_field_name
26
+ from .parse_fallback import DataclassParseFallback, parse_fallback_legacy_type
25
27
  from .parser_error import (
26
28
  ParserEnumError,
27
29
  ParserError,
@@ -255,6 +257,39 @@ def _build_parser_for_type_alias(
255
257
  return build_parser_inner(alias.__value__, context, merged_alias_map)
256
258
 
257
259
 
260
+ def _build_parser_for_dict(
261
+ *,
262
+ key_type: typing.Any,
263
+ value_type: typing.Any,
264
+ context: ParserContext,
265
+ type_var_map: dict[typing.TypeVar, type],
266
+ ) -> ParserFunction[T]:
267
+ k_inner_parser = build_parser_inner(
268
+ key_type,
269
+ context,
270
+ type_var_map,
271
+ )
272
+
273
+ def key_parser(value: typing.Any) -> object:
274
+ inner = k_inner_parser(value)
275
+ if (
276
+ isinstance(inner, str)
277
+ # enum keys and OpaqueData's would also have string value types,
278
+ # but their explicit type is not a string, thus shouldn't be converted
279
+ and key_type is str
280
+ and context.options.is_api_encoding
281
+ ):
282
+ return camel_to_snake_case(value)
283
+ return inner
284
+
285
+ v_parser = build_parser_inner(value_type, context, type_var_map)
286
+
287
+ def parse_dict(value: typing.Any) -> typing.Any:
288
+ return {key_parser(k): v_parser(v) for k, v in value.items()}
289
+
290
+ return parse_dict
291
+
292
+
258
293
  def _build_parser_for_collection(
259
294
  *,
260
295
  parsed_type: type[T]
@@ -289,6 +324,32 @@ def _build_parser_for_collection(
289
324
  return parse
290
325
 
291
326
 
327
+ # first parse ints to decimal to allow scientific notation and decimals
328
+ # e.g. (1) 1e4 => 1000, (2) 3.0 => 3
329
+ def _parse_int(value: typing.Any) -> int:
330
+ if isinstance(value, str):
331
+ assert "_" not in value, "numbers with underscores not considered integers"
332
+
333
+ dec_value = Decimal(value)
334
+ int_value = int(dec_value)
335
+ assert int_value == dec_value, (
336
+ f"value ({value}) cannot be parsed to int without discarding precision"
337
+ )
338
+ return int_value
339
+
340
+
341
+ def _parse_str(value: typing.Any) -> str:
342
+ if isinstance(value, str):
343
+ return value
344
+ if isinstance(value, (Decimal, float, int)):
345
+ return str(value)
346
+ raise ParserError(
347
+ f"Invalid string value: {type(value)}: {value}",
348
+ value=value,
349
+ expected_type=str,
350
+ )
351
+
352
+
292
353
  def build_parser_inner(
293
354
  parsed_type: type[T]
294
355
  | typing.TypeVar
@@ -437,53 +498,20 @@ def build_parser_inner(
437
498
  "Dict types only support two arguments for now",
438
499
  expected_type=parsed_type,
439
500
  )
440
- k_inner_parser = build_parser_inner(
441
- args[0],
442
- context,
443
- type_var_map,
501
+ key_type, value_type = args
502
+ return _build_parser_for_dict(
503
+ key_type=key_type,
504
+ value_type=value_type,
505
+ context=context,
506
+ type_var_map=type_var_map,
444
507
  )
445
508
 
446
- def key_parser(value: typing.Any) -> object:
447
- inner = k_inner_parser(value)
448
- if (
449
- isinstance(inner, str)
450
- # enum keys and OpaqueData's would also have string value types,
451
- # but their explicit type is not a string, thus shouldn't be converted
452
- and args[0] is str
453
- and context.options.is_api_encoding
454
- ):
455
- return camel_to_snake_case(value)
456
- return inner
457
-
458
- v_parser = build_parser_inner(args[1], context, type_var_map)
459
-
460
- def parse_dict(value: typing.Any) -> typing.Any:
461
- return origin((key_parser(k), v_parser(v)) for k, v in value.items())
462
-
463
- return parse_dict
464
-
465
509
  if origin == typing.Literal:
466
510
  valid_values: set[T] = set(typing.get_args(parsed_type))
467
511
  return lambda value: _invoke_membership_parser(valid_values, value)
468
512
 
469
513
  if parsed_type is int:
470
- # first parse ints to decimal to allow scientific notation and decimals
471
- # e.g. (1) 1e4 => 1000, (2) 3.0 => 3
472
-
473
- def parse_int(value: typing.Any) -> int:
474
- if isinstance(value, str):
475
- assert "_" not in value, (
476
- "numbers with underscores not considered integers"
477
- )
478
-
479
- dec_value = Decimal(value)
480
- int_value = int(dec_value)
481
- assert int_value == dec_value, (
482
- f"value ({value}) cannot be parsed to int without discarding precision"
483
- )
484
- return int_value
485
-
486
- return parse_int # type: ignore[return-value]
514
+ return _parse_int # type: ignore[return-value]
487
515
 
488
516
  if parsed_type is datetime.datetime:
489
517
 
@@ -511,19 +539,7 @@ def build_parser_inner(
511
539
  # MyPy: It's unclear why `parsed_type in (str, OpaqueKey)` is flagged as invalid
512
540
  # Thus an or statement is used instead, which isn't flagged as invalid.
513
541
  if parsed_type is str or parsed_type is OpaqueKey:
514
-
515
- def parse_str(value: typing.Any) -> str:
516
- if isinstance(value, str):
517
- return value
518
- if isinstance(value, (Decimal, float, int)):
519
- return str(value)
520
- raise ParserError(
521
- f"Invalid string value: {type(value)}: {value}",
522
- value=value,
523
- expected_type=str,
524
- )
525
-
526
- return parse_str # type: ignore[return-value]
542
+ return _parse_str # type: ignore[return-value]
527
543
 
528
544
  if parsed_type in (float, Decimal):
529
545
  numeric_type: type[Decimal | float] = (
@@ -636,6 +652,7 @@ def _build_parser_dataclass(
636
652
  ] = []
637
653
 
638
654
  serial_class_data = get_serial_class_data(parsed_type)
655
+ dc_parse_fallbacks: dict[str, DataclassParseFallback] = {}
639
656
 
640
657
  def parse(value: typing.Any) -> typing.Any:
641
658
  # Use an exact type match to prevent base/derived class mismatches
@@ -656,10 +673,17 @@ def _build_parser_dataclass(
656
673
  serialized_field_name,
657
674
  dataclasses.MISSING,
658
675
  )
676
+ parse_fallback = dc_parse_fallbacks.get(field.name)
659
677
  if context.options.strict_property_parsing:
660
678
  all_allowed_field_names.add(serialized_field_name)
679
+ if parse_fallback is not None:
680
+ all_allowed_field_names.add(parse_fallback.legacy_key)
661
681
  field_value: typing.Any
662
- if field_raw_value == dataclasses.MISSING:
682
+ if parse_fallback is not None and parse_fallback.applies(
683
+ value=value, field_raw_value=field_raw_value
684
+ ):
685
+ field_value = parse_fallback.parse(value)
686
+ elif field_raw_value == dataclasses.MISSING:
663
687
  if serial_class_data.has_parse_require(field.name):
664
688
  raise ParserError(
665
689
  "missing-required-field", field_name=field.name
@@ -726,4 +750,42 @@ def _build_parser_dataclass(
726
750
  build_parser_inner(field_type_hint, context, type_var_map),
727
751
  ))
728
752
 
753
+ dc_parse_fallbacks.update(
754
+ _build_dataclass_parse_fallbacks(
755
+ parsed_type=parsed_type,
756
+ serial_class_data=serial_class_data,
757
+ context=context,
758
+ type_var_map=type_var_map,
759
+ )
760
+ )
761
+
729
762
  return parser_function
763
+
764
+
765
+ def _build_dataclass_parse_fallbacks(
766
+ *,
767
+ parsed_type: type[DataclassT],
768
+ serial_class_data: SerialClassDataInspector[DataclassT],
769
+ context: ParserContext,
770
+ type_var_map: dict[typing.TypeVar, type],
771
+ ) -> dict[str, DataclassParseFallback]:
772
+ field_names = {field.name for field in dataclasses.fields(parsed_type)}
773
+ dc_parse_fallbacks: dict[str, DataclassParseFallback] = {}
774
+ for field_name, parse_fallback in serial_class_data.get_parse_fallbacks().items():
775
+ assert field_name in field_names, (
776
+ f"parse fallback for {field_name!r}, which is not a field of {parsed_type}"
777
+ )
778
+ dc_parse_fallbacks[field_name] = DataclassParseFallback(
779
+ legacy_key=resolve_serialized_field_name(
780
+ serial_class_data=serial_class_data,
781
+ field_name=parse_fallback.legacy_field,
782
+ source_encoding=context.options.encoding,
783
+ ),
784
+ legacy_parser=build_parser_inner(
785
+ parse_fallback_legacy_type(parse_fallback.convert),
786
+ context,
787
+ type_var_map,
788
+ ),
789
+ convert=parse_fallback.convert,
790
+ )
791
+ return dc_parse_fallbacks
@@ -12,6 +12,7 @@ from .serial_alias import (
12
12
  serial_alias_annotation as serial_alias_annotation,
13
13
  )
14
14
  from .serial_class import (
15
+ ParseFallback as ParseFallback,
15
16
  SerialClassDataInspector as SerialClassDataInspector,
16
17
  get_serial_class_data as get_serial_class_data,
17
18
  get_serial_string_enum_data as get_serial_string_enum_data,
@@ -3,11 +3,18 @@ from __future__ import annotations
3
3
  import dataclasses
4
4
  from collections.abc import Callable
5
5
  from enum import StrEnum
6
- from typing import Any, TypeVar, cast
6
+ from typing import Any, Generic, TypeVar, cast
7
7
 
8
8
  from .annotation import SerialBase, SerialInspector, SerialType
9
9
 
10
10
  ClassT = TypeVar("ClassT")
11
+ LegacyT = TypeVar("LegacyT")
12
+
13
+
14
+ @dataclasses.dataclass(kw_only=True, frozen=True)
15
+ class ParseFallback(Generic[LegacyT]):
16
+ legacy_field: str
17
+ convert: Callable[[LegacyT], object]
11
18
 
12
19
 
13
20
  @dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
@@ -17,6 +24,9 @@ class _SerialClassData(SerialBase):
17
24
  to_string_values: set[str] = dataclasses.field(default_factory=set)
18
25
  parse_require: set[str] = dataclasses.field(default_factory=set)
19
26
  emit_api_overrides: dict[str, str] = dataclasses.field(default_factory=dict)
27
+ parse_fallbacks: dict[str, ParseFallback[Any]] = dataclasses.field(
28
+ default_factory=dict
29
+ )
20
30
  named_type_path: str | None = None
21
31
 
22
32
 
@@ -30,6 +40,7 @@ def serial_class(
30
40
  to_string_values: set[str] | None = None,
31
41
  parse_require: set[str] | None = None,
32
42
  emit_api_overrides: dict[str, str] | None = None,
43
+ parse_fallbacks: dict[str, ParseFallback[Any]] | None = None,
33
44
  named_type_path: str | None = None,
34
45
  is_dynamic_allowed: bool = False,
35
46
  ) -> Callable[[ClassT], ClassT]:
@@ -59,6 +70,12 @@ def serial_class(
59
70
  storage encoding are unaffected. Replaces the default camelCase
60
71
  conversion. Cannot be combined with `unconverted_keys` for the
61
72
  same field; doing so raises at parse and serialize time.
73
+ @param parse_fallbacks
74
+ Maps Python field name -> a legacy field that the parser reads when
75
+ the input omits the field or sets it to null. The parser parses the
76
+ legacy value with the type of the first parameter of `convert`, and
77
+ stores the result of `convert` in the field. The legacy field is not
78
+ part of the type, so serialization never writes it.
62
79
  @param named_type_path
63
80
  The type_spec type-path to this type. This applies only to named types.
64
81
  """
@@ -70,6 +87,7 @@ def serial_class(
70
87
  to_string_values=to_string_values or set(),
71
88
  parse_require=parse_require or set(),
72
89
  emit_api_overrides=emit_api_overrides or {},
90
+ parse_fallbacks=parse_fallbacks or {},
73
91
  named_type_path=named_type_path,
74
92
  from_decorator=True,
75
93
  is_dynamic_allowed=is_dynamic_allowed,
@@ -103,6 +121,9 @@ class SerialClassDataInspector(SerialInspector[ClassT]):
103
121
  def get_emit_api_override(self, key: str) -> str | None:
104
122
  return self.current.emit_api_overrides.get(key)
105
123
 
124
+ def get_parse_fallbacks(self) -> dict[str, ParseFallback[Any]]:
125
+ return self.current.parse_fallbacks
126
+
106
127
 
107
128
  def get_merged_serial_class_data(
108
129
  type_class: SerialType[Any],
@@ -136,6 +157,10 @@ def get_merged_serial_class_data(
136
157
  **curr_base_class_data.emit_api_overrides,
137
158
  **base_class_data.emit_api_overrides,
138
159
  },
160
+ parse_fallbacks={
161
+ **curr_base_class_data.parse_fallbacks,
162
+ **base_class_data.parse_fallbacks,
163
+ },
139
164
  )
140
165
  return base_class_data
141
166
 
pkgs/type_spec/builder.py CHANGED
@@ -17,11 +17,12 @@ import regex
17
17
  from pkgs.serialization import Usage
18
18
 
19
19
  from . import util
20
- from .builder_types import CrossOutputPaths
20
+ from .builder_types import CrossOutputPaths, PythonParseFallback
21
21
  from .non_discriminated_union_exceptions import NON_DISCRIMINATED_UNION_EXCEPTIONS
22
22
  from .util import parse_type_str
23
23
 
24
24
  RawDict = dict[Any, Any]
25
+ PYTHON_PARSE_FALLBACK_KEYS = {"legacy_property", "convert"}
25
26
  EndpointKey = str
26
27
  RE_TYPESCRIPT_ENUM_MEMBER_NAME = regex.compile(
27
28
  r"^[$_\p{ID_Start}][$\p{ID_Continue}\u200c\u200d]*$"
@@ -312,6 +313,13 @@ def unwrap_literal_type(stype: SpecType) -> SpecTypeLiteralWrapper | None:
312
313
  return None
313
314
 
314
315
 
316
+ def serialized_literal_value(*, literal: SpecTypeLiteralWrapper) -> str:
317
+ if isinstance(literal.value_type, SpecTypeDefnStringEnum):
318
+ assert isinstance(literal.value, str)
319
+ return literal.value_type.values[literal.value].value
320
+ return str(literal.value)
321
+
322
+
315
323
  class SpecTypeDefn(SpecType):
316
324
  """
317
325
  Base for type definitions. Do not instantiate this directly, use a derived class.
@@ -582,7 +590,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
582
590
  self.properties: dict[str, SpecProperty] | None = None
583
591
  self._kw_only: bool = True
584
592
  self.desc: str | None = None
585
- self.python_post_init: str | None = None
593
+ self.python_parse_fallbacks: dict[str, PythonParseFallback] = {}
586
594
 
587
595
  def is_value_converted(self) -> bool:
588
596
  if self.base and not self.base.is_value_converted():
@@ -615,7 +623,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
615
623
  "name_case",
616
624
  "hashable",
617
625
  "kw_only",
618
- "python_post_init",
626
+ "python_parse_fallbacks",
619
627
  ],
620
628
  )
621
629
  type_base = builder.parse_type(self.namespace, data["type"], scope=self)
@@ -652,17 +660,47 @@ class SpecTypeDefnObject(SpecTypeDefn):
652
660
  self._kw_only = data.get("kw_only", True)
653
661
  self.desc = data.get("desc", None)
654
662
 
655
- python_post_init = data.get("python_post_init")
656
- if python_post_init is not None:
657
- assert isinstance(python_post_init, str)
658
- python_prepart = builder.preparts["python"].get(self.namespace.name, "")
659
- python_post_init_def_prefix = f"def {python_post_init}"
663
+ python_parse_fallbacks = data.get("python_parse_fallbacks")
664
+ if python_parse_fallbacks is not None:
665
+ self.python_parse_fallbacks = self._process_python_parse_fallbacks(
666
+ builder, python_parse_fallbacks
667
+ )
668
+
669
+ def _process_python_parse_fallbacks(
670
+ self, builder: SpecBuilder, data: object
671
+ ) -> dict[str, PythonParseFallback]:
672
+ builder.ensure(
673
+ isinstance(data, dict),
674
+ "python_parse_fallbacks: expected a mapping of property name to fallback",
675
+ )
676
+ assert isinstance(data, dict)
677
+ property_names = set(self.properties or {})
678
+ parse_fallbacks: dict[str, PythonParseFallback] = {}
679
+ for property_name, fallback_data in data.items():
680
+ builder.ensure(
681
+ property_name in property_names,
682
+ f"python_parse_fallbacks: {property_name} is not a property of this type",
683
+ )
660
684
  builder.ensure(
661
- f"{python_post_init_def_prefix}(" in python_prepart
662
- or f"{python_post_init_def_prefix}[" in python_prepart,
663
- f"python_post_init: the {self.namespace.name} python prepart file must define {python_post_init}",
685
+ isinstance(fallback_data, dict)
686
+ and set(fallback_data.keys()) == PYTHON_PARSE_FALLBACK_KEYS,
687
+ f"python_parse_fallbacks: {property_name} needs exactly the keys {sorted(PYTHON_PARSE_FALLBACK_KEYS)}",
664
688
  )
665
- self.python_post_init = python_post_init
689
+ legacy_property = fallback_data["legacy_property"]
690
+ convert = fallback_data["convert"]
691
+ builder.ensure(
692
+ isinstance(legacy_property, str) and isinstance(convert, str),
693
+ f"python_parse_fallbacks: {property_name} legacy_property and convert must be strings",
694
+ )
695
+ builder.ensure_python_prepart_function(
696
+ namespace_name=self.namespace.name,
697
+ function_name=convert,
698
+ option_name="python_parse_fallbacks",
699
+ )
700
+ parse_fallbacks[property_name] = PythonParseFallback(
701
+ legacy_property=legacy_property, convert=convert
702
+ )
703
+ return parse_fallbacks
666
704
 
667
705
  def is_kw_only(self) -> bool:
668
706
  return self._kw_only
@@ -758,12 +796,7 @@ class SpecTypeDefnUnion(SpecTypeDefn):
758
796
  prop_type = unwrap_literal_type(discriminator_type.spec_type)
759
797
  assert prop_type is not None
760
798
  assert prop_type.is_value_to_string()
761
- value_type = prop_type.value_type
762
- if isinstance(value_type, SpecTypeDefnStringEnum):
763
- assert isinstance(prop_type.value, str)
764
- discriminant = value_type.values[prop_type.value].value
765
- else:
766
- discriminant = str(prop_type.value)
799
+ discriminant = serialized_literal_value(literal=prop_type)
767
800
  assert discriminant not in self.discriminator_map, (
768
801
  f"duplicated-discriminant, {discriminant} in {sub_type}"
769
802
  )
@@ -1741,6 +1774,16 @@ class SpecBuilder:
1741
1774
  print(msg)
1742
1775
  raise BuilderError()
1743
1776
 
1777
+ def ensure_python_prepart_function(
1778
+ self, *, namespace_name: str, function_name: str, option_name: str
1779
+ ) -> None:
1780
+ python_prepart = self.preparts["python"].get(namespace_name, "")
1781
+ def_prefix = f"def {function_name}"
1782
+ self.ensure(
1783
+ f"{def_prefix}(" in python_prepart or f"{def_prefix}[" in python_prepart,
1784
+ f"{option_name}: the {namespace_name} python prepart file must define {function_name}",
1785
+ )
1786
+
1744
1787
  def prescan(self, namespace_path: str, data: RawDict) -> None:
1745
1788
  assert namespace_path not in self.namespaces
1746
1789
  namespace = SpecNamespace(namespace_path)
@@ -7,3 +7,9 @@ class CrossOutputPaths:
7
7
  typescript_types_output: str
8
8
  typescript_routes_output_by_endpoint: dict[str, str]
9
9
  typespec_files_input: list[str]
10
+
11
+
12
+ @dataclass(kw_only=True, frozen=True)
13
+ class PythonParseFallback:
14
+ legacy_property: str
15
+ convert: str
@@ -73,7 +73,7 @@ def _emit_type_io_ts_impl(ctx: EmitTypescriptContext, stype: builder.SpecType) -
73
73
  missable_lines = []
74
74
  for prop in stype.properties.values():
75
75
  ref_type = refer_to_io_ts(ctx, prop.spec_type)
76
- prop_name = ts_property_name(prop, stype.name_case)
76
+ prop_name = ts_property_name(prop)
77
77
  if prop.extant == builder.PropertyExtant.missing:
78
78
  # Unlike optional below, missing does not imply null is possible. They
79
79
  # treated distinctly.
@@ -12,11 +12,11 @@ from enum import StrEnum
12
12
  from typing import assert_never, cast
13
13
 
14
14
  from pkgs.serialization import yaml
15
- from pkgs.serialization_util import serialize_for_api
16
15
 
17
16
  from . import builder, util
18
17
  from .builder import EndpointGuideKey, RootGuideKey
19
18
  from .config import OpenAPIConfig
19
+ from .emit_open_api_examples import emit_endpoint_example
20
20
  from .emit_open_api_util import (
21
21
  MODIFY_NOTICE,
22
22
  EmitOpenAPIContext,
@@ -50,6 +50,9 @@ from .open_api_util import (
50
50
  OpenAPIUnionType,
51
51
  )
52
52
 
53
+ ARGUMENTS_TYPE_NAME = "Arguments"
54
+ DATA_TYPE_NAME = "Data"
55
+
53
56
  base_name_map = {
54
57
  builder.BaseTypeName.s_boolean: OpenAPIBooleanT,
55
58
  builder.BaseTypeName.s_date: OpenAPIStringT, # IMPROVE: Aliased DateStr
@@ -212,14 +215,14 @@ def _emit_endpoint_argument_examples(
212
215
  if len(examples) == 0:
213
216
  return {}
214
217
 
215
- response_examples = {}
218
+ argument_examples = {}
216
219
  for example in examples:
217
- response_examples[example.ref_name] = {
220
+ argument_examples[example.ref_name] = {
218
221
  "summary": example.summary,
219
222
  "description": example.description,
220
223
  "value": example.arguments,
221
224
  }
222
- return {"examples": response_examples}
225
+ return {"examples": argument_examples}
223
226
 
224
227
 
225
228
  def _emit_endpoint_parameter_examples(
@@ -228,7 +231,7 @@ def _emit_endpoint_parameter_examples(
228
231
  if len(examples) == 0:
229
232
  return {}
230
233
 
231
- paramater_examples = []
234
+ parameter_examples = []
232
235
  comment_new_line = "\n// "
233
236
  new_line = "\n"
234
237
  for example in examples:
@@ -236,12 +239,12 @@ def _emit_endpoint_parameter_examples(
236
239
  f"// {comment_new_line.join(example.description.split(new_line))}"
237
240
  )
238
241
  javascript_json_payload = f"{json.dumps(example.arguments, indent=2)}"
239
- paramater_examples.append({
242
+ parameter_examples.append({
240
243
  "lang": "JavaScript",
241
244
  "label": f"Payload - {example.summary}",
242
245
  "source": f"{javascript_description}\n{javascript_json_payload}",
243
246
  })
244
- return {"x-codeSamples": paramater_examples}
247
+ return {"x-codeSamples": parameter_examples}
245
248
 
246
249
 
247
250
  def _emit_endpoint_parameters(
@@ -428,7 +431,7 @@ def _emit_namespace(
428
431
 
429
432
  if ctx.endpoint is not None:
430
433
  endpoint = ctx.endpoint
431
- argument_type = ctx.types.get("Arguments")
434
+ argument_type = ctx.types.get(ARGUMENTS_TYPE_NAME)
432
435
  oa_endpoint = dict()
433
436
  oa_endpoint[endpoint.method] = (
434
437
  {
@@ -464,19 +467,19 @@ def _emit_namespace(
464
467
  if ctx.endpoint is not None:
465
468
  # OpenAPI always expected Arguments and Data create
466
469
  # them here if they were not already created
467
- if "Arguments" not in types:
468
- types["Arguments"] = OpenAPIObjectType({})
469
- if "Data" not in types:
470
- types["Data"] = OpenAPIObjectType({})
470
+ if ARGUMENTS_TYPE_NAME not in types:
471
+ types[ARGUMENTS_TYPE_NAME] = OpenAPIObjectType({})
472
+ if DATA_TYPE_NAME not in types:
473
+ types[DATA_TYPE_NAME] = OpenAPIObjectType({})
471
474
 
472
- if ctx.endpoint is not None and "Data" in ctx.types:
475
+ if ctx.endpoint is not None and DATA_TYPE_NAME in ctx.types:
473
476
  # IMPROVE: Handle the general case here: wrap result in data object
474
- types["Data"] = OpenAPIObjectType({"data": types["Data"]})
477
+ types[DATA_TYPE_NAME] = OpenAPIObjectType({"data": types[DATA_TYPE_NAME]})
475
478
 
476
- if ctx.endpoint is not None and "Arguments" in ctx.types:
479
+ if ctx.endpoint is not None and ARGUMENTS_TYPE_NAME in ctx.types:
477
480
  # OpenAPI Expects a different format for arguments
478
481
  # then for regular types; do conversion here
479
- raw_type = types["Arguments"]
482
+ raw_type = types[ARGUMENTS_TYPE_NAME]
480
483
  if isinstance(
481
484
  raw_type, (OpenAPIIntersectionType, OpenAPIUnionType, OpenAPIRefType)
482
485
  ):
@@ -584,7 +587,7 @@ def _emit_type(
584
587
 
585
588
  for prop in stype.properties.values():
586
589
  ref_type = open_api_type(ctx, prop.spec_type, config=config)
587
- prop_name = ts_property_name(prop, prop.name_case or stype.name_case)
590
+ prop_name = ts_property_name(prop)
588
591
  if prop.desc:
589
592
  property_desc[prop_name] = prop.desc
590
593
  if prop.has_default and not prop.parse_require:
@@ -638,18 +641,17 @@ def _emit_endpoint(
638
641
  assert namespace.endpoint is not None
639
642
  assert namespace.path[0] == "api"
640
643
 
641
- has_arguments = "Arguments" in namespace.types
642
- has_data = "Data" in namespace.types
644
+ arguments_type = namespace.types.get(ARGUMENTS_TYPE_NAME)
645
+ data_type = namespace.types.get(DATA_TYPE_NAME)
643
646
  has_deprecated_result = "DeprecatedResult" in namespace.types
644
647
  is_binary = endpoint.result_type == builder.ResultType.binary
645
648
 
646
- result_type_count = sum([has_data, has_deprecated_result, is_binary])
649
+ result_type_count = sum([data_type is not None, has_deprecated_result, is_binary])
647
650
 
648
651
  assert result_type_count < 2
649
- is_binary = endpoint.result_type == builder.ResultType.binary
650
652
 
651
653
  # Don't emit interface for those with unsupported types
652
- if not has_arguments or result_type_count == 0:
654
+ if arguments_type is None or result_type_count == 0:
653
655
  return
654
656
  if not is_binary and endpoint.result_type != builder.ResultType.json:
655
657
  return
@@ -686,12 +688,11 @@ def _emit_endpoint(
686
688
  deprecated=namespace.endpoint.deprecated,
687
689
  stability_level=namespace.endpoint.stability_level,
688
690
  examples=[
689
- EmitOpenAPIEndpointExample(
691
+ emit_endpoint_example(
692
+ arguments_type=arguments_type,
693
+ data_type=data_type,
694
+ example=example,
690
695
  ref_name=f"ex_{i}",
691
- summary=example.summary,
692
- description=example.description,
693
- arguments=serialize_for_api(example.arguments),
694
- data=serialize_for_api(example.data),
695
696
  )
696
697
  for i, example in enumerate(endpoint_examples)
697
698
  ],
@@ -752,11 +753,7 @@ def open_api_type(
752
753
  parts: list[str] = []
753
754
  for parameter in stype.parameters:
754
755
  assert isinstance(parameter, builder.SpecTypeLiteralWrapper)
755
- if isinstance(parameter.value_type, builder.SpecTypeDefnStringEnum):
756
- assert isinstance(parameter.value, str)
757
- parts.append(parameter.value_type.values[parameter.value].value)
758
- else:
759
- parts.append(str(parameter.value))
756
+ parts.append(builder.serialized_literal_value(literal=parameter))
760
757
 
761
758
  return OpenAPIEnumType(parts)
762
759
  if stype.defn_type.name == builder.BaseTypeName.s_optional: