UncountablePythonSDK 0.0.191__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.
- pkgs/argument_parser/parse_fallback.py +26 -0
- pkgs/argument_parser/parser_inner.py +116 -54
- pkgs/serialization/__init__.py +1 -0
- pkgs/serialization/serial_class.py +26 -1
- pkgs/type_spec/builder.py +61 -18
- pkgs/type_spec/builder_types.py +6 -0
- pkgs/type_spec/emit_io_ts.py +1 -1
- pkgs/type_spec/emit_open_api.py +29 -32
- pkgs/type_spec/emit_open_api_examples.py +221 -0
- pkgs/type_spec/emit_python.py +17 -81
- pkgs/type_spec/emit_typescript_util.py +2 -4
- uncountable/core/client.py +4 -1
- uncountable/core/file_upload.py +5 -3
- uncountable/core/tls.py +34 -0
- uncountable/integration/telemetry.py +13 -3
- uncountable/types/__init__.py +2 -0
- uncountable/types/api/listing/fetch_listing.py +2 -1
- uncountable/types/api/notebooks/get_notebook_content.py +15 -1
- uncountable/types/client_base.py +5 -2
- uncountable/types/listing.py +2 -0
- uncountable/types/listing_t.py +18 -0
- uncountable/types/notebook_files.py +10 -0
- uncountable/types/notebook_files_t.py +64 -0
- {uncountablepythonsdk-0.0.191.dist-info → uncountablepythonsdk-0.0.192.dist-info}/METADATA +1 -1
- {uncountablepythonsdk-0.0.191.dist-info → uncountablepythonsdk-0.0.192.dist-info}/RECORD +27 -22
- {uncountablepythonsdk-0.0.191.dist-info → uncountablepythonsdk-0.0.192.dist-info}/WHEEL +0 -0
- {uncountablepythonsdk-0.0.191.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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
pkgs/serialization/__init__.py
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
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
|
-
|
|
656
|
-
if
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
f"
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
pkgs/type_spec/builder_types.py
CHANGED
pkgs/type_spec/emit_io_ts.py
CHANGED
|
@@ -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
|
|
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.
|
pkgs/type_spec/emit_open_api.py
CHANGED
|
@@ -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
|
-
|
|
218
|
+
argument_examples = {}
|
|
216
219
|
for example in examples:
|
|
217
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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(
|
|
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
|
|
468
|
-
types[
|
|
469
|
-
if
|
|
470
|
-
types[
|
|
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
|
|
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[
|
|
477
|
+
types[DATA_TYPE_NAME] = OpenAPIObjectType({"data": types[DATA_TYPE_NAME]})
|
|
475
478
|
|
|
476
|
-
if ctx.endpoint is not None and
|
|
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[
|
|
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
|
|
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
|
-
|
|
642
|
-
|
|
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([
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|