Skip to content

Commit

Permalink
Merge pull request #329 from mesozoic/formulas
Browse files Browse the repository at this point in the history
Rewrite of pyairtable.formulas
  • Loading branch information
mesozoic authored Mar 19, 2024
2 parents b676bf0 + 2744c67 commit 7e7c853
Show file tree
Hide file tree
Showing 18 changed files with 1,726 additions and 290 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ include *.rst
include tox.ini
include LICENSE
include README.md
exclude pyairtable/formulas.txt
9 changes: 4 additions & 5 deletions docs/source/_substitutions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
.. |kwarg_formula| replace:: An Airtable formula. The formula will be evaluated for each record, and if the result
is none of ``0``, ``false``, ``""``, ``NaN``, ``[]``, or ``#Error!`` the record will be included
in the response. If combined with view, only records in that view which satisfy the
formula will be returned. For example, to only include records where
``COLUMN_A`` isn't empty, pass in ``formula="{COLUMN_A}"``.
formula will be returned. Read more at :doc:`formulas`.

.. |kwarg_typecast| replace:: The Airtable API will perform best-effort
automatic data conversion from string values.
Expand All @@ -46,17 +45,17 @@

.. |kwarg_user_locale| replace:: The user locale that should be used to format
dates when using `string` as the `cell_format`. See
https://support.airtable.com/hc/en-us/articles/220340268-Supported-locale-modifiers-for-SET-LOCALE
`Supported SET_LOCALE modifiers <https://support.airtable.com/docs/supported-locale-modifiers-for-set-locale>`__
for valid values.

.. |kwarg_time_zone| replace:: The time zone that should be used to format dates
when using `string` as the `cell_format`. See
https://support.airtable.com/hc/en-us/articles/216141558-Supported-timezones-for-SET-TIMEZONE
`Supported SET_TIMEZONE timezones <https://support.airtable.com/docs/supported-timezones-for-set-timezone>`__
for valid values.

.. |kwarg_replace| replace:: If ``True``, record is replaced in its entirety
by provided fields; if a field is not included its value will
bet set to null. If False, only provided fields are updated.
bet set to null. If ``False``, only provided fields are updated.

.. |kwarg_return_fields_by_field_id| replace:: An optional boolean value that lets you return field objects where the
key is the field id. This defaults to `false`, which returns field objects where the key is the field name.
Expand Down
6 changes: 6 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Changelog
=========

3.0 (TBD)
------------------------

* Rewrite of :mod:`pyairtable.formulas` module.
- `PR #329 <https://github.com/gtalarico/pyairtable/pull/329>`_.

2.3.2 (2024-03-18)
------------------------

Expand Down
95 changes: 95 additions & 0 deletions docs/source/formulas.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
Building Formulas
=================

pyAirtable lets you construct formulas at runtime using Python syntax,
and will convert those formula objects into the appropriate strings when
sending them to the Airtable API.


Basics
--------------------------

In cases where you want to find records with fields matching a computed value,
this library provides the :func:`~pyairtable.formulas.match` function, which
returns a formula that can be passed to methods like :func:`Table.all <pyairtable.Table.all>`:

.. autofunction:: pyairtable.formulas.match
:noindex:


Compound conditions
--------------------------

Formulas and conditions can be chained together if you need to create
more complex criteria:

>>> from datetime import date
>>> from pyairtable.formulas import AND, GTE, Field, match
>>> formula = AND(
... match("Customer", 'Alice'),
... GTE(Field("Delivery Date"), date.today())
... )
>>> formula
AND(EQ(Field('Customer'), 'Alice'),
GTE(Field('Delivery Date'), datetime.date(2023, 12, 10)))
>>> str(formula)
"AND({Customer}='Alice', {Delivery Date}>=DATETIME_PARSE('2023-12-10'))"

pyAirtable has support for the following comparisons:

.. list-table::

* - :class:`pyairtable.formulas.EQ`
- ``lval = rval``
* - :class:`pyairtable.formulas.NE`
- ``lval != rval``
* - :class:`pyairtable.formulas.GT`
- ``lval > rval``
* - :class:`pyairtable.formulas.GTE`
- ``lval >= rval``
* - :class:`pyairtable.formulas.LT`
- ``lval < rval``
* - :class:`pyairtable.formulas.LTE`
- ``lval <= rval``

These are also implemented as convenience methods on all instances
of :class:`~pyairtable.formulas.Formula`, so that the following are equivalent:

>>> EQ(Field("Customer"), "Alice")
>>> match({"Customer": "Alice"})
>>> Field("Customer").eq("Alice")

