Skip to content

Commit b198012

Browse files
committed
feat: limit response parsing to values the SDK does not recognize
#651 conflated two things. Handling values the SDK does not recognize -- new event types, action types, error codes, enum values -- was already on main before it, and is what forward compatibility needs. What #651 added on top was tolerance for payloads that are malformed rather than merely new: a list sent as a scalar, an object sent as a string, a payload that is not an object. That is not forward compatibility, it is a defect being swallowed. A null list was already read as empty, so the new guards only caught genuinely broken data and turned it into an empty list or None. Worse, catching exceptions around a known variant meant one bad field silently downgraded a typed event to an untyped DeepAttrDict, with nothing to tell the caller why the type changed. Removes seam/parse.py, the isinstance guards in every from_dict, and the exception handling around known variants. The unrecognized-discriminator fallback that predates #651 is untouched, so unknown event types still yield a DeepAttrDict, unknown error codes still fall back per item, and unknown enum values still read as themselves. SeamActionAttemptUnknownStatusError stays. The action attempt contract is strict: waiting promises a succeeded attempt or a raise, and returning an unrecognized status as a success reports something the SDK cannot vouch for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
1 parent 4bab5aa commit b198012

38 files changed

Lines changed: 1540 additions & 3006 deletions

‎codegen/layouts/partials/resource-dataclass.hbs‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,8 @@
3131
{{#unless properties}}
3232
{{memberIndent}} # pylint: disable=unused-argument
3333
{{/unless}}
34-
{{memberIndent}} if not isinstance(d, dict):
35-
{{memberIndent}} d = {}
3634
{{memberIndent}} return cls(
3735
{{#each properties}}
38-
{{../memberIndent}} {{pythonIdentifier name}}={{#if isRequiredObject}}_required_object_from_dict(cls.{{nestedClassName}}, d.get("{{name}}")){{else}}{{#if isObject}}_object_from_dict(cls.{{nestedClassName}}, d.get("{{name}}")){{else}}{{#if isDiscriminatedObjectList}}_discriminated_list_from_dict(d.get("{{name}}"), cls._{{nestedClassName}}Variants, "{{discriminator}}"){{else}}{{#if isObjectList}}_object_list_from_dict(cls.{{nestedClassName}}, d.get("{{name}}")){{else}}{{#if isDictParam}}_record_from_dict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}}{{/if}}{{/if}}{{/if}}{{/if}},
36+
{{../memberIndent}} {{pythonIdentifier name}}={{#if isRequiredObject}}cls.{{nestedClassName}}.from_dict(d.get("{{name}}") or {}){{else}}{{#if isObject}}cls.{{nestedClassName}}.from_dict(d.get("{{name}}")) if d.get("{{name}}") is not None else None{{else}}{{#if isDiscriminatedObjectList}}[_from_discriminated_dict(i, cls._{{nestedClassName}}Variants, "{{discriminator}}") for i in d.get("{{name}}") or []]{{else}}{{#if isObjectList}}[cls.{{nestedClassName}}.from_dict(i) for i in d.get("{{name}}") or []]{{else}}{{#if isDictParam}}DeepAttrDict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}}{{/if}}{{/if}}{{/if}}{{/if}},
3937
{{/each}}
4038
{{memberIndent}} )

‎codegen/layouts/resource.hbs‎

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
from typing import Any, Dict, List, Literal, Optional, {{#if union.secondaryDiscriminator}}Tuple, {{/if}}Union{{#if union}}, cast{{/if}}
22
from dataclasses import dataclass
33
from ..deep_attr_dict import DeepAttrDict
4-
from ..parse import (
5-
discriminated_list_from_dict as _discriminated_list_from_dict,
6-
object_from_dict as _object_from_dict,
7-
object_list_from_dict as _object_list_from_dict,
8-
record_from_dict as _record_from_dict,
9-
required_object_from_dict as _required_object_from_dict,
10-
)
114
from ..resource_mapping import ResourceMapping
125

6+
{{#if hasDiscriminatedLists}}
7+
8+
def _from_discriminated_dict(
9+
d: Any, variants: Dict[str, Any], discriminator: str
10+
) -> Any:
11+
variant = variants.get(d.get(discriminator))
12+
return DeepAttrDict(d) if variant is None else variant.from_dict(d)
13+
{{/if}}
14+
1315
{{#each classes}}
1416
{{> resource-dataclass}}
1517

@@ -34,27 +36,11 @@ from ..resource_mapping import ResourceMapping
3436
def {{union.fromDictName}}(d: Any) -> {{union.className}}:
3537
"""Deserialize a known {{union.discriminator}}{{#if union.secondaryDiscriminator}} and {{union.secondaryDiscriminator}}{{/if}} variant.
3638

37-
An unrecognized discriminator, or a known one whose payload does not
38-
convert, returns ``DeepAttrDict`` so payloads from a newer API remain
39-
readable. The static return type covers known variants.
39+
Unknown discriminator values return ``DeepAttrDict`` so payloads from a
40+
newer API remain readable. The static return type covers known variants.
4041
"""
41-
if not isinstance(d, dict):
42-
return cast({{union.className}}, DeepAttrDict(d) if isinstance(d, dict) else d)
43-
{{#if union.secondaryDiscriminator}}
44-
key = (d.get("{{union.discriminator}}"), d.get("{{union.secondaryDiscriminator}}"))
45-
variant = (
46-
{{union.variantsName}}.get(cast(Tuple[str, str], key))
47-
if isinstance(key[0], str) and isinstance(key[1], str)
48-
else None
49-
)
50-
{{else}}
51-
key = d.get("{{union.discriminator}}")
52-
variant = {{union.variantsName}}.get(key) if isinstance(key, str) else None
53-
{{/if}}
42+
variant = {{union.variantsName}}.get({{#if union.secondaryDiscriminator}}(d.get("{{union.discriminator}}"), d.get("{{union.secondaryDiscriminator}}")){{else}}d.get("{{union.discriminator}}"){{/if}})
5443
if variant is None:
5544
return cast({{union.className}}, DeepAttrDict(d))
56-
try:
57-
return variant.from_dict(d)
58-
except Exception: # pylint: disable=broad-exception-caught
59-
return cast({{union.className}}, DeepAttrDict(d))
45+
return variant.from_dict(d)
6046
{{/if}}

‎seam/parse.py‎

Lines changed: 0 additions & 86 deletions
This file was deleted.

0 commit comments

Comments
 (0)