Skip to content

Commit

Permalink
Fix documentation for 'Upload Attachment'
Browse files Browse the repository at this point in the history
  • Loading branch information
mesozoic committed Sep 9, 2024
1 parent 813b5fe commit 869408c
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 44 deletions.
4 changes: 3 additions & 1 deletion docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ Changelog
* Changed the return type of :meth:`Model.save <pyairtable.orm.Model.save>`
from ``bool`` to :class:`~pyairtable.orm.SaveResult`.
- `PR #387 <https://github.com/gtalarico/pyairtable/pull/387>`_
* Added support for `Upload attachment <https://airtable.com/developers/web/api/upload-attachment>`_.
* Added support for `Upload attachment <https://airtable.com/developers/web/api/upload-attachment>`_
via :meth:`Table.upload_attachment <pyairtable.Table.upload_attachment>`
or :meth:`AttachmentsList.upload <pyairtable.orm.lists.AttachmentsList.upload>`.

2.3.3 (2024-03-22)
------------------------
Expand Down
54 changes: 33 additions & 21 deletions docs/source/migrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,40 @@ The full list of breaking changes is below:
Changes to the ORM in 3.0
---------------------------------------------

:data:`Model.created_time <pyairtable.orm.Model.created_time>` is now a ``datetime`` (or ``None``)
instead of ``str``. This change also applies to all timestamp fields used in :ref:`API: pyairtable.models`.
* :data:`Model.created_time <pyairtable.orm.Model.created_time>` is now a ``datetime`` (or ``None``)
instead of ``str``. This change also applies to all timestamp fields used in :ref:`API: pyairtable.models`.

* :meth:`Model.save <pyairtable.orm.Model.save>` now only saves changed fields to the API, which
means it will sometimes not perform any network traffic (though this behavior can be overridden).
It also now returns an instance of :class:`~pyairtable.orm.SaveResult` instead of ``bool``.

* Fields which contain lists of values now return instances of ``ChangeTrackingList``, which
is still a subclass of ``list``. This should not affect most uses, but it does mean that
some code which relies on exact type checking may need to be updated:

>>> isinstance(Foo().atts, list)
True
>>> type(Foo().atts) is list
False
>>> type(Foo().atts)
<class 'pyairtable.orm.lists.ChangeTrackingList'>

* The 3.0 release has changed the API for retrieving ORM model configuration:

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

* - Method in 2.x
- Method in 3.0
* - ``Model.get_api()``
- ``Model.meta.api``
* - ``Model.get_base()``
- ``Model.meta.base``
* - ``Model.get_table()``
- ``Model.meta.table``
* - ``Model._get_meta(name)``
- ``Model.meta.get(name)``

:meth:`Model.save <pyairtable.orm.Model.save>` now only saves changed fields to the API, which
means it will sometimes not perform any network traffic (though this behavior can be overridden).
It also now returns an instance of :class:`~pyairtable.orm.SaveResult` instead of ``bool``.

The 3.0 release has changed the API for retrieving ORM model configuration:

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

* - Method in 2.x
- Method in 3.0
* - ``Model.get_api()``
- ``Model.meta.api``
* - ``Model.get_base()``
- ``Model.meta.base``
* - ``Model.get_table()``
- ``Model.meta.table``
* - ``Model._get_meta(name)``
- ``Model.meta.get(name)``

Miscellaneous name changes
---------------------------------------------
Expand Down
21 changes: 7 additions & 14 deletions docs/source/orm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,8 @@ comments on a particular record, just like their :class:`~pyairtable.Table` equi
>>> comment.delete()


Attachments
------------------
Attachments in the ORM
----------------------

When retrieving attachments from the API, pyAirtable will return a list of
:class:`~pyairtable.api.types.AttachmentDict`.
Expand All @@ -602,23 +602,16 @@ When retrieving attachments from the API, pyAirtable will return a list of
...
]

You can append your own values to this list, and as long as they conform
to :class:`~pyairtable.api.types.CreateAttachmentDict`, they will be saved
back to the API.
You can append your own values to this list, and as long as they have
either a ``"id"`` or ``"url"`` key, they will be saved back to the API.

>>> model.attachments.append({"url": "https://example.com/example.jpg"})
>>> model.save()

You can also use :meth:`~pyairtable.orm.fields.AttachmentList.upload` to
directly upload content to Airtable. You do not need to call
:meth:`~pyairtable.orm.Model.save`; the change will be saved immediately.
Note that this means any other unsaved changes to this field will be lost.
You can also use :meth:`~pyairtable.orm.lists.AttachmentsList.upload` to
directly upload content to Airtable:

>>> model.attachments.upload("example.jpg", b"...", "image/jpeg")
>>> model.attachments[-1]["filename"]
'example.jpg'
>>> model.attachments[-1]["url"]
'https://v5.airtableusercontent.com/...'
.. automethod:: pyairtable.orm.lists.AttachmentsList.upload


