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

Use datetime instead of str for all model timestamps #352

Merged
merged 4 commits into from
Mar 28, 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
2 changes: 1 addition & 1 deletion docs/source/orm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ comments on a particular record, just like their :class:`~pyairtable.Table` equi
Comment(
id='comdVMNxslc6jG0Xe',
text='Hello, @[usrVMNxslc6jG0Xed]!',
created_time='2023-06-07T17:46:24.435891',
created_time=datetime.datetime(...),
last_updated_time=None,
mentioned={
'usrVMNxslc6jG0Xed': Mentioned(
Expand Down
2 changes: 1 addition & 1 deletion docs/source/tables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ and :meth:`~pyairtable.Table.add_comment` methods will return instances of
Comment(
id='comdVMNxslc6jG0Xe',
text='Hello, @[usrVMNxslc6jG0Xed]!',
created_time='2023-06-07T17:46:24.435891',
created_time=datetime.datetime(...),
last_updated_time=None,
mentioned={
'usrVMNxslc6jG0Xed': Mentioned(
Expand Down
4 changes: 2 additions & 2 deletions pyairtable/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def webhooks(self) -> List[Webhook]:
last_successful_notification_time=None,
notification_url="https://example.com",
last_notification_result=None,
expiration_time="2023-07-01T00:00:00.000Z",
expiration_time=datetime.datetime(...),
specification: WebhookSpecification(...)
)
]
Expand Down Expand Up @@ -264,7 +264,7 @@ def add_webhook(
CreateWebhookResponse(
id='ach00000000000001',
mac_secret_base64='c3VwZXIgZHVwZXIgc2VjcmV0',
expiration_time='2023-07-01T00:00:00.000Z'
expiration_time=datetime.datetime(...)
)

Raises:
Expand Down
2 changes: 1 addition & 1 deletion pyairtable/api/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ def comments(self, record_id: RecordId) -> List["pyairtable.models.Comment"]:
Comment(
id='comdVMNxslc6jG0Xe',
text='Hello, @[usrVMNxslc6jG0Xed]!',
created_time='2023-06-07T17:46:24.435891',
created_time=datetime.datetime(...),
last_updated_time=None,
mentioned={
'usrVMNxslc6jG0Xed': Mentioned(
Expand Down
21 changes: 19 additions & 2 deletions pyairtable/models/_base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from datetime import datetime
from functools import partial
from typing import Any, ClassVar, Dict, Iterable, Mapping, Optional, Set, Type, Union

import inflection
from typing_extensions import Self as SelfType

from pyairtable._compat import pydantic
from pyairtable.utils import _append_docstring_text
from pyairtable.utils import (
_append_docstring_text,
datetime_from_iso_str,
datetime_to_iso_str,
)


class AirtableModel(pydantic.BaseModel):
Expand All @@ -30,8 +35,16 @@ class Config:
_raw: Any = pydantic.PrivateAttr()

def __init__(self, **data: Any) -> None:
self._raw = data.copy()
# Convert JSON-serializable input data to the types expected by our model.
# For now this only converts ISO 8601 strings to datetime objects.
for field_model in self.__fields__.values():
for name in {field_model.name, field_model.alias}:
if not (value := data.get(name)):
continue
if isinstance(value, str) and field_model.type_ is datetime:
data[name] = datetime_from_iso_str(value)
super().__init__(**data)
self._raw = data

@classmethod
def from_api(
Expand Down Expand Up @@ -244,6 +257,10 @@ def save(self) -> None:
exclude=exclude,
exclude_none=(not self.__save_none),
)
# This undoes the finagling we do in __init__, converting datetime back to str.
for key in data:
if isinstance(value := data.get(key), datetime):
data[key] = datetime_to_iso_str(value)
response = self._api.request(self.__save_http_method, self._url, json=data)
if self.__reload_after_save:
self._reload(response)
Expand Down
3 changes: 2 additions & 1 deletion pyairtable/models/audit.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime
from typing import Any, Dict, List, Optional

from typing_extensions import TypeAlias
Expand Down Expand Up @@ -30,7 +31,7 @@ class AuditLogEvent(AirtableModel):
"""

id: str
timestamp: str
timestamp: datetime
action: str
actor: "AuditLogActor"
model_id: str
Expand Down
5 changes: 3 additions & 2 deletions pyairtable/models/comment.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime
from typing import Dict, Optional

from ._base import AirtableModel, CanDeleteModel, CanUpdateModel, update_forward_refs
Expand All @@ -19,7 +20,7 @@ class Comment(
Comment(
id='comdVMNxslc6jG0Xe',
text='Hello, @[usrVMNxslc6jG0Xed]!',
created_time='2023-06-07T17:46:24.435891',
created_time=datetime.datetime(...),
last_updated_time=None,
mentioned={
'usrVMNxslc6jG0Xed': Mentioned(
Expand Down Expand Up @@ -48,7 +49,7 @@ class Comment(
text: str

#: The ISO 8601 timestamp of when the comment was created.
created_time: str
created_time: datetime

#: The ISO 8601 timestamp of when the comment was last edited.
last_updated_time: Optional[str]
Expand Down
29 changes: 15 additions & 14 deletions pyairtable/models/schema.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import importlib
from datetime import datetime
from functools import partial
from typing import Any, Dict, Iterable, List, Literal, Optional, TypeVar, Union, cast

Expand Down Expand Up @@ -178,7 +179,7 @@ class InterfaceCollaborators(
_Collaborators,
url="meta/bases/{base.id}/interfaces/{key}",
):
created_time: str
created_time: datetime
group_collaborators: List["GroupCollaborator"] = _FL()
individual_collaborators: List["IndividualCollaborator"] = _FL()
invite_links: List["InterfaceInviteLink"] = _FL()
Expand Down Expand Up @@ -214,7 +215,7 @@ class Info(
):
state: str
created_by_user_id: str
created_time: str
created_time: datetime
share_id: str
type: str
is_password_protected: bool
Expand Down Expand Up @@ -348,15 +349,15 @@ class ViewSchema(CanDeleteModel, url="meta/bases/{base.id}/views/{self.id}"):


class GroupCollaborator(AirtableModel):
created_time: str
created_time: datetime
granted_by_user_id: str
group_id: str
name: str
permission_level: str


class IndividualCollaborator(AirtableModel):
created_time: str
created_time: datetime
granted_by_user_id: str
user_id: str
email: str
Expand All @@ -380,7 +381,7 @@ class InviteLink(CanDeleteModel, url="{invite_links._url}/{self.id}"):

id: str
type: str
created_time: str
created_time: datetime
invited_email: Optional[str]
referred_by_user_id: str
permission_level: str
Expand Down Expand Up @@ -427,7 +428,7 @@ class EnterpriseInfo(AirtableModel):
"""

id: str
created_time: str
created_time: datetime
group_ids: List[str]
user_ids: List[str]
workspace_ids: List[str]
Expand All @@ -447,7 +448,7 @@ class WorkspaceCollaborators(_Collaborators, url="meta/workspaces/{self.id}"):

id: str
name: str
created_time: str
created_time: datetime
base_ids: List[str]
restrictions: "WorkspaceCollaborators.Restrictions" = pydantic.Field(alias="workspaceRestrictions") # fmt: skip
group_collaborators: "WorkspaceCollaborators.GroupCollaborators" = _F("WorkspaceCollaborators.GroupCollaborators") # fmt: skip
Expand Down Expand Up @@ -527,7 +528,7 @@ def workspaces(self) -> Dict[str, "Collaborations.WorkspaceCollaboration"]:

class BaseCollaboration(AirtableModel):
base_id: str
created_time: str
created_time: datetime
granted_by_user_id: str
permission_level: str

Expand All @@ -536,7 +537,7 @@ class InterfaceCollaboration(BaseCollaboration):

class WorkspaceCollaboration(AirtableModel):
workspace_id: str
created_time: str
created_time: datetime
granted_by_user_id: str
permission_level: str

Expand All @@ -559,8 +560,8 @@ class UserInfo(
state: str
is_sso_required: bool
is_two_factor_auth_enabled: bool
last_activity_time: Optional[str]
created_time: Optional[str]
last_activity_time: Optional[datetime]
created_time: Optional[datetime]
enterprise_user_type: Optional[str]
invited_to_airtable_by_user_id: Optional[str]
is_managed: bool = False
Expand All @@ -581,8 +582,8 @@ class UserGroup(AirtableModel):
id: str
name: str
enterprise_account_id: str
created_time: str
updated_time: str
created_time: datetime
updated_time: datetime
members: List["UserGroup.Member"]
collaborations: "Collaborations" = pydantic.Field(default_factory=Collaborations)

Expand All @@ -592,7 +593,7 @@ class Member(AirtableModel):
first_name: str
last_name: str
role: str
created_time: str
created_time: datetime


# The data model is a bit confusing here, but it's designed for maximum reuse.
Expand Down
17 changes: 9 additions & 8 deletions pyairtable/models/webhook.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import base64
from datetime import datetime
from functools import partial
from hmac import HMAC
from typing import Any, Callable, Dict, Iterator, List, Optional, Union
Expand Down Expand Up @@ -30,7 +31,7 @@ class Webhook(CanDeleteModel, url="bases/{base.id}/webhooks/{self.id}"):
CreateWebhookResponse(
id='ach00000000000001',
mac_secret_base64='c3VwZXIgZHVwZXIgc2VjcmV0',
expiration_time='2023-07-01T00:00:00.000Z'
expiration_time=datetime.datetime(...)
)
>>> webhooks = base.webhooks()
>>> webhooks[0]
Expand All @@ -42,7 +43,7 @@ class Webhook(CanDeleteModel, url="bases/{base.id}/webhooks/{self.id}"):
last_successful_notification_time=None,
notification_url="https://example.com",
last_notification_result=None,
expiration_time="2023-07-01T00:00:00.000Z",
expiration_time=datetime.datetime(...),
specification: WebhookSpecification(...)
)
>>> webhooks[0].disable_notifications()
Expand Down Expand Up @@ -110,7 +111,7 @@ def payloads(
>>> iter_payloads = webhook.payloads()
>>> next(iter_payloads)
WebhookPayload(
timestamp="2022-02-01T21:25:05.663Z",
timestamp=datetime.datetime(...),
base_transaction_number=4,
payload_format="v0",
action_metadata=ActionMetadata(
Expand Down Expand Up @@ -204,7 +205,7 @@ def airtable_webhook():

base: _NestedId
webhook: _NestedId
timestamp: str
timestamp: datetime

@classmethod
def from_request(
Expand Down Expand Up @@ -241,7 +242,7 @@ def from_request(

class WebhookNotificationResult(AirtableModel):
success: bool
completion_timestamp: str
completion_timestamp: datetime
duration_ms: float
retry_number: int
will_be_retried: Optional[bool] = None
Expand Down Expand Up @@ -300,7 +301,7 @@ class CreateWebhookResponse(AirtableModel):
mac_secret_base64: str

#: The timestamp when the webhook will expire and be deleted.
expiration_time: Optional[str]
expiration_time: Optional[datetime]


class WebhookPayload(AirtableModel):
Expand All @@ -309,7 +310,7 @@ class WebhookPayload(AirtableModel):
`Webhooks payload <https://airtable.com/developers/web/api/model/webhooks-payload>`_.
"""

timestamp: str
timestamp: datetime
base_transaction_number: int
payload_format: str
action_metadata: Optional["WebhookPayload.ActionMetadata"]
Expand Down Expand Up @@ -372,7 +373,7 @@ class CellValuesByFieldId(AirtableModel):
cell_values_by_field_id: Dict[str, Any]

class RecordCreated(AirtableModel):
created_time: str
created_time: datetime
cell_values_by_field_id: Dict[str, Any]


Expand Down
17 changes: 10 additions & 7 deletions pyairtable/orm/model.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
from functools import lru_cache
from typing import Any, Dict, Iterable, List, Optional

Expand All @@ -16,6 +17,7 @@
from pyairtable.formulas import EQ, OR, RECORD_ID
from pyairtable.models import Comment
from pyairtable.orm.fields import AnyField, Field
from pyairtable.utils import datetime_from_iso_str, datetime_to_iso_str


class Model:
Expand Down Expand Up @@ -69,7 +71,7 @@ def api_key():
"""

id: str = ""
created_time: str = ""
created_time: Optional[datetime.datetime] = None
_deleted: bool = False
_fields: Dict[FieldName, Any]

Expand Down Expand Up @@ -219,7 +221,7 @@ def save(self) -> bool:
did_create = False

self.id = record["id"]
self.created_time = record["createdTime"]
self.created_time = datetime_from_iso_str(record["createdTime"])
return did_create

def delete(self) -> bool:
Expand Down Expand Up @@ -275,7 +277,8 @@ def to_record(self, only_writable: bool = False) -> RecordDict:
for field, value in self._fields.items()
if not (map_[field].readonly and only_writable)
}
return {"id": self.id, "createdTime": self.created_time, "fields": fields}
ct = datetime_to_iso_str(self.created_time) if self.created_time else ""
return {"id": self.id, "createdTime": ct, "fields": fields}

@classmethod
def from_record(cls, record: RecordDict) -> SelfType:
Expand All @@ -299,7 +302,7 @@ def from_record(cls, record: RecordDict) -> SelfType:
# any readonly fields, instead we directly set instance._fields.
instance = cls(id=record["id"])
instance._fields = field_values
instance.created_time = record["createdTime"]
instance.created_time = datetime_from_iso_str(record["createdTime"])
return instance

@classmethod
Expand Down Expand Up @@ -388,9 +391,9 @@ def batch_save(cls, models: List[SelfType]) -> None:
table = cls.get_table()
table.batch_update(update_records, typecast=cls._typecast())
created_records = table.batch_create(create_records, typecast=cls._typecast())
for model, created_record in zip(create_models, created_records):
model.id = created_record["id"]
model.created_time = created_record["createdTime"]
for model, record in zip(create_models, created_records):
model.id = record["id"]
model.created_time = datetime_from_iso_str(record["createdTime"])

@classmethod
def batch_delete(cls, models: List[SelfType]) -> None:
Expand Down
Loading
Loading