diff --git a/leancloud/file_.py b/leancloud/file_.py index 2bdc517..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, @@ -289,10 +300,19 @@ 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: 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/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 14afb6a..1474cb6 100644 --- a/leancloud/utils.py +++ b/leancloud/utils.py @@ -137,14 +137,45 @@ def decode(key, value): if _type == "File": f = leancloud.File(value["name"]) meta_data = value.get("metaData") - if 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 +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_file.py b/tests/test_file.py index 76a6cbf..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,12 +148,31 @@ 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: + assert f.key 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.created_at == f.updated_at + 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 @@ -160,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 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)