From 0a7c94a5d4b8247fd165ebe1f0cd5b843c8a9f8e Mon Sep 17 00:00:00 2001 From: Jang Rush Date: Fri, 10 Dec 2021 17:37:41 +0800 Subject: [PATCH 1/4] test: file scan --- tests/test_file.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_file.py b/tests/test_file.py index 76a6cbf..94ba45f 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -152,6 +152,14 @@ def test_query(): # type: () -> None assert isinstance(leancloud.File.query.first(), File) +@with_setup(setup_func) +def test_scan(): # type: () -> None + files = leancloud.Query("File").scan() + for f in files: + assert isinstance(f, File) + assert f.key + assert f.name + assert f.metadata @with_setup(setup_func) def test_save_external(): # type: () -> None From 9b19ef1ccb3355521511e646df97133d1dce6b53 Mon Sep 17 00:00:00 2001 From: Jang Rush Date: Mon, 13 Dec 2021 22:00:15 +0800 Subject: [PATCH 2/4] feat: refined file.key support To work around that scan does not return a `url` field. LC ticket: 38823 slack: https://xindong.slack.com/archives/C01TLAHFTBJ/p1639134311164500 --- leancloud/file_.py | 2 ++ leancloud/utils.py | 3 ++- tests/test_file.py | 10 +++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/leancloud/file_.py b/leancloud/file_.py index 2bdc517..fedca71 100644 --- a/leancloud/file_.py +++ b/leancloud/file_.py @@ -289,6 +289,8 @@ def _update_data(self, server_data): if "url" in server_data: self._url = server_data.get("url") self._successful_url = self._url + if "key" in server_data: + self.key = server_data.get("key") if "mime_type" in server_data: self._mime_type = server_data["mime_type"] if "metaData" in server_data: diff --git a/leancloud/utils.py b/leancloud/utils.py index 14afb6a..61e448a 100644 --- a/leancloud/utils.py +++ b/leancloud/utils.py @@ -137,8 +137,9 @@ def decode(key, value): if _type == "File": f = leancloud.File(value["name"]) meta_data = value.get("metaData") + key = value.get("key") if meta_data: - f._metadata = meta_data + f._metadata = meta_data f._url = value["url"] f._successful_url = value["url"] f.id = value["objectId"] diff --git a/tests/test_file.py b/tests/test_file.py index 94ba45f..57abf03 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -149,6 +149,10 @@ def test_query(): # type: () -> None assert f.url assert f.name assert f.metadata + if f.metadata.get("__source") == 'external': + assert f.url + else: + assert f.key assert isinstance(leancloud.File.query.first(), File) @@ -157,9 +161,13 @@ def test_scan(): # type: () -> None files = leancloud.Query("File").scan() for f in files: assert isinstance(f, File) - assert f.key assert f.name assert f.metadata + if f.metadata.get("__source") == 'external': + assert f.url + else: + assert f.key + @with_setup(setup_func) def test_save_external(): # type: () -> None From f621caccb5f7999a827b52858ce3919ec570081b Mon Sep 17 00:00:00 2001 From: Jang Rush Date: Tue, 14 Dec 2021 00:17:11 +0800 Subject: [PATCH 3/4] fix: set object updatedAt on creation --- leancloud/object_.py | 48 ++++++++++++++++++++++++++------------------ leancloud/utils.py | 26 ++++++++++++++++++++++++ tests/test_object.py | 1 + 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/leancloud/object_.py b/leancloud/object_.py index 1799a78..920822e 100644 --- a/leancloud/object_.py +++ b/leancloud/object_.py @@ -334,25 +334,18 @@ def _to_pointer(self): } def _merge_metadata(self, server_data): - for key in ("objectId", "createdAt", "updatedAt"): - if server_data.get(key) is None: - continue - if key == "objectId": - self.id = server_data[key] - else: - if isinstance(server_data[key], six.string_types): - dt = utils.decode(key, {"__type": "Date", "iso": server_data[key]}) - elif server_data[key]["__type"] == "Date": - dt = utils.decode(key, server_data[key]) - else: - raise TypeError("Invalid date type") - server_data[key] = dt - if key == "createdAt": - self.created_at = dt - elif key == "updatedAt": - self.updated_at = dt - else: - raise TypeError + object_id = server_data.get("objectId") + _created_at = utils.decode_date_string(server_data.get("createdAt")) + _updated_at = utils.decode_updated_at(server_data.get("updatedAt"), _created_at) + + if object_id is not None: + self.id = object_id + if _created_at is not None: + self.created_at = _created_at + if _updated_at is not None: + self.updated_at = _updated_at + + def validate(self, attrs): if "ACL" in attrs and not isinstance(attrs["ACL"], leancloud.ACL): @@ -370,6 +363,23 @@ def get(self, attr, default=None, deafult=None): # for backward compatibility if (deafult is not None) and (default is None): default = deafult + + # createdAt is stored as string in the cloud but used as datetime object on the client side. + # We need to make sure that `.created_at` and `.get("createdAt")` return the same value. + # Otherwise users will get confused. + if attr == "createdAt": + if self.created_at is None: + return None + else: + return self.created_at + + # Similar to createdAt. + if attr == "updatedAt": + if self.updated_at is None: + return None + else: + return self.updated_at + return self._attributes.get(attr, default) def relation(self, attr): diff --git a/leancloud/utils.py b/leancloud/utils.py index 61e448a..cf2c88a 100644 --- a/leancloud/utils.py +++ b/leancloud/utils.py @@ -146,6 +146,32 @@ def decode(key, value): return f +def decode_date_string(date_or_string): + if date_or_string is None: + return None + elif isinstance(date_or_string, six.string_types): + return decode_date_string({"__type": "Date", "iso": date_or_string}) + elif date_or_string["__type"] == "Date": + return arrow.get(iso8601.parse_date(date_or_string["iso"])).to("local").datetime + else: + raise TypeError("Invalid date type") + + +def decode_updated_at(updated_at_date_string, created_at_datetime): + updated_at = decode_date_string(updated_at_date_string) + if updated_at is None: + if created_at_datetime is None: + return None + else: + # When a new object is created, updatedAt will be set as the same value as createdAt on the cloud. + # However, the cloud will only return objectId and createdAt in REST API response, without updatedAt. + # Thus we need to set updatedAt as the same value as createdAt, consistent with the value on the cloud. + # This behaviour is consistent with other SDKs such as JavaScript and Go. + return created_at_datetime + else: + return updated_at + + def traverse_object(obj, callback, seen=None): seen = seen or set() diff --git a/tests/test_object.py b/tests/test_object.py index fdcf9c0..952a3cf 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -314,6 +314,7 @@ def test_fetch_when_save(): # type: () -> None foo = Foo() foo.set("counter", 1) foo.save() + assert foo.created_at == foo.updated_at assert foo.get("counter") == 1 foo_from_other_thread = leancloud.Query(Foo).get(foo.id) From d6ecd119d0eef1e91a6b81bf56b33a16c6b98e00 Mon Sep 17 00:00:00 2001 From: Jang Rush Date: Tue, 14 Dec 2021 00:19:04 +0800 Subject: [PATCH 4/4] fix: LCFile creaet_at and updated_at close #533 --- leancloud/file_.py | 18 ++++++++++++++++++ leancloud/utils.py | 10 +++++++--- tests/test_file.py | 6 ++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/leancloud/file_.py b/leancloud/file_.py index fedca71..b97322f 100644 --- a/leancloud/file_.py +++ b/leancloud/file_.py @@ -35,6 +35,8 @@ def __init__(self, name="", data=None, mime_type=None): self._name = name self.key = None self.id = None + self.created_at = None + self.updated_at = None self._url = None self._successful_url = None self._acl = None @@ -226,9 +228,18 @@ def _save_external(self): } response = client.post("/files".format(self._name), data) content = response.json() + self.id = content["objectId"] + self._successful_url = self._url + _created_at = utils.decode_date_string(content.get("createdAt")) + _updated_at = utils.decode_updated_at(content.get("updatedAt"), _created_at) + if _created_at is not None: + self.created_at = _created_at + if _updated_at is not None: + self.updated_at = _updated_at + def _save_to_qcloud(self, token, upload_url): headers = { "Authorization": token, @@ -295,6 +306,13 @@ def _update_data(self, server_data): self._mime_type = server_data["mime_type"] if "metaData" in server_data: self._metadata = server_data.get("metaData") + + _created_at = utils.decode_date_string(server_data.get("createdAt")) + _updated_at = utils.decode_updated_at(server_data.get("updatedAt"), _created_at) + if _created_at is not None: + self.created_at = _created_at + if _updated_at is not None: + self.updated_at = _updated_at def _get_file_token(self): data = { diff --git a/leancloud/utils.py b/leancloud/utils.py index cf2c88a..1474cb6 100644 --- a/leancloud/utils.py +++ b/leancloud/utils.py @@ -137,12 +137,16 @@ def decode(key, value): if _type == "File": f = leancloud.File(value["name"]) meta_data = value.get("metaData") - key = value.get("key") - if meta_data: - f._metadata = meta_data + file_key = value.get("key") + if file_key is not None: + f.key = file_key + if meta_data is not None: + f._metadata = meta_data f._url = value["url"] f._successful_url = value["url"] f.id = value["objectId"] + f.created_at = decode_date_string(value.get("createdAt")) + f.updated_at = decode_date_string(value.get("updatedAt")) return f diff --git a/tests/test_file.py b/tests/test_file.py index 57abf03..158666b 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -108,6 +108,7 @@ def test_save(): # type: () -> None assert f.name == "Blah.txt" assert f.mime_type == "text/plain" assert not f.url.endswith(".") + assert f.created_at == f.updated_at @with_setup(setup_func) @@ -118,6 +119,7 @@ def test_save_with_specified_key(): # type: () -> None f.save() assert f.id + assert f.created_at == f.updated_at assert f.name == "Blah.txt" assert f.mime_type == "text/plain" path = urlparse(f.url).path @@ -146,9 +148,11 @@ def test_query(): # type: () -> None files = leancloud.Query("File").find() for f in files: assert isinstance(f, File) + assert f.id assert f.url assert f.name assert f.metadata + assert f.created_at == f.updated_at if f.metadata.get("__source") == 'external': assert f.url else: @@ -161,6 +165,7 @@ def test_scan(): # type: () -> None files = leancloud.Query("File").scan() for f in files: assert isinstance(f, File) + assert f.created_at == f.updated_at assert f.name assert f.metadata if f.metadata.get("__source") == 'external': @@ -176,6 +181,7 @@ def test_save_external(): # type: () -> None f = File.create_with_url(file_name, file_url) f.save() assert f.id + assert f.created_at == f.updated_at file_on_cloud = File.create_without_data(f.id) file_on_cloud.fetch() assert file_on_cloud.name == file_name