pyAirtable exports ``AND``, ``OR``, ``NOT``, and ``XOR`` for chaining conditions.
You can also use Python operators to modify and combine formulas:

>>> from pyairtable.formulas import match
>>> match({"Customer": "Bob"}) & ~match({"Product": "TEST"})
AND(EQ(Field('Customer'), 'Bob'),
NOT(EQ(Field('Product'), 'TEST')))

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

* - Python operator
- `Airtable expression <https://support.airtable.com/docs/formula-field-reference#logical-operators-and-functions-in-airtable>`__
* - ``lval & rval``
- ``AND(lval, rval)``
* - ``lval | rval``
- ``OR(lval, rval)``
* - ``~rval``
- ``NOT(rval)``
* - ``lval ^ rval``
- ``XOR(lval, rval)``

Calling functions
--------------------------

pyAirtable also exports functions that act as placeholders for calling
Airtable formula functions:

>>> from pyairtable.formulas import Field, GTE, DATETIME_DIFF, TODAY
>>> formula = GTE(DATETIME_DIFF(TODAY(), Field("Purchase Date"), "days"), 7)
>>> str(formula)
"DATETIME_DIFF(TODAY(), {Purchase Date}, 'days')>=7"

All supported functions are listed in the :mod:`pyairtable.formulas` API reference.
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pyAirtable

getting-started
tables
formulas
orm
metadata
webhooks
Expand Down
44 changes: 44 additions & 0 deletions docs/source/migrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,50 @@ Migration Guide
*****************


Migrating from 2.x to 3.0
============================

In this release we've made breaking changes to the :mod:`pyairtable.formulas` module.
In general, most functions and methods in this module will return instances of
:class:`~pyairtable.formulas.Formula`, which can be chained, combined, and eventually
passed to the ``formula=`` keyword argument to methods like :meth:`~pyairtable.Table.all`.

The full list of breaking changes is below:

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

* - Function
- Changes
* - :func:`~pyairtable.formulas.match`
- This now raises ``ValueError`` on empty input,
instead of returning ``None``.
* - ``to_airtable_value()``
- Removed. Use :func:`~pyairtable.formulas.to_formula_str` instead.
* - ``EQUAL()``
- Removed. Use :class:`~pyairtable.formulas.EQ` instead.
* - ``NOT_EQUAL()``
- Removed. Use :class:`~pyairtable.formulas.NE` instead.
* - ``LESS()``
- Removed. Use :class:`~pyairtable.formulas.LT` instead.
* - ``LESS_EQUAL()``
- Removed. Use :class:`~pyairtable.formulas.LTE` instead.
* - ``GREATER()``
- Removed. Use :class:`~pyairtable.formulas.GT` instead.
* - ``GREATER_EQUAL()``
- Removed. Use :class:`~pyairtable.formulas.GTE` instead.
* - ``FIELD()``
- Removed. Use :class:`~pyairtable.formulas.Field` or :func:`~pyairtable.formulas.field_name`.
* - ``STR_VALUE()``
- Removed. Use :func:`~pyairtable.formulas.quoted` instead.
* - :func:`~pyairtable.formulas.AND`, :func:`~pyairtable.formulas.OR`
- These no longer return ``str``, and instead return instances of
:class:`~pyairtable.formulas.Comparison`.
* - :func:`~pyairtable.formulas.IF`, :func:`~pyairtable.formulas.FIND`, :func:`~pyairtable.formulas.LOWER`
- These no longer return ``str``, and instead return instances of
:class:`~pyairtable.formulas.FunctionCall`.


Migrating from 2.2 to 2.3
============================

Expand Down
30 changes: 24 additions & 6 deletions docs/source/orm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ The :class:`~pyairtable.orm.Model` class allows you create ORM-style classes for
api_key = "keyapikey"
Once you have a model, you can create new objects to represent your
Airtable records. Call :meth:`~pyairtable.orm.Model.save` to save the
newly created object to the Airtable API.
Once you have a model, you can query for existing records using the
``first()`` and ``all()`` methods, which take the same arguments as
:meth:`Table.first <pyairtable.Table.first>` and :meth:`Table.all <pyairtable.Table.all>`.

You can also create new objects to represent Airtable records you wish
to create and save. Call :meth:`~pyairtable.orm.Model.save` to save the
newly created object back to Airtable.

