Skip to content

Commit

Permalink
Merge pull request #374 from mesozoic/docs
Browse files Browse the repository at this point in the history
Documentation and API cleanup ahead of 3.0 release
  • Loading branch information
mesozoic authored Jun 3, 2024
2 parents ca4237c + a75e30f commit 7029671
Show file tree
Hide file tree
Showing 17 changed files with 116 additions and 56 deletions.
39 changes: 35 additions & 4 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,28 @@ API: pyairtable
.. autofunction:: pyairtable.retry_strategy


API: pyairtable.api.enterprise
*******************************

.. automodule:: pyairtable.api.enterprise
:members:
:exclude-members: Enterprise


API: pyairtable.api.types
*******************************

.. automodule:: pyairtable.api.types
:members:


API: pyairtable.exceptions
*******************************

.. automodule:: pyairtable.exceptions
:members:


API: pyairtable.formulas
*******************************

Expand All @@ -49,19 +64,28 @@ API: pyairtable.models
:inherited-members: AirtableModel


API: pyairtable.models.audit
********************************
API: pyairtable.models.comment
-------------------------------

.. automodule:: pyairtable.models.audit
.. automodule:: pyairtable.models.comment
:members:
:exclude-members: Comment
:inherited-members: AirtableModel


API: pyairtable.models.schema
********************************
-------------------------------

.. automodule:: pyairtable.models.schema
:members:


API: pyairtable.models.webhook
-------------------------------

.. automodule:: pyairtable.models.webhook
:members:
:exclude-members: Webhook, WebhookNotification, WebhookPayload
:inherited-members: AirtableModel


Expand All @@ -81,6 +105,13 @@ API: pyairtable.orm.fields
:no-inherited-members:


API: pyairtable.testing
*******************************

.. automodule:: pyairtable.testing
:members:


API: pyairtable.utils
*******************************

Expand Down
17 changes: 10 additions & 7 deletions docs/source/migrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,16 @@ The 3.0 release has changed the API for retrieving ORM model configuration:
Miscellaneous name changes
---------------------------------------------

.. list-table::
:header-rows: 1

* - Old name
- New name
* - :class:`~pyairtable.api.enterprise.ClaimUsersResponse`
- :class:`~pyairtable.api.enterprise.ManageUsersResponse`
* - | ``pyairtable.api.enterprise.ClaimUsersResponse``
| has become :class:`pyairtable.api.enterprise.ManageUsersResponse`
* - | ``pyairtable.formulas.CircularDependency``
| has become :class:`pyairtable.exceptions.CircularFormulaError`
* - | ``pyairtable.params.InvalidParamException``
| has become :class:`pyairtable.exceptions.InvalidParameterError`
* - | ``pyairtable.orm.fields.MissingValue``
| has become :class:`pyairtable.exceptions.MissingValueError`
* - | ``pyairtable.orm.fields.MultipleValues``
| has become :class:`pyairtable.exceptions.MultipleValuesError`

Migrating from 2.2 to 2.3
Expand Down
8 changes: 2 additions & 6 deletions pyairtable/api/params.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
from typing import Any, Dict, List, Tuple


class InvalidParamException(ValueError):
"""
Raised when invalid parameters are passed to ``all()``, ``first()``, etc.
"""
from pyairtable.exceptions import InvalidParameterError


