diff --git a/mypy.ini b/mypy.ini --- a/mypy.ini +++ b/mypy.ini @@ -43,3 +43,6 @@ [mypy-systemd.*] ignore_missing_imports = True + +[mypy-attrs_strict.*] +ignore_missing_imports = True diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ Deprecated PyYAML sentry-sdk +attrs_strict >= 0.0.7 diff --git a/swh/core/api/model.py b/swh/core/api/model.py new file mode 100644 --- /dev/null +++ b/swh/core/api/model.py @@ -0,0 +1,28 @@ +# Copyright (C) 2015-2020 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +import attr + +from typing import ( + Generic, + List, + Optional, + TypeVar, +) + +from attrs_strict import type_validator + + +T = TypeVar("T") + + +@attr.s(frozen=True) +class PagedResult(Generic[T]): + """Represents a page of results; with a token to get the next page""" + + results = attr.ib(type=List[T], default=[]) + next_page_token = attr.ib( + type=Optional[str], default=None, validator=type_validator() + ) diff --git a/swh/core/api/serializers.py b/swh/core/api/serializers.py --- a/swh/core/api/serializers.py +++ b/swh/core/api/serializers.py @@ -18,6 +18,8 @@ from typing import Any, Dict, Union, Tuple from requests import Response +from swh.core.api.model import PagedResult + def encode_datetime(dt: datetime.datetime) -> str: """Wrapper of datetime.datetime.isoformat() that forbids naive datetimes.""" @@ -26,6 +28,20 @@ return dt.isoformat() +def _encode_paged_result(obj: PagedResult) -> Dict[str, Any]: + """Serialize PagedResult to a Dict.""" + return { + "__type__": type(obj).__name__, + "results": obj.results, + "next_page_token": obj.next_page_token, + } + + +def _decode_paged_result(obj: Dict[str, Any]) -> PagedResult: + """Deserialize Dict into PagedResult""" + return PagedResult(results=obj["results"], next_page_token=obj["next_page_token"],) + + ENCODERS = [ (arrow.Arrow, "arrow", arrow.Arrow.isoformat), (datetime.datetime, "datetime", encode_datetime), @@ -39,6 +55,7 @@ }, ), (UUID, "uuid", str), + (PagedResult, "core_class", _encode_paged_result), # Only for JSON: (bytes, "bytes", lambda o: base64.b85encode(o).decode("ascii")), ] @@ -48,6 +65,7 @@ "datetime": lambda d: iso8601.parse_date(d, default_timezone=None), "timedelta": lambda d: datetime.timedelta(**d), "uuid": UUID, + "core_class": _decode_paged_result, # Only for JSON: "bytes": base64.b85decode, } diff --git a/swh/core/api/tests/test_serializers.py b/swh/core/api/tests/test_serializers.py --- a/swh/core/api/tests/test_serializers.py +++ b/swh/core/api/tests/test_serializers.py @@ -13,6 +13,7 @@ import arrow import requests +from swh.core.api.model import PagedResult from swh.core.api.serializers import ( SWHJSONDecoder, SWHJSONEncoder, @@ -48,33 +49,50 @@ TZ = datetime.timezone(datetime.timedelta(minutes=118)) +DATA_BYTES = b"123456789\x99\xaf\xff\x00\x12" +ENCODED_DATA_BYTES = {"swhtype": "bytes", "d": "F)}kWH8wXmIhn8j01^"} + +DATA_ARROW = arrow.get("2018-04-25T16:17:53.533672+00:00") +ENCODED_DATA_ARROW = {"swhtype": "arrow", "d": "2018-04-25T16:17:53.533672+00:00"} + DATA = { - "bytes": b"123456789\x99\xaf\xff\x00\x12", + "bytes": DATA_BYTES, "datetime_tz": datetime.datetime(2015, 3, 4, 18, 25, 13, 1234, tzinfo=TZ,), "datetime_utc": datetime.datetime( 2015, 3, 4, 18, 25, 13, 1234, tzinfo=datetime.timezone.utc ), "datetime_delta": datetime.timedelta(64), - "arrow_date": arrow.get("2018-04-25T16:17:53.533672+00:00"), + "arrow_date": DATA_ARROW, "swhtype": "fake", "swh_dict": {"swhtype": 42, "d": "test"}, "random_dict": {"swhtype": 43}, "uuid": UUID("cdd8f804-9db6-40c3-93ab-5955d3836234"), + "paged-result": PagedResult( + results=["data0", DATA_BYTES, DATA_ARROW], next_page_token="10" + ), } ENCODED_DATA = { - "bytes": {"swhtype": "bytes", "d": "F)}kWH8wXmIhn8j01^"}, + "bytes": ENCODED_DATA_BYTES, "datetime_tz": {"swhtype": "datetime", "d": "2015-03-04T18:25:13.001234+01:58",}, "datetime_utc": {"swhtype": "datetime", "d": "2015-03-04T18:25:13.001234+00:00",}, "datetime_delta": { "swhtype": "timedelta", "d": {"days": 64, "seconds": 0, "microseconds": 0}, }, - "arrow_date": {"swhtype": "arrow", "d": "2018-04-25T16:17:53.533672+00:00"}, + "arrow_date": ENCODED_DATA_ARROW, "swhtype": "fake", "swh_dict": {"swhtype": 42, "d": "test"}, "random_dict": {"swhtype": 43}, "uuid": {"swhtype": "uuid", "d": "cdd8f804-9db6-40c3-93ab-5955d3836234"}, + "paged-result": { + "d": { + "__type__": "PagedResult", + "results": ["data0", ENCODED_DATA_BYTES, ENCODED_DATA_ARROW], + "next_page_token": "10", + }, + "swhtype": "core_class", + }, }