From 855177b347d3ae048a8b3e0c0216c352f1176110 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Mon, 31 Jul 2023 12:06:51 -0700 Subject: [PATCH] Introduce "Collaborator" model --- docs/source/api.rst | 2 ++ docs/source/orm.rst | 10 ++++---- docs/source/tables.rst | 10 ++++---- pyairtable/api/table.py | 10 ++++---- pyairtable/models/__init__.py | 2 ++ pyairtable/models/collaborator.py | 26 ++++++++++++++++++++ pyairtable/models/comment.py | 16 ++++++------- tests/integration/test_integration_api.py | 2 +- tests/test_models_collaborator.py | 29 +++++++++++++++++++++++ tests/test_models_comment.py | 11 +++++++++ 10 files changed, 94 insertions(+), 24 deletions(-) create mode 100644 pyairtable/models/collaborator.py create mode 100644 tests/test_models_collaborator.py diff --git a/docs/source/api.rst b/docs/source/api.rst index c5a7fbf1..fd97cc1f 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -38,6 +38,8 @@ Module: pyairtable.models .. autoclass:: pyairtable.models.Comment :members: +.. autoclass:: pyairtable.models.Collaborator + :members: Module: pyairtable.orm ******************************* diff --git a/docs/source/orm.rst b/docs/source/orm.rst index a4eca882..035d3f9f 100644 --- a/docs/source/orm.rst +++ b/docs/source/orm.rst @@ -348,11 +348,11 @@ comments on a particular record, just like their :class:`~pyairtable.Table` equi type='user' ) }, - author={ - 'id': 'usr0000pyairtable', - 'email': 'pyairtable@example.com', - 'name': 'Your pyairtable access token' - } + author=Collaborator( + id='usr0000pyairtable', + email='pyairtable@example.com', + name='Your pyairtable access token' + ) ) ] >>> comment.text = "Never mind!" diff --git a/docs/source/tables.rst b/docs/source/tables.rst index b2f79a0c..2b5a0fee 100644 --- a/docs/source/tables.rst +++ b/docs/source/tables.rst @@ -297,11 +297,11 @@ and :meth:`~pyairtable.Table.add_comment` methods will return instances of type='user' ) }, - author={ - 'id': 'usr0000pyairtable', - 'email': 'pyairtable@example.com', - 'name': 'Your pyairtable access token' - } + author=Collaborator( + id='usr0000pyairtable', + email='pyairtable@example.com', + name='Your pyairtable access token' + ) ) ] >>> comment.text = "Never mind!" diff --git a/pyairtable/api/table.py b/pyairtable/api/table.py index e149cc1f..94b07d62 100644 --- a/pyairtable/api/table.py +++ b/pyairtable/api/table.py @@ -489,11 +489,11 @@ def comments(self, record_id: RecordId) -> List["pyairtable.models.Comment"]: type='user' ) }, - author={ - 'id': 'usr0000pyairtable', - 'email': 'pyairtable@example.com', - 'name': 'Your pyairtable access token' - } + author=Collaborator( + id='usr0000pyairtable', + email='pyairtable@example.com', + name='Your pyairtable access token' + ) ) ] diff --git a/pyairtable/models/__init__.py b/pyairtable/models/__init__.py index 13af15d8..0f96c301 100644 --- a/pyairtable/models/__init__.py +++ b/pyairtable/models/__init__.py @@ -1,5 +1,7 @@ +from .collaborator import Collaborator from .comment import Comment __all__ = [ + "Collaborator", "Comment", ] diff --git a/pyairtable/models/collaborator.py b/pyairtable/models/collaborator.py new file mode 100644 index 00000000..a55c0332 --- /dev/null +++ b/pyairtable/models/collaborator.py @@ -0,0 +1,26 @@ +from typing import Optional + +from typing_extensions import TypeAlias + +from ._base import AirtableModel + +UserId: TypeAlias = str + + +class Collaborator(AirtableModel): + """ + Represents an Airtable user being passed from the API. + + This is only used in contexts involving other models (e.g. :class:`~pyairtable.models.Comment`). + In contexts where API values are returned as ``dict``, we will return + collaborator information as a ``dict`` as well. + """ + + #: Airtable's unique ID for the user, in the format ``usrXXXXXXXXXXXXXX``. + id: UserId + + #: The email address of the user. + email: Optional[str] + + #: The display name of the user. + name: Optional[str] diff --git a/pyairtable/models/comment.py b/pyairtable/models/comment.py index a5c5f406..88821abb 100644 --- a/pyairtable/models/comment.py +++ b/pyairtable/models/comment.py @@ -1,7 +1,7 @@ from typing import Dict, Optional -from pyairtable.api.types import CollaboratorDict -from pyairtable.models._base import AirtableModel, SerializableModel +from ._base import AirtableModel, SerializableModel +from .collaborator import Collaborator class Comment(SerializableModel): @@ -24,11 +24,11 @@ class Comment(SerializableModel): type='user' ) }, - author={ - 'id': 'usrL2xZC5xoH4luAi', - 'email': 'pyairtable@example.com', - 'name': 'Your pyairtable access token' - } + author=Collaborator( + id='usr0000pyairtable', + email='pyairtable@example.com', + name='Your pyairtable access token' + ) ) ] >>> comment.text = "Never mind!" @@ -51,7 +51,7 @@ class Comment(SerializableModel): last_updated_time: Optional[str] #: The account which created the comment. - author: CollaboratorDict + author: Collaborator #: Users or groups that were mentioned in the text. mentioned: Optional[Dict[str, "Comment.Mentioned"]] diff --git a/tests/integration/test_integration_api.py b/tests/integration/test_integration_api.py index 6c6e1e4a..b8b994fe 100644 --- a/tests/integration/test_integration_api.py +++ b/tests/integration/test_integration_api.py @@ -311,7 +311,7 @@ def test_integration_comments(api, table: Table, cols): comments = table.comments(record["id"]) assert len(comments) == 1 assert whoami in comments[0].text - assert comments[0].author + assert comments[0].author.id == whoami assert comments[0].mentioned[whoami].id == whoami # Test that we can modify the comment and examine its updated state diff --git a/tests/test_models_collaborator.py b/tests/test_models_collaborator.py new file mode 100644 index 00000000..a1630475 --- /dev/null +++ b/tests/test_models_collaborator.py @@ -0,0 +1,29 @@ +import pytest + +from pyairtable.models import Collaborator + +fake_user_data = { + "id": "usr000000fakeuser", + "email": "fake@example.com", + "name": "Fake User", +} + + +def test_parse(): + user = Collaborator.parse_obj(fake_user_data) + assert user.id == fake_user_data["id"] + assert user.email == fake_user_data["email"] + assert user.name == fake_user_data["name"] + + +def test_init(): + c = Collaborator(id="usrXXXXXXXXXXXXX") + assert c.id == "usrXXXXXXXXXXXXX" + assert c.email is None + assert c.name is None + + with pytest.raises(ValueError): + Collaborator() + + with pytest.raises(ValueError): + Collaborator(name="Fake User") diff --git a/tests/test_models_comment.py b/tests/test_models_comment.py index 25ab33c5..45564c6f 100644 --- a/tests/test_models_comment.py +++ b/tests/test_models_comment.py @@ -45,6 +45,16 @@ def test_parse(comment_json): Comment.parse_obj(comment_json) +@pytest.mark.parametrize("attr", ["mentioned", "last_updated_time"]) +def test_missing_attributes(comment_json, attr): + """ + Test that we can parse the payload when missing optional values. + """ + del comment_json[Comment.__fields__[attr].alias] + comment = Comment.parse_obj(comment_json) + assert getattr(comment, attr) is None + + @pytest.mark.parametrize( "attr,value", [ @@ -82,6 +92,7 @@ def test_save(comment, requests_mock): # ...but our model loaded whatever values the API sent back. assert comment.text == new_text + assert comment.author.email == "author@example.com" assert not comment.mentioned