def dict_list_to_request_params(
Expand Down Expand Up @@ -85,7 +81,7 @@ def _option_to_param(name: str) -> str:
try:
return OPTIONS_TO_PARAMETERS[name]
except KeyError:
raise InvalidParamException(name)
raise InvalidParameterError(name)


#: List of option names that cannot be passed via POST, only GET
Expand Down
2 changes: 1 addition & 1 deletion pyairtable/api/retrying.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,5 @@ def __init__(self, retry_strategy: Retry):

__all__ = [
"Retry",
"_RetryingSession",
"retry_strategy",
]
4 changes: 2 additions & 2 deletions pyairtable/api/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ def move_base(
See https://airtable.com/developers/web/api/move-base
Usage:
>>> base = api.base("appCwFmhESAta6clC")
>>> ws = api.workspace("wspmhESAta6clCCwF")
>>> base = api.workspace("appCwFmhESAta6clC")
>>> workspace.move_base(base, "wspSomeOtherPlace", index=0)
>>> ws.move_base(base, "wspSomeOtherPlace", index=0)
"""
base_id = base if isinstance(base, str) else base.id
target_id = target if isinstance(target, str) else target.id
Expand Down
28 changes: 28 additions & 0 deletions pyairtable/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class PyAirtableError(Exception):
"""
Base class for all exceptions raised by PyAirtable.
"""


class CircularFormulaError(PyAirtableError, RecursionError):
"""
A circular dependency was encountered when flattening nested conditions.
"""


class InvalidParameterError(PyAirtableError, ValueError):
"""
Raised when invalid parameters are passed to ``all()``, ``first()``, etc.
"""


class MissingValueError(PyAirtableError, ValueError):
"""
A required field received an empty value, either from Airtable or other code.
"""


class MultipleValuesError(PyAirtableError, ValueError):
"""
SingleLinkField received more than one value from either Airtable or calling code.
"""
9 changes: 2 additions & 7 deletions pyairtable/formulas.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing_extensions import Self as SelfType

from pyairtable.api.types import Fields
from pyairtable.exceptions import CircularFormulaError
from pyairtable.utils import date_to_iso_str, datetime_to_iso_str


Expand Down Expand Up @@ -235,7 +236,7 @@ def flatten(self, /, memo: Optional[Set[int]] = None) -> "Compound":
flattened: List[Formula] = []
for item in self.components:
if id(item) in memo:
raise CircularDependency(item)
raise CircularFormulaError(item)
if isinstance(item, Compound) and item.operator == self.operator:
flattened.extend(item.flatten(memo=memo).components)
else:
Expand Down Expand Up @@ -315,12 +316,6 @@ def NOT(component: Optional[Formula] = None, /, **fields: Any) -> Compound:
return Compound.build("NOT", items)


class CircularDependency(RecursionError):
"""
A circular dependency was encountered when flattening nested conditions.
"""


def match(field_values: Fields, *, match_any: bool = False) -> Formula:
r"""
Create one or more equality expressions for each provided value,
Expand Down
7 changes: 7 additions & 0 deletions pyairtable/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@
pyAirtable will wrap certain API responses in type-annotated models,
some of which will be deeply nested within each other. Models which
implementers can interact with directly are documented below.
Nested or internal models are documented in each submodule.
Due to its complexity, the :mod:`pyairtable.models.schema` module is
documented separately, and none of its classes are exposed here.
"""

from .audit import AuditLogEvent, AuditLogResponse
from .collaborator import Collaborator
from .comment import Comment
from .webhook import Webhook, WebhookNotification, WebhookPayload

__all__ = [
"AuditLogResponse",
"AuditLogEvent",
"Collaborator",
"Comment",
"Webhook",
Expand Down
4 changes: 3 additions & 1 deletion pyairtable/models/comment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from datetime import datetime
from typing import Dict, Optional

from pyairtable._compat import pydantic

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

Expand Down Expand Up @@ -58,7 +60,7 @@ class Comment(
author: Collaborator

#: Users or groups that were mentioned in the text.
mentioned: Optional[Dict[str, "Mentioned"]]
mentioned: Dict[str, "Mentioned"] = pydantic.Field(default_factory=dict)


class Mentioned(AirtableModel):
Expand Down
Empty file removed pyairtable/models/record.py
Empty file.
6 changes: 5 additions & 1 deletion pyairtable/models/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1345,7 +1345,11 @@ class _HasFieldSchema(AirtableModel):
field_schema: FieldSchema


def parse_field_schema(obj: Any) -> FieldSchema:
def parse_field_schema(obj: Dict[str, Any]) -> FieldSchema:
"""
Given a ``dict`` representing a field schema,
parse it into the appropriate FieldSchema subclass.
"""
return _HasFieldSchema.parse_obj({"field_schema": obj}).field_schema


Expand Down
21 changes: 6 additions & 15 deletions pyairtable/orm/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
CollaboratorDict,
RecordId,
)
from pyairtable.exceptions import MissingValueError, MultipleValuesError

if TYPE_CHECKING:
from pyairtable.orm import Model # noqa
Expand Down Expand Up @@ -278,21 +279,15 @@ def __get__(
) -> Union[SelfType, T_ORM]:
value = super().__get__(instance, owner)
if value is None or value == "":
raise MissingValue(f"{self._description} received an empty value")
raise MissingValueError(f"{self._description} received an empty value")
return value

def __set__(self, instance: "Model", value: Optional[T_ORM]) -> None:
if value in (None, ""):
raise MissingValue(f"{self._description} does not accept empty values")
raise MissingValueError(f"{self._description} does not accept empty values")
super().__set__(instance, value)


class MissingValue(ValueError):
"""
A required field received an empty value, either from Airtable or other code.
"""


#: A generic Field with internal and API representations that are the same type.
_BasicField: TypeAlias = Field[T, T, None]
_BasicFieldWithMissingValue: TypeAlias = Field[T, T, T]
Expand Down Expand Up @@ -810,7 +805,9 @@ def __get__(
if not instance:
return self
if self._raise_if_many and len(instance._fields.get(self.field_name) or []) > 1:
raise MultipleValues(f"{self._description} got more than one linked record")
raise MultipleValuesError(
f"{self._description} got more than one linked record"
)
links = self._link_field.__get__(instance, owner)
try:
return links[0]
Expand Down Expand Up @@ -844,12 +841,6 @@ def linked_model(self) -> Type[T_Linked]:
return self._link_field.linked_model


class MultipleValues(ValueError):
"""
SingleLinkField received more than one value from either Airtable or calling code.
"""


# Many of these are "passthrough" subclasses for now. E.g. there is no real
# difference between `field = TextField()` and `field = PhoneNumberField()`.
#
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_integration_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ def test_integration_comments(api, table: Table, cols):
comments[0].text = "Never mind!"
comments[0].save()
assert whoami not in comments[0].text
assert comments[0].mentioned is None
assert not comments[0].mentioned

# Test that we can delete the comment
comments[0].delete()
3 changes: 2 additions & 1 deletion tests/test_formulas.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest
from mock import call

import pyairtable.exceptions
from pyairtable import formulas as F
from pyairtable import orm
from pyairtable.formulas import AND, EQ, GT, GTE, LT, LTE, NE, NOT, OR
Expand Down Expand Up @@ -181,7 +182,7 @@ def test_compound_flatten():
def test_compound_flatten_circular_dependency():
circular = NOT(F.Formula("x"))
circular.components = [circular]
with pytest.raises(F.CircularDependency):
with pytest.raises(pyairtable.exceptions.CircularFormulaError):
circular.flatten()


Expand Down
9 changes: 5 additions & 4 deletions tests/test_models_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,15 @@ 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):
def test_missing_attributes(comment_json):
"""
Test that we can parse the payload when missing optional values.
"""
del comment_json[Comment.__fields__[attr].alias]
del comment_json["lastUpdatedTime"]
del comment_json["mentioned"]
comment = Comment.parse_obj(comment_json)
assert getattr(comment, attr) is None
assert comment.mentioned == {}
assert comment.last_updated_time is None


@pytest.mark.parametrize(
Expand Down
9 changes: 5 additions & 4 deletions tests/test_orm_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest
from requests_mock import NoMockAddress

import pyairtable.exceptions
from pyairtable.formulas import OR, RECORD_ID
from pyairtable.orm import fields as f
from pyairtable.orm.model import Model
Expand Down Expand Up @@ -465,11 +466,11 @@ class T(Model):
the_field = field_type("Field Name")

obj = T()
with pytest.raises(f.MissingValue):
with pytest.raises(pyairtable.exceptions.MissingValueError):
obj.the_field
with pytest.raises(f.MissingValue):
with pytest.raises(pyairtable.exceptions.MissingValueError):
obj.the_field = None
with pytest.raises(f.MissingValue):
with pytest.raises(pyairtable.exceptions.MissingValueError):
T(the_field=None)


Expand Down Expand Up @@ -855,7 +856,7 @@ class Book(Model):
author = f.SingleLinkField("Author", Author, raise_if_many=True)

book = Book.from_record(fake_record(Author=[fake_id(), fake_id()]))
with pytest.raises(f.MultipleValues):
with pytest.raises(pyairtable.exceptions.MultipleValuesError):
book.author


Expand Down
Loading

0 comments on commit 7029671

Please sign in to comment.