Skip to content

Commit

Permalink
fold CanDeleteModel, CanUpdateModel back into RestfulModel
Browse files Browse the repository at this point in the history
  • Loading branch information
mesozoic committed Oct 21, 2024
1 parent c5166c0 commit 1d225f8
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 93 deletions.
108 changes: 61 additions & 47 deletions pyairtable/models/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,64 @@ class RestfulModel(AirtableModel):
Subclasses can pass a number of keyword arguments to control serialization behavior:
* ``url=``: format string for building the URL to be used when saving changes to this model.
* ``allow_update=True``: allow calling save() to write changes to the API.
* ``allow_delete=True``: allow calling delete() to post a DELETE request to the API.
* ``writable=``: field names that can be modified; all others are implicitly read-only.
* ``readonly=``: field names that cannot be modified; all others are implicitly writable.
If ``allow_update`` is True, the following keyword arguments can be used to control serialization:
* ``writable=``: field names that should be written to API on ``save()`` (default: all)
* ``readonly=``: field names that should not be written to API on ``save()`` (default: none)
* ``save_null_values=``: whether ``save()`` should write nulls (default: true)
* ``save_method=``: HTTP method to use for save requests (default: PATCH)
* ``reload_after_save=``: whether to reload the model after saving (default: true)
"""

__url_pattern: ClassVar[str] = ""
__allow_update: ClassVar[bool] = False
__allow_delete: ClassVar[bool] = False
__writable: ClassVar[Optional[Iterable[str]]] = None
__readonly: ClassVar[Optional[Iterable[str]]] = None

# The following are only relevant if allow_update is True:
__save_null_values: ClassVar[bool] = True
__save_http_method: ClassVar[str] = "PATCH"
__reload_after_save: ClassVar[bool] = True

_api: "pyairtable.api.api.Api" = pydantic.PrivateAttr()
_url: str = pydantic.PrivateAttr(default="")
_url_context: Any = None

def __init_subclass__(cls, **kwargs: Any) -> None:
cls.__url_pattern = kwargs.pop("url", cls.__url_pattern)
cls.__allow_update = kwargs.pop("allow_update", cls.__allow_update)
cls.__allow_delete = kwargs.pop("allow_delete", cls.__allow_delete)

if "writable" in kwargs and "readonly" in kwargs:
raise ValueError("incompatible kwargs 'writable' and 'readonly'")
cls.__writable = kwargs.pop("writable", cls.__writable)
cls.__readonly = kwargs.pop("readonly", cls.__readonly)
if cls.__writable:
_append_docstring_text(
cls,
"The following fields can be modified: "
+ ", ".join(f"``{field}``" for field in cls.__writable),
)
if cls.__readonly:
_append_docstring_text(
cls,
"The following fields are read-only and cannot be modified:\n"
+ ", ".join(f"``{field}``" for field in cls.__readonly),
)

if cls.__allow_update:
cls.__save_http_method = kwargs.pop("save_method", cls.__save_http_method)
cls.__save_null_values = bool(
kwargs.pop("save_null_values", cls.__save_null_values)
)
cls.__reload_after_save = bool(
kwargs.pop("reload_after_save", cls.__reload_after_save)
)
super().__init_subclass__()

def _set_api(self, api: "pyairtable.api.api.Api", context: Dict[str, Any]) -> None:
Expand Down Expand Up @@ -172,11 +220,7 @@ def _reload(self, obj: Optional[Dict[str, Any]] = None) -> None:
{key: copyable.__dict__.get(key) for key in type(self).__fields__}
)


class CanDeleteModel(RestfulModel):
"""
Mix-in for RestfulModel that allows a model to be deleted.
"""
# formerly CanDeleteModel:

_deleted: bool = pydantic.PrivateAttr(default=False)

Expand All @@ -191,52 +235,17 @@ def delete(self) -> None:
"""
Delete the record on the server and mark this instance as deleted.
"""
if not self.__allow_delete:
# Back when CanUpdateModel was a mixin, this would raise AttributeError.
# To maintain compatibility with existing try/except code, we raise that
# here instead of something a bit more sensible like TypeError.
raise AttributeError(f"{self.__class__.__name__}.delete() is not supported")
if not self._url:
raise RuntimeError("delete() called with no URL specified")
self._api.request("DELETE", self._url)
self._deleted = True


class CanUpdateModel(RestfulModel):
"""
Mix-in for RestfulModel that allows a model to be modified and saved.
Subclasses can pass a number of keyword arguments to control serialization behavior:
* ``writable=``: field names that should be written to API on ``save()``.
* ``readonly=``: field names that should not be written to API on ``save()``.
* ``save_null_values=``: boolean indicating whether ``save()`` should write nulls (default: true)
"""

__writable: ClassVar[Optional[Iterable[str]]] = None
__readonly: ClassVar[Optional[Iterable[str]]] = None
__save_none: ClassVar[bool] = True
__save_http_method: ClassVar[str] = "PATCH"
__reload_after_save: ClassVar[bool] = True

def __init_subclass__(cls, **kwargs: Any) -> None:
if "writable" in kwargs and "readonly" in kwargs:
raise ValueError("incompatible kwargs 'writable' and 'readonly'")
cls.__writable = kwargs.pop("writable", cls.__writable)
cls.__readonly = kwargs.pop("readonly", cls.__readonly)
cls.__save_none = bool(kwargs.pop("save_null_values", cls.__save_none))
cls.__save_http_method = kwargs.pop("save_method", cls.__save_http_method)
cls.__reload_after_save = bool(
kwargs.pop("reload_after_save", cls.__reload_after_save)
)
if cls.__writable:
_append_docstring_text(
cls,
"The following fields can be modified and saved: "
+ ", ".join(f"``{field}``" for field in cls.__writable),
)
if cls.__readonly:
_append_docstring_text(
cls,
"The following fields are read-only and cannot be modified:\n"
+ ", ".join(f"``{field}``" for field in cls.__readonly),
)
super().__init_subclass__(**kwargs)
# formerly CanUpdateModel:

def save(self) -> None:
"""
Expand All @@ -245,6 +254,11 @@ def save(self) -> None:
Will raise ``RuntimeError`` if the record has been deleted.
"""
if not self.__allow_update:
# Back when CanUpdateModel was a mixin, this would raise AttributeError.
# To maintain compatibility with existing try/except code, we raise that
# here instead of something a bit more sensible like TypeError.
raise AttributeError(f"{self.__class__.__name__}.save() is not supported")
if getattr(self, "_deleted", None):
raise RuntimeError("save() called after delete()")
if not self._url:
Expand All @@ -255,7 +269,7 @@ def save(self) -> None:
by_alias=True,
include=include,
exclude=exclude,
exclude_none=(not self.__save_none),
exclude_none=(not self.__save_null_values),
)
# This undoes the finagling we do in __init__, converting datetime back to str.
for key in data:
Expand Down
7 changes: 4 additions & 3 deletions pyairtable/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@

from pyairtable._compat import pydantic

from ._base import AirtableModel, CanDeleteModel, CanUpdateModel, update_forward_refs
from ._base import AirtableModel, RestfulModel, update_forward_refs
from .collaborator import Collaborator


class Comment(
CanUpdateModel,
CanDeleteModel,
RestfulModel,
allow_update=True,
allow_delete=True,
writable=["text"],
url="{record_url}/comments/{self.id}",
):
Expand Down
33 changes: 17 additions & 16 deletions pyairtable/models/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,7 @@
from pyairtable._compat import pydantic
from pyairtable.api.types import AddCollaboratorDict

from ._base import (
AirtableModel,
CanDeleteModel,
CanUpdateModel,
RestfulModel,
update_forward_refs,
)
from ._base import AirtableModel, RestfulModel, update_forward_refs

_T = TypeVar("_T", bound=Any)
_FL = partial(pydantic.Field, default_factory=list)
Expand Down Expand Up @@ -208,8 +202,9 @@ class BaseShares(AirtableModel):
shares: List["BaseShares.Info"]

class Info(
CanUpdateModel,
CanDeleteModel,
RestfulModel,
allow_update=True,
allow_delete=True,
url="meta/bases/{base.id}/shares/{self.share_id}",
writable=["state"],
reload_after_save=False,
Expand Down Expand Up @@ -271,7 +266,8 @@ def table(self, id_or_name: str) -> "TableSchema":


class TableSchema(
CanUpdateModel,
RestfulModel,
allow_update=True,
save_null_values=False,
writable=["name", "description"],
url="meta/bases/{base.id}/tables/{self.id}",
Expand Down Expand Up @@ -327,7 +323,9 @@ def view(self, id_or_name: str) -> "ViewSchema":
return _find(self.views, id_or_name)


class ViewSchema(CanDeleteModel, url="meta/bases/{base.id}/views/{self.id}"):
class ViewSchema(
RestfulModel, allow_delete=True, url="meta/bases/{base.id}/views/{self.id}"
):
"""
Metadata for a view.
Expand Down Expand Up @@ -375,7 +373,7 @@ class BaseGroupCollaborator(GroupCollaborator):