ORM Limitations
Expand Down
50 changes: 47 additions & 3 deletions pyairtable/api/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,21 +662,31 @@ def schema(self, *, force: bool = False) -> TableSchema:
def create_field(
self,
name: str,
type: str,
field_type: str,
description: Optional[str] = None,
options: Optional[Dict[str, Any]] = None,
) -> FieldSchema:
"""
Create a field on the table.
Usage:
>>> table.create_field("Attachments", "multipleAttachment")
FieldSchema(
id='fldslc6jG0XedVMNx',
name='Attachments',
type='multipleAttachment',
description=None,
options=MultipleAttachmentsFieldOptions(is_reversed=False)
)
Args:
name: The unique name of the field.
field_type: One of the `Airtable field types <https://airtable.com/developers/web/api/model/field-type>`__.
description: A long form description of the table.
options: Only available for some field types. For more information, read about the
`Airtable field model <https://airtable.com/developers/web/api/field-model>`__.
"""
request: Dict[str, Any] = {"name": name, "type": type}
request: Dict[str, Any] = {"name": name, "type": field_type}
if description:
request["description"] = description
if options:
Expand Down Expand Up @@ -704,7 +714,41 @@ def upload_attachment(
content: Optional[Union[str, bytes]] = None,
content_type: Optional[str] = None,
) -> UploadAttachmentResultDict:
""" """
"""
Upload an attachment to the Airtable API, either by supplying the path to the file
or by providing the content directly as a variable.
See `Upload attachment <https://airtable.com/developers/web/api/upload-attachment>`__.
Usage:
>>> table.upload_attachment("recAdw9EjV90xbZ", "Attachments", "/tmp/example.jpg")
{
'id': 'recAdw9EjV90xbZ',
'createdTime': '2023-05-22T21:24:15.333134Z',
'fields': {
'Attachments': [
{
'id': 'attW8eG2x0ew1Af',
'url': 'https://content.airtable.com/...',
'filename': 'example.jpg'
}
]
}
}
Args:
record_id: |arg_record_id|
field: The ID or name of the ``multipleAttachments`` type field.
filename: The path to the file to upload. If ``content`` is provided, this
argument is still used to tell Airtable what name to give the file.
content: The content of the file as a string or bytes object. If no value
is provided, pyAirtable will attempt to read the contents of ``filename``.
content_type: The MIME type of the file. If not provided, the library will attempt to
guess the content type based on ``filename``.
Returns:
A full list of attachments in the given field, including the new attachment.
"""
if content is None:
with open(filename, "rb") as fp:
content = fp.read()
Expand Down
21 changes: 21 additions & 0 deletions pyairtable/api/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,27 @@ class UserAndScopesDict(TypedDict, total=False):


class UploadAttachmentResultDict(TypedDict):
"""
A ``dict`` representing the payload returned by
`Upload attachment <https://airtable.com/developers/web/api/upload-attachment>`__.
Usage:
>>> table.upload_attachment("recAdw9EjV90xbZ", "Attachments", "/tmp/example.jpg")
{
'id': 'recAdw9EjV90xbZ',
'createdTime': '2023-05-22T21:24:15.333134Z',
'fields': {
'Attachments': [
{
'id': 'attW8eG2x0ew1Af',
'url': 'https://content.airtable.com/...',
'filename': 'example.jpg'
}
]
}
}
"""

id: RecordId
createdTime: str
fields: Dict[str, List[AttachmentDict]]
Expand Down
5 changes: 4 additions & 1 deletion pyairtable/orm/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,7 +919,10 @@ class AttachmentsField(
list_class=AttachmentsList,
contains_type=dict,
):
pass
"""
Accepts a list of dicts in the format detailed in
`Attachments <https://airtable.com/developers/web/api/field-model#multipleattachment>`_.
"""


class BarcodeField(_DictField[BarcodeDict]):
Expand Down
19 changes: 16 additions & 3 deletions pyairtable/orm/lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,22 @@ def upload(
content_type: Optional[str] = None,
) -> None:
"""
Upload an attachment to the Airtable API. This will replace the current
list with the response from the server, which will contain a full list of
:class:`~pyairtable.api.types.AttachmentDict`.
Upload an attachment to the Airtable API and refresh the field's values.
This method will replace the current list with the response from the server,
which will contain a list of :class:`~pyairtable.api.types.AttachmentDict` for
all attachments in the field (not just the ones uploaded).
You do not need to call :meth:`~pyairtable.orm.Model.save`; the new attachment
will be saved immediately. Note that this means any other unsaved changes to
this field will be lost.
Example:
>>> model.attachments.upload("example.jpg", b"...", "image/jpeg")
>>> model.attachments[-1]["filename"]
'example.jpg'
>>> model.attachments[-1]["url"]
'https://v5.airtableusercontent.com/...'
"""
if not self._model.id:
raise UnsavedRecordError("cannot upload attachments to an unsaved record")
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_integration_enterprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def test_create_field(blank_base: pyairtable.Base):
assert len(table.schema().fields) == 1
fld = table.create_field(
"Status",
type="singleSelect",
field_type="singleSelect",
options={
"choices": [
{"name": "Todo"},
Expand Down

0 comments on commit 869408c

Please sign in to comment.