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
12 changes: 9 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,13 @@ Serializing URL search params

The Seam API parses URL search params as complex types.
If you call it with your own HTTP client,
``serialize_url_search_params`` is exported for that purpose:
``serialize_url_search_params`` is exported for that purpose.

.. note::

The ``_strict=true`` parameter is added to any non-empty query so the Seam API
uses strict, schema-aware parsing. A query with no serializable params remains
empty.

.. code-block:: python

Expand All @@ -604,10 +610,10 @@ as `URLSearchParams`_ does for the `reference implementation`_:
update_url_search_params(search_params, {"device_ids": ["device1", "device2"]})

list(search_params)
# => [('device_ids', 'device1'), ('device_ids', 'device2')]
# => [('device_ids', 'device1'), ('device_ids', 'device2'), ('_strict', 'true')]

str(search_params)
# => 'device_ids=device1&device_ids=device2'
# => 'device_ids=device1&device_ids=device2&_strict=true'

Pass either the query string or the pairs to your HTTP client.
A client may percent-encode a few characters differently than
Expand Down
5 changes: 2 additions & 3 deletions seam/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
from .seam_webhook import SeamWebhook
from svix.webhooks import WebhookVerificationError as SeamWebhookVerificationError
from .null import NULL, Null
from .url_search_params_serializer import (
UnserializableParamError,
UrlSearchParams,
from .url_search_params_serializer import UnserializableParamError, UrlSearchParams
from .strict_url_search_params_serializer import (
serialize_url_search_params,
update_url_search_params,
)
2 changes: 1 addition & 1 deletion seam/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
SeamHttpUnauthorizedError,
)
from .null import replace_null
from .url_search_params_serializer import serialize_url_search_params
from .strict_url_search_params_serializer import serialize_url_search_params

SDK_HEADERS = {
"seam-sdk-name": "seamapi/python",
Expand Down
20 changes: 20 additions & 0 deletions seam/strict_url_search_params_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Strict URL search parameter serializer used by the Seam SDK."""

from .url_search_params_serializer import (
Params,
UrlSearchParams,
serialize_url_search_params as _serialize_url_search_params,
update_url_search_params as _update_url_search_params,
)


def serialize_url_search_params(params: Params) -> str:
"""Serialize params with strict API validation enabled."""

return _serialize_url_search_params(params, strict=True)


def update_url_search_params(search_params: UrlSearchParams, params: Params) -> None:
"""Update params with strict API validation enabled."""

_update_url_search_params(search_params, params, strict=True)
17 changes: 14 additions & 3 deletions seam/url_search_params_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,24 +216,28 @@ def __iter__(self) -> Iterator[Tuple[str, str]]:
return iter(self._pairs)


def serialize_url_search_params(params: Params) -> str:
def serialize_url_search_params(params: Params, *, strict: bool = False) -> str:
"""Serializes params to a URL search param query string.

:param params: The params to serialize
:type params: Mapping[str, Any]
:param strict: Whether to add ``_strict=true`` to non-empty query strings
:type strict: bool

:returns: The query string, without a leading ``?``

:raises UnserializableParamError: If any param could not be serialized
"""

search_params = UrlSearchParams()
update_url_search_params(search_params, params)
update_url_search_params(search_params, params, strict=strict)

return search_params.to_string()


def update_url_search_params(search_params: UrlSearchParams, params: Params) -> None:
def update_url_search_params(
search_params: UrlSearchParams, params: Params, *, strict: bool = False
) -> None:
"""Updates existing URL search params with serialized params.

Existing params are preserved unless overwritten by a serialized param.
Expand All @@ -243,13 +247,20 @@ def update_url_search_params(search_params: UrlSearchParams, params: Params) ->
:type search_params: UrlSearchParams
:param params: The params to serialize
:type params: Mapping[str, Any]
:param strict: Whether to add ``_strict=true`` when the result is non-empty
:type strict: bool

:raises UnserializableParamError: If any param could not be serialized
"""

_nested_update_url_search_params(search_params, params, [])

search_params.sort()

if strict and len(search_params) > 0:
search_params.delete("_strict")
search_params.append("_strict", "true")


def _nested_update_url_search_params(
search_params: UrlSearchParams, params: Params, path: List[str]
Expand Down
2 changes: 1 addition & 1 deletion test/headers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def test_seam_sends_default_headers(recording_server):
[request] = requests

assert request["path"] == "/devices/get"
assert request["query"] == f"device_id={device_id}"
assert request["query"] == f"device_id={device_id}&_strict=true"
assert request["body"] is None

assert request["headers"]["seam-sdk-name"] == "seamapi/python"
Expand Down
13 changes: 7 additions & 6 deletions test/search_params_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def test_client_serializes_search_params(recording_server):
"&device_ids=device1"
"&device_ids=device2"
"&limit=20"
"&_strict=true"
)


Expand All @@ -48,7 +49,7 @@ def test_client_does_not_reencode_the_serialized_search_params(recording_server)

[request] = requests

assert request["query"] == "search=a+*%7E+b"
assert request["query"] == "search=a+*%7E+b&_strict=true"


def test_client_omits_search_params_set_to_none(recording_server):
Expand All @@ -59,7 +60,7 @@ def test_client_omits_search_params_set_to_none(recording_server):

[request] = requests

assert request["query"] == "limit=20"
assert request["query"] == "limit=20&_strict=true"


def test_client_serializes_search_params_set_to_null(recording_server):
Expand All @@ -70,7 +71,7 @@ def test_client_serializes_search_params_set_to_null(recording_server):

[request] = requests

assert request["query"] == "limit=20&search="
assert request["query"] == "limit=20&search=&_strict=true"


def test_client_sends_no_query_string_without_search_params(recording_server):
Expand All @@ -93,8 +94,8 @@ def test_client_serializes_search_params_of_every_verb(recording_server):
seam.client.delete("/access_codes/delete", params={"sync": True})

assert [(request["method"], request["query"]) for request in requests] == [
("GET", "device_ids=device1"),
("DELETE", "sync=true"),
("GET", "device_ids=device1&_strict=true"),
("DELETE", "sync=true&_strict=true"),
]


Expand Down Expand Up @@ -173,4 +174,4 @@ def test_client_serializes_the_search_params_of_a_generated_route(recording_serv

assert request["method"] == "GET"
assert request["path"] == "/devices/get"
assert request["query"] == "name=Front+Door"
assert request["query"] == "name=Front+Door&_strict=true"
10 changes: 10 additions & 0 deletions test/url_search_params_serializer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ def test_serializes_empty_object():
assert serialize_url_search_params({}) == ""


def test_strict_mode_adds_strict_to_non_empty_query_strings():
assert serialize_url_search_params({}, strict=True) == ""
assert serialize_url_search_params({"foo": "d"}, strict=True) == (
"foo=d&_strict=true"
)
assert (
serialize_url_search_params({"_strict": False}, strict=True) == "_strict=true"
)


def test_serializes_string():
assert serialize_url_search_params({"foo": "d"}) == "foo=d"
assert serialize_url_search_params({"foo": "null"}) == "foo=null"
Expand Down
Loading