>>> contact = Contact(
... first_name="Mike",
Expand All @@ -47,7 +51,6 @@ newly created object to the Airtable API.
>>> contact.id
'recS6qSLw0OCA6Xul'


You can read and modify attributes, then call :meth:`~pyairtable.orm.Model.save`
when you're ready to save your changes to the API.

Expand All @@ -63,7 +66,7 @@ To refresh a record from the API, use :meth:`~pyairtable.orm.Model.fetch`:
>>> contact.is_registered
True

Finally, you can use :meth:`~pyairtable.orm.Model.delete` to delete the record:
Use :meth:`~pyairtable.orm.Model.delete` to delete the record:

>>> contact.delete()
True
Expand All @@ -77,6 +80,21 @@ create, modify, or delete several records at once:
>>> Contact.batch_save(contacts)
>>> Contact.batch_delete(contacts)

You can use your model's fields in :doc:`formula expressions <formulas>`.
ORM models' fields also provide shortcut methods
:meth:`~pyairtable.orm.fields.Field.eq`,
:meth:`~pyairtable.orm.fields.Field.ne`,
:meth:`~pyairtable.orm.fields.Field.gt`,
:meth:`~pyairtable.orm.fields.Field.gte`,
:meth:`~pyairtable.orm.fields.Field.lt`, and
:meth:`~pyairtable.orm.fields.Field.lte`:

>>> formula = Contact.last_name.eq("Smith") & Contact.is_registered
>>> str(formula)
"AND({Last Name}='Smith', {Registered})"
>>> results = Contact.all(formula=formula)
[...]


Supported Field Types
-----------------------------
Expand Down Expand Up @@ -176,7 +194,7 @@ read `Field types and cell values <https://airtable.com/developers/web/api/field
.. [[[end]]] (checksum: 01c5696293e7571ac8250c4e8a2453e8)
Formulas, Rollups, and Lookups
Formula, Rollup, and Lookup Fields
----------------------------------

The data type of "formula", "rollup", and "lookup" fields will depend on the underlying fields
Expand Down
27 changes: 8 additions & 19 deletions docs/source/tables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,32 +152,21 @@ This library will return records as :class:`~pyairtable.api.types.RecordDict`.
Formulas
********

The :mod:`pyairtable.formulas` module provides functionality to help you compose
`Airtable formulas <https://support.airtable.com/hc/en-us/articles/203255215-Formula-field-reference>`_.
Methods like :meth:`~pyairtable.Table.all` or :meth:`~pyairtable.Table.first`
accept a ``formula=`` keyword argument so you can filter results using an
`Airtable formula <https://support.airtable.com/hc/en-us/articles/203255215-Formula-field-reference>`_.

* :func:`~pyairtable.formulas.match` checks field values from a Python ``dict``:
The simplest option is to pass your formula as a string; however, if your use case
is complex and you want to avoid lots of f-strings and escaping, use
:func:`~pyairtable.formulas.match` to check field values from a ``dict``:

.. code-block:: python
>>> from pyairtable.formulas import match
>>> formula = match({"First Name": "John", "Age": 21})
>>> formula
"AND({First Name}='John',{Age}=21)"
>>> table.first(formula=formula)
>>> table.first(formula=match({"First Name": "John", "Age": 21}))
{"id": "recUwKa6lbNSMsetH", "fields": {"First Name": "John", "Age": 21}}
* :func:`~pyairtable.formulas.to_airtable_value` converts a Python value
to an expression that can be included in a formula:

.. code-block:: python
>>> from pyairtable.formulas import to_airtable_value
>>> to_airtable_value(1)
1
>>> to_airtable_value(datetime.date.today())
'2023-06-13'
For more on generating formulas, look over the :mod:`pyairtable.formulas` API reference.
For more on generating formulas, read the :doc:`formulas` documentation.


Retries
Expand Down
3 changes: 3 additions & 0 deletions pyairtable/api/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
assert_typed_dict,
assert_typed_dicts,
)
from pyairtable.formulas import Formula, to_formula_str
from pyairtable.models.schema import FieldSchema, TableSchema, parse_field_schema
from pyairtable.utils import is_table_id

Expand Down Expand Up @@ -227,6 +228,8 @@ def iterate(self, **options: Any) -> Iterator[List[RecordDict]]:
time_zone: |kwarg_time_zone|
return_fields_by_field_id: |kwarg_return_fields_by_field_id|
"""
if isinstance(formula := options.get("formula"), Formula):
options["formula"] = to_formula_str(formula)
for page in self.api.iterate_requests(
method="get",
url=self.url,
Expand Down
Loading

0 comments on commit 7e7c853

Please sign in to comment.