Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions codegen/layouts/partials/route-method.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@

return {{#if isAsync}}await resolve_action_attempt_async{{else}}resolve_action_attempt{{/if}}(
client=self.client,
action_attempt=action_attempt_from_dict(res["action_attempt"]),
action_attempt=action_attempt_from_dict(unwrap(res, "action_attempt", "{{path}}")),
wait_for_action_attempt=wait_for_action_attempt
)
{{else if (eq returnType "None")}}

return None
{{else if (isListType returnType)}}

return [{{fromDict (listItemType returnType)}}(item) for item in res{{#each returnPath}}["{{this}}"]{{/each}}]
return [{{fromDict (listItemType returnType)}}(item) for item in unwrap_list(res, "{{returnPath.[0]}}", "{{path}}")]
{{else}}

return {{fromDict returnType}}(res{{#each returnPath}}["{{this}}"]{{/each}})
return {{fromDict returnType}}(unwrap(res, "{{returnPath.[0]}}", "{{path}}"))
{{/if}}
6 changes: 6 additions & 0 deletions codegen/layouts/route.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ from .{{module}} import {{abstractClassName}}, {{className}}, {{asyncAbstractCla
{{#if importResolveActionAttempt}}
from ..modules.action_attempts import resolve_action_attempt, resolve_action_attempt_async
{{/if}}
{{#if importUnwrap}}
from ..response import unwrap
{{/if}}
{{#if importUnwrapList}}
from ..response import unwrap_list
{{/if}}


{{> abstract-route-class abstractClass}}
Expand Down
14 changes: 14 additions & 0 deletions codegen/lib/layouts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export interface RouteLayoutContext {
}>
importResolveActionAttempt: boolean
importNull: boolean
importUnwrap: boolean
importUnwrapList: boolean
methods: MethodLayoutContext[]
}

Expand Down Expand Up @@ -130,6 +132,16 @@ export const setRouteLayoutContext = (cls: ClassModel): RouteLayoutContext => {
params.some(({ isNullable }) => isNullable),
)

const importUnwrap = methods.some(
({ returnPath, returnType }) =>
returnPath.length > 0 && !returnType.startsWith('List['),
)

const importUnwrapList = methods.some(
({ returnPath, returnType }) =>
returnPath.length > 0 && returnType.startsWith('List['),
)

const showPass =
cls.methods.length === 0 && cls.childClassIdentifiers.length === 0

Expand Down Expand Up @@ -172,6 +184,8 @@ export const setRouteLayoutContext = (cls: ClassModel): RouteLayoutContext => {
})),
importResolveActionAttempt,
importNull,
importUnwrap,
importUnwrapList,
methods,
}
}
1 change: 1 addition & 0 deletions seam/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .exceptions import (
SeamError,
SeamHttpApiError,
SeamHttpInvalidResponseError,
SeamHttpUnauthorizedError,
SeamHttpInvalidInputError,
SeamValidationError,
Expand Down
9 changes: 8 additions & 1 deletion seam/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections.abc import Mapping
from json import JSONDecodeError
from typing import Any, Dict, Optional
from importlib.metadata import version
import abc
Expand Down Expand Up @@ -68,7 +69,13 @@ def _handle_response(self, response: Response):
self._handle_error_response(response)

if "application/json" in response.headers.get("content-type", ""):
return response.json()
try:
return response.json()
except JSONDecodeError:
# A body that lies about its content type is handed on as
# text, so readers report an invalid response instead of
# leaking a decode error.
return response.text

return response.text

Expand Down
30 changes: 30 additions & 0 deletions seam/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,36 @@ class SeamError(Exception):


# HTTP
class SeamHttpInvalidResponseError(SeamError):
"""
Exception raised when a success response from the Seam API has an
unexpected shape, e.g., a proxy rewrote the body or the expected
response key is missing.

:ivar path: The request path that produced the response
:vartype path: str
:ivar response_key: The response key the SDK expected to read
:vartype response_key: str
"""

def __init__(self, path: str, response_key: str, reason: str):
"""
:param path: The request path that produced the response
:type path: str
:param response_key: The response key the SDK expected to read
:type response_key: str
:param reason: Description of how the response diverged
:type reason: str
"""

super().__init__(
f"Seam returned an invalid response for {path}: "
f'expected "{response_key}", {reason}'
)
self.path = path
self.response_key = response_key


class SeamHttpApiError(SeamError):
"""
Base exception for Seam HTTP API errors.
Expand Down
9 changes: 7 additions & 2 deletions seam/modules/action_attempts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ..exceptions import SeamActionAttemptFailedError, SeamActionAttemptTimeoutError
from ..options import SeamInvalidOptionsError
from ..resources import ActionAttempt, SuccessActionAttempt, action_attempt_from_dict
from ..response import unwrap

TIMEOUT = 5.0
POLLING_INTERVAL = 0.5
Expand Down Expand Up @@ -72,7 +73,9 @@ def get_action_attempt(client: SeamHttpClient, action_attempt_id: str) -> Action
"/action_attempts/get", params={"action_attempt_id": action_attempt_id}
)

return action_attempt_from_dict(res["action_attempt"])
return action_attempt_from_dict(
unwrap(res, "action_attempt", "/action_attempts/get")
)


def poll_until_ready(
Expand Down Expand Up @@ -142,7 +145,9 @@ async def get_action_attempt_async(
"/action_attempts/get", params={"action_attempt_id": action_attempt_id}
)

return action_attempt_from_dict(res["action_attempt"])
return action_attempt_from_dict(
unwrap(res, "action_attempt", "/action_attempts/get")
)


async def poll_until_ready_async(
Expand Down
52 changes: 52 additions & 0 deletions seam/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Read success response payloads defensively.

A 2xx response with an unexpected shape, e.g., a proxy rewrote the body or
the response key was renamed, raises the SDK's own error instead of leaking
a bare KeyError or TypeError from inside a generated route method.
"""

from typing import Any, Dict, List

from .exceptions import SeamHttpInvalidResponseError


def _read_response_key(res: Any, response_key: str, path: str) -> Any:
if not isinstance(res, dict):
raise SeamHttpInvalidResponseError(
path,
response_key,
f"got {type(res).__name__} instead of a response object",
)

if response_key not in res:
raise SeamHttpInvalidResponseError(
path, response_key, "which the response does not contain"
)

return res[response_key]


def unwrap(res: Any, response_key: str, path: str) -> Dict[str, Any]:
"""Read an object under the response key, or raise for a malformed response."""

value = _read_response_key(res, response_key, path)

if not isinstance(value, dict):
raise SeamHttpInvalidResponseError(
path, response_key, f"got {type(value).__name__} instead of an object"
)

return value


def unwrap_list(res: Any, response_key: str, path: str) -> List[Any]:
"""Read a list under the response key, or raise for a malformed response."""

value = _read_response_key(res, response_key, path)

if not isinstance(value, list):
raise SeamHttpInvalidResponseError(
path, response_key, f"got {type(value).__name__} instead of a list"
)

return value
50 changes: 38 additions & 12 deletions seam/routes/access_codes.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 15 additions & 2 deletions seam/routes/access_codes_simulate.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading