Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: simplify santization #28

Merged
merged 1 commit into from
Mar 14, 2024
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
32 changes: 28 additions & 4 deletions src/waylay/sdk/api/_models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, List, Union
from datetime import date, datetime
from decimal import Decimal
from typing import TYPE_CHECKING, Any, Dict, List, Self, Union
from typing_extensions import TypeAliasType

try:
from typing import Self
except ImportError:
from typing_extensions import Self # <3.11

try:
from typing import Annotated
except ImportError:
Expand Down Expand Up @@ -38,15 +45,32 @@ def __model_construct_recursive(cls, obj: Any):
else:
return obj

def to_dict(self):
"""Convert model instance to dict."""
def to_dict(self) -> Dict[str, Any]:
"""Convert the model instance to dict."""
return self.model_dump()

def to_json(self) -> str:
"""Convert the model instance to a JSON-encoded string."""
return self.model_dump_json()

@classmethod
def from_dict(cls, obj: dict) -> Self:
"""Create a model instance from a dict."""
return cls.model_validate(obj)

@classmethod
def from_json(cls, json_data: str | bytes | bytearray) -> Self:
"""Create a model instance from a JSON-encoded string."""
return cls.model_validate_json(json_data)


Primitive: TypeAlias = Union[
str, bool, int, float, Decimal, bytes, datetime, date, object, None
]
Model: TypeAlias = TypeAliasType( # type: ignore[valid-type] #(https://github.com/python/mypy/issues/16614)
"Model",
Annotated[
Union[List["Model"], "_Model", Any], # type: ignore[misc,possible cyclic definition]
Union[List["Model"], "_Model", Primitive], # type: ignore[misc,possible cyclic definition]
"A basic model that acts like a `simpleNamespace`, or a collection over such models.",
],
)
82 changes: 5 additions & 77 deletions src/waylay/sdk/api/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
import re
import datetime
from urllib.parse import quote
from importlib import import_module
from inspect import isclass
from typing import Any, Mapping, Optional, cast, AsyncIterable, Union
from io import BufferedReader
from abc import abstractmethod
import warnings

from pydantic import BaseModel, ConfigDict, TypeAdapter, ValidationError
from pydantic_core import to_jsonable_python
from jsonpath_ng import parse as jsonpath_parse # type: ignore[import-untyped]
from httpx import QueryParams, USE_CLIENT_DEFAULT
import httpx._client as httpxc
Expand Down Expand Up @@ -73,11 +73,9 @@ def build_request(
"""Build the HTTP request params needed by the request."""
method = _validate_method(method)
url = _interpolate_resource_path(resource_path, path_params)
params = _sanitize_for_serialization(params)
headers = _sanitize_for_serialization(headers)
files = _sanitize_files_parameters(files)
json = _sanitize_for_serialization(json)
data = _sanitize_for_serialization(data)
params = to_jsonable_python(params, by_alias=True)
json = to_jsonable_python(json, by_alias=True)
data = to_jsonable_python(data, by_alias=True)
return self.http_client.build_request(
method,
url,
Expand Down Expand Up @@ -172,7 +170,7 @@ def build_params(
api_params: Optional[Mapping[str, Any]], extra_params: Optional[QueryParamTypes]
) -> Optional[QueryParamTypes]:
"""Sanitize and merge parameters."""
api_params = cast(dict, _sanitize_for_serialization(api_params))
api_params = cast(dict, to_jsonable_python(api_params, by_alias=True))
if not api_params:
return extra_params
if not extra_params:
Expand Down Expand Up @@ -232,70 +230,10 @@ def _interpolate_resource_path(
return resource_path


def _sanitize_for_serialization(obj):
"""Build a JSON POST object.

If obj is None, return None.
If obj is str, int, long, float, bool, return directly.
If obj is datetime.datetime, datetime.date convert to string in iso8601 format.
If obj is list, sanitize each element in the list.
If obj is dict, return the dict.
If obj is OpenAPI model, return the properties dict.

:param obj: The data to serialize.
:return: The serialized form of data.
"""
if obj is None:
return None
elif isinstance(obj, _PRIMITIVE_TYPES + _PRIMITIVE_BYTE_TYPES):
return obj
elif isinstance(obj, list):
return [_sanitize_for_serialization(sub_obj) for sub_obj in obj]
elif isinstance(obj, tuple):
return tuple(_sanitize_for_serialization(sub_obj) for sub_obj in obj)
elif isinstance(obj, (datetime.datetime, datetime.date)):
return obj.isoformat()

elif isinstance(obj, dict):
obj_dict = obj
else:
# Convert model obj to dict except
# attributes `openapi_types`, `attribute_map`
# and attributes which value is not None.
# Convert attribute name to json key in
# model definition for request.
try:
obj_dict = obj.to_dict()
except AttributeError:
return obj

return {key: _sanitize_for_serialization(val) for key, val in obj_dict.items()}


def _deserialize(data: Any, klass: Any):
"""Deserializes response content into a `klass` instance."""
if isinstance(klass, str) and klass in _CLASS_MAPPING:
klass = _CLASS_MAPPING[klass]
elif isinstance(klass, str):
if klass.startswith("List["):
inner_kls = re.match(r"List\[(.*)]", klass).group(1) # type: ignore[union-attr]
return [_deserialize(sub_data, inner_kls) for sub_data in data]
elif klass.startswith("Dict["):
match = re.match(r"Dict\[([^,]*), (.*)]", klass)
(key_kls, val_kls) = (match.group(1), match.group(2)) # type: ignore[union-attr]
return {
_deserialize(k, key_kls): _deserialize(v, val_kls)
for k, v in data.items()
}
elif "." in klass:
try:
# get the actual class from the class name
[types_module_name, class_name] = klass.rsplit(".", 1)
types_module = import_module(types_module_name)
klass = getattr(types_module, class_name)
except (AttributeError, ValueError, TypeError, ImportError):
return _MODEL_TYPE_ADAPTER.validate_python(data)

config = (
ConfigDict(arbitrary_types_allowed=True)
if isclass(klass) and not issubclass(klass, BaseModel)
Expand All @@ -320,13 +258,3 @@ def _deserialize(data: Any, klass: Any):
exc_info=exc2,
)
return data


def _sanitize_files_parameters(files=Optional[RequestFiles]):
"""Build form parameters.

:param files: File parameters.
:return: Form parameters with files.

"""
return files
79 changes: 6 additions & 73 deletions test/unit/api/__snapshots__/api_client_test.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -127,22 +127,7 @@
_Model(name='Lord Biscuit, Master of Naps', owner=_Model(id=123, name='Simon'), tag='doggo'),
)
# ---
# name: test_deserialize[json_dict_*_dict]
tuple(
b'{"message": "some not found message", "code": "RESOURCE_NOT_FOUND"}',
201,
dict({
'*': 'Dict[str, str]',
}),
None,
'dict',
dict({
'code': 'RESOURCE_NOT_FOUND',
'message': 'some not found message',
}),
)
# ---
# name: test_deserialize[json_dict_*_union]
# name: test_deserialize[json_dict_*_dummy_union]
tuple(
b'{"name": "Lord Biscuit, Master of Naps", "owner": {"id": 123, "name": "Simon"}, "tag": "doggo"}',
200,
Expand All @@ -154,12 +139,12 @@
Pet(name='Lord Biscuit, Master of Naps', owner=PetOwner(id=123, name='Simon'), tag='doggo'),
)
# ---
# name: test_deserialize[json_dict_*_union_str]
# name: test_deserialize[json_dict_*_union]
tuple(
b'{"name": "Lord Biscuit, Master of Naps", "owner": {"id": 123, "name": "Simon"}, "tag": "doggo"}',
200,
dict({
'*': 'unit.api.example.pet_model.PetUnion',
'*': typing.Union[unit.api.example.pet_model.PetList, unit.api.example.pet_model.Pet],
}),
None,
'Pet',
Expand Down Expand Up @@ -254,18 +239,6 @@
Pet(name='Lord Biscuit, Master of Naps', owner=PetOwner(id=123, name='Simon'), tag='doggo'),
)
# ---
# name: test_deserialize[json_dict_modelstr]
tuple(
b'{"name": "Lord Biscuit, Master of Naps", "owner": {"id": 123, "name": "Simon"}, "tag": "doggo"}',
200,
dict({
'200': 'unit.api.example.pet_model.Pet',
}),
None,
'Pet',
Pet(name='Lord Biscuit, Master of Naps', owner=PetOwner(id=123, name='Simon'), tag='doggo'),
)
# ---
# name: test_deserialize[json_dict_no_mapping]
tuple(
b'{"message": "some not found message", "code": "RESOURCE_NOT_FOUND"}',
Expand Down Expand Up @@ -333,30 +306,6 @@
Pet(name='Lord Biscuit, Master of Naps', owner=PetOwner(id=123, name='Simon'), tag='doggo'),
)
# ---
# name: test_deserialize[json_dict_wrongmodelstr]
tuple(
b'{"name": "Lord Biscuit, Master of Naps", "owner": {"id": 123, "name": "Simon"}, "tag": "doggo"}',
200,
dict({
'200': 'unit.api.example.pet_model.Unexisting',
}),
None,
'_Model',
_Model(name='Lord Biscuit, Master of Naps', owner=_Model(id=123, name='Simon'), tag='doggo'),
)
# ---
# name: test_deserialize[json_dict_wrongmodulestr]
tuple(
b'{"name": "Lord Biscuit, Master of Naps", "owner": {"id": 123, "name": "Simon"}, "tag": "doggo"}',
200,
dict({
'200': 'some.unexisting.module.Pet',
}),
None,
'_Model',
_Model(name='Lord Biscuit, Master of Naps', owner=_Model(id=123, name='Simon'), tag='doggo'),
)
# ---
# name: test_deserialize[json_list]
tuple(
b'["hello", "world", 123, {"key": "value"}]',
Expand All @@ -378,7 +327,7 @@
b'{"pets": [{"name": "Lord Biscuit, Master of Naps", "owner": {"id": 123, "name": "Simon"}, "tag": "doggo"}, {"name": "Lord Biscuit, Master of Naps", "owner": {"id": 123, "name": "Simon"}, "tag": "doggo"}]}',
200,
dict({
'*': 'unit.api.example.pet_model.PetUnion',
'*': typing.Union[unit.api.example.pet_model.PetList, unit.api.example.pet_model.Pet],
}),
None,
'PetList',
Expand All @@ -401,22 +350,6 @@
]),
)
# ---
# name: test_deserialize[json_list_X_list_int_str]
tuple(
b'["11", "22", 33]',
200,
dict({
'2XX': 'List[int]',
}),
None,
'list',
list([
11,
22,
33,
]),
)
# ---
# name: test_deserialize[json_list_X_union]
tuple(
b'["hello", "world", 123, {"key": "value"}]',
Expand Down Expand Up @@ -468,7 +401,7 @@
b'{"name": "Lord Biscuit, Master of Naps", "owner": {"id": 123, "name": "Simon"}, "tag": "doggo"}',
200,
dict({
'200': 'List[str]',
'200': typing.List[ForwardRef('str')],
}),
'[*].name',
'list',
Expand All @@ -482,7 +415,7 @@
b'{"pets": [{"name": "Lord Biscuit, Master of Naps", "owner": {"id": 123, "name": "Simon"}, "tag": "doggo"}, {"name": "Lord Biscuit, Master of Naps", "owner": {"id": 123, "name": "Simon"}, "tag": "doggo"}]}',
200,
dict({
'200': 'List[str]',
'200': typing.List[str],
}),
'pets[*].name',
'list',
Expand Down
Loading