# URL generation for an InviteLink assumes that it is nested within
# a RestfulModel class named "InviteLink" that provides URL context.
class InviteLink(CanDeleteModel, url="{invite_links._url}/{self.id}"):
class InviteLink(RestfulModel, allow_delete=True, url="{invite_links._url}/{self.id}"):
"""
Represents an `invite link <https://airtable.com/developers/web/api/model/invite-link>`__.
"""
Expand Down Expand Up @@ -457,7 +455,8 @@ class WorkspaceCollaborators(_Collaborators, url="meta/workspaces/{self.id}"):
invite_links: "WorkspaceCollaborators.InviteLinks" = _F("WorkspaceCollaborators.InviteLinks") # fmt: skip

class Restrictions(
CanUpdateModel,
RestfulModel,
allow_update=True,
url="{workspace_collaborators._url}/updateRestrictions",
save_method="POST",
reload_after_save=False,
Expand Down Expand Up @@ -544,8 +543,9 @@ class WorkspaceCollaboration(AirtableModel):


class UserInfo(
CanUpdateModel,
CanDeleteModel,
RestfulModel,
allow_update=True,
allow_delete=True,
url="{enterprise.url}/users/{self.id}",
writable=["state", "email", "first_name", "last_name"],
):
Expand Down Expand Up @@ -1017,7 +1017,8 @@ class UnknownFieldConfig(AirtableModel):


class _FieldSchemaBase(
CanUpdateModel,
RestfulModel,
allow_update=True,
save_null_values=False,
writable=["name", "description"],
url="meta/bases/{base.id}/tables/{table_schema.id}/fields/{self.id}",
Expand Down
8 changes: 6 additions & 2 deletions pyairtable/models/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@
from pyairtable._compat import pydantic
from pyairtable.api.types import RecordId

from ._base import AirtableModel, CanDeleteModel, update_forward_refs
from ._base import AirtableModel, RestfulModel, update_forward_refs

# Shortcuts to avoid lots of line wrapping
FD: Callable[[], Any] = partial(pydantic.Field, default_factory=dict)
FL: Callable[[], Any] = partial(pydantic.Field, default_factory=list)


class Webhook(CanDeleteModel, url="bases/{base.id}/webhooks/{self.id}"):
class Webhook(
RestfulModel,
url="bases/{base.id}/webhooks/{self.id}",
allow_delete=True,
):
"""
A webhook that has been retrieved from the Airtable API.
Expand Down
Loading

0 comments on commit 1d225f8

Please sign in to comment.