diff --git a/swh/graphql/app.py b/swh/graphql/app.py --- a/swh/graphql/app.py +++ b/swh/graphql/app.py @@ -36,6 +36,7 @@ resolvers.directory_entry_target, resolvers.search_result_target, resolvers.binary_string, + resolvers.date, scalars.id_scalar, scalars.datetime_scalar, scalars.swhid_scalar, diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -16,6 +16,7 @@ # Every scalar is expected to resolve this way # - As an attribute/item in the object/dict returned by a backend (eg: Origin.url) +import datetime from typing import Optional, Union from ariadne import ObjectType, UnionType @@ -23,6 +24,7 @@ from swh.graphql import resolvers as rs from swh.graphql.utils import utils +from swh.model.model import TimestampWithTimezone from .resolver_factory import ConnectionObjectFactory, NodeObjectFactory @@ -38,6 +40,7 @@ directory_entry: ObjectType = ObjectType("DirectoryEntry") search_result: ObjectType = ObjectType("SearchResult") binary_string: ObjectType = ObjectType("BinaryString") +date: ObjectType = ObjectType("Date") branch_target: UnionType = UnionType("BranchTarget") release_target: UnionType = UnionType("ReleaseTarget") @@ -312,3 +315,22 @@ @binary_string.field("base64") def binary_string_base64_resolver(obj: bytes, *args, **kw) -> str: return utils.get_b64_string(obj) + + +# Date object resolver + + +@date.field("date") +def date_date_resolver( + obj: TimestampWithTimezone, *args: GraphQLResolveInfo, **kw +) -> datetime.datetime: + # This will be serialised as a DateTime Scalar + return obj.to_datetime() + + +@date.field("offset") +def date_offset_resolver( + obj: TimestampWithTimezone, *args: GraphQLResolveInfo, **kw +) -> bytes: + # This will be serialised as a Binary string + return obj.offset_bytes diff --git a/swh/graphql/resolvers/revision.py b/swh/graphql/resolvers/revision.py --- a/swh/graphql/resolvers/revision.py +++ b/swh/graphql/resolvers/revision.py @@ -37,6 +37,10 @@ def directory_hash(self): # for RevisionDirectoryNode resolver return self._node.directory + @property + def committerDate(self): # To support the schema naming convention + return self._node.committer_date + @property def type(self): return self._node.type.value diff --git a/swh/graphql/resolvers/scalars.py b/swh/graphql/resolvers/scalars.py --- a/swh/graphql/resolvers/scalars.py +++ b/swh/graphql/resolvers/scalars.py @@ -3,14 +3,14 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information -from datetime import datetime +import datetime +from typing import Optional from ariadne import ScalarType from swh.graphql.errors import InvalidInputError from swh.graphql.utils import utils from swh.model.exceptions import ValidationError -from swh.model.model import TimestampWithTimezone from swh.model.swhids import CoreSWHID datetime_scalar = ScalarType("DateTime") @@ -26,13 +26,8 @@ @datetime_scalar.serializer -def serialize_datetime(value): - # FIXME, handle error and return None - if type(value) == TimestampWithTimezone: - value = value.to_datetime() - if type(value) == datetime: - return utils.get_formatted_date(value) - return None +def serialize_datetime(value: Optional[datetime.datetime]) -> Optional[str]: + return utils.get_formatted_date(value) if type(value) == datetime.datetime else None @swhid_scalar.value_parser diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -570,6 +570,21 @@ node: Revision } +""" +Object with Date values +""" +type Date { + """ + ISO-8601 encoded date string. + """ + date: DateTime + + """ + UTC offset + """ + offset: BinaryString +} + """ A revision object """ @@ -600,9 +615,14 @@ committer: Person """ - Revision date ISO-8601 encoded + Commit date """ - date: DateTime + committerDate: Date + + """ + Revision date + """ + date: Date """ Type of the revision, eg: git/hg @@ -691,9 +711,9 @@ author: Person """ - Release date ISO-8601 encoded + Release date """ - date: DateTime + date: Date """ Type of release target diff --git a/swh/graphql/tests/data.py b/swh/graphql/tests/data.py --- a/swh/graphql/tests/data.py +++ b/swh/graphql/tests/data.py @@ -112,6 +112,22 @@ ] +def get_revisions_with_none_date(): + return [ + Revision( + message=b"hello", + date=None, + committer=swh_model_data.COMMITTERS[0], + author=swh_model_data.COMMITTERS[0], + committer_date=swh_model_data.DATES[0], + type=RevisionType.GIT, + directory=b"\x01" * 20, + synthetic=False, + parents=(get_revisions()[0].id, get_revisions()[1].id), + ) + ] + + def get_directories_with_nested_path(): return [ Directory( @@ -158,7 +174,7 @@ GRAPHQL_EXTRA_TEST_OBJECTS = { "release": get_releases_with_target(), - "revision": get_revisions_with_parents(), + "revision": get_revisions_with_parents() + get_revisions_with_none_date(), "directory": get_directories_with_nested_path() + get_directories_with_special_name_entries(), "origin_visit_status": get_visit_with_multiple_status(), diff --git a/swh/graphql/tests/functional/test_release_node.py b/swh/graphql/tests/functional/test_release_node.py --- a/swh/graphql/tests/functional/test_release_node.py +++ b/swh/graphql/tests/functional/test_release_node.py @@ -43,7 +43,13 @@ text } } - date + date { + date + offset { + text + base64 + } + } targetType } } @@ -64,7 +70,15 @@ } if release.author else None, - "date": release.date.to_datetime().isoformat() if release.date else None, + "date": { + "date": release.date.to_datetime().isoformat(), + "offset": { + "text": release.date.offset_bytes.decode(), + "base64": base64.b64encode(release.date.offset_bytes).decode("ascii"), + }, + } + if release.date + else None, "targetType": release.target_type.value, } diff --git a/swh/graphql/tests/functional/test_revision.py b/swh/graphql/tests/functional/test_revision.py --- a/swh/graphql/tests/functional/test_revision.py +++ b/swh/graphql/tests/functional/test_revision.py @@ -3,12 +3,18 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information +import base64 + import pytest from swh.model.swhids import CoreSWHID from . import utils -from ..data import get_revisions, get_revisions_with_parents +from ..data import ( + get_revisions, + get_revisions_with_none_date, + get_revisions_with_parents, +) @pytest.mark.parametrize("revision", get_revisions()) @@ -42,7 +48,20 @@ text } } - date + date { + date + offset { + text + base64 + } + } + committerDate { + date + offset { + text + base64 + } + } type directory { swhid @@ -51,6 +70,7 @@ } """ data, _ = utils.get_query_response(client, query_str, swhid=str(revision.swhid())) + assert data["revision"] == { "swhid": str(revision.swhid()), "message": {"text": revision.message.decode()}, @@ -64,7 +84,26 @@ "name": {"text": revision.committer.name.decode()}, "email": {"text": revision.committer.email.decode()}, }, - "date": revision.date.to_datetime().isoformat(), + "date": { + "date": revision.date.to_datetime().isoformat(), + "offset": { + "text": revision.date.offset_bytes.decode(), + "base64": base64.b64encode(revision.date.offset_bytes).decode("ascii"), + }, + } + if revision.date + else None, + "committerDate": { + "date": revision.committer_date.to_datetime().isoformat(), + "offset": { + "text": revision.committer_date.offset_bytes.decode(), + "base64": base64.b64encode(revision.committer_date.offset_bytes).decode( + "ascii" + ), + }, + } + if revision.committer_date + else None, "type": revision.type.value, "directory": { "swhid": str(CoreSWHID(object_id=revision.directory, object_type="dir")) @@ -175,3 +214,23 @@ obj_type="revision", swhid=f"swh:1:rev:{unknown_sha1}", ) + + +def test_get_revisions_with_none_date(client): + revision_swhid = get_revisions_with_none_date()[0].swhid() + query_str = """ + query getRevision($swhid: SWHID!) { + revision(swhid: $swhid) { + swhid + date { + date + offset { + text + base64 + } + } + } + } + """ + data, _ = utils.get_query_response(client, query_str, swhid=str(revision_swhid)) + assert data == {"revision": {"swhid": str(revision_swhid), "date": None}} diff --git a/swh/graphql/tests/unit/resolvers/test_scalars.py b/swh/graphql/tests/unit/resolvers/test_scalars.py --- a/swh/graphql/tests/unit/resolvers/test_scalars.py +++ b/swh/graphql/tests/unit/resolvers/test_scalars.py @@ -16,14 +16,6 @@ assert scalars.serialize_id(b"test") == "74657374" -def test_serialize_datetime(): - assert scalars.serialize_datetime("invalid") is None - # python datetime - date = datetime.datetime(2020, 5, 17) - assert scalars.serialize_datetime(date) == date.isoformat() - # FIXME, Timestamp with timezone - - def test_validate_swhid_invalid(): with pytest.raises(InvalidInputError): scalars.validate_swhid("invalid") @@ -32,3 +24,12 @@ def test_validate_swhid(): swhid = scalars.validate_swhid(f"swh:1:rev:{'1' * 40}") assert str(swhid) == "swh:1:rev:1111111111111111111111111111111111111111" + + +def test_serialize_datetime_from_datetime(): + dt = datetime.datetime(2010, 1, 15, 2, 12, 10, 2, datetime.timezone.utc) + assert scalars.serialize_datetime(dt) == "2010-01-15T02:12:10.000002+00:00" + + +def test_serialize_datetime_invalid_input(): + assert scalars.serialize_datetime("test") is None