Repository: gtalarico/pyairtable
Pull request #380 (2024-08-16 01:25:09)

Add support for "Manual sort" fields in schema & ORM
This field type can be created through a list interface; it saves a text value which indicates how records have been manually sorted in the interface. It's undocumented but we've encountered it in our own bases.


Patch:
diff --git a/docs/source/orm.rst b/docs/source/orm.rst
index 3a37cc4..40473cc 100644
--- a/docs/source/orm.rst
+++ b/docs/source/orm.rst
@@ -118,7 +118,7 @@ read `Field types and cell values <https://airtable.com/developers/web/api/field
             links = re.findall(r"`.+? <.*?field-model.*?>`", cls.__doc__ or "")
             ro = ' 🔒' if cls.readonly else ''
             cog.outl(f"   * - :class:`~pyairtable.orm.fields.{cls.__name__}`{ro}")
-            cog.outl(f"     - {', '.join(f'{link}__' for link in links) if links else '(see docs)'}")
+            cog.outl(f"     - {', '.join(f'{link}__' for link in links) if links else '(undocumented)'}")
 
     classes = sorted(fields.ALL_FIELDS, key=attrgetter("__name__"))
     optional = [cls for cls in classes if not cls.__name__.startswith("Required")]
@@ -185,6 +185,8 @@ read `Field types and cell values <https://airtable.com/developers/web/api/field
      - `Link to another record <https://airtable.com/developers/web/api/field-model#foreignkey>`__
    * - :class:`~pyairtable.orm.fields.LookupField` 🔒
      - `Lookup <https://airtable.com/developers/web/api/field-model#lookup>`__
+   * - :class:`~pyairtable.orm.fields.ManualSortField` 🔒
+     - (undocumented)
    * - :class:`~pyairtable.orm.fields.MultipleCollaboratorsField`
      - `Multiple Collaborators <https://airtable.com/developers/web/api/field-model#multicollaborator>`__
    * - :class:`~pyairtable.orm.fields.MultipleSelectField`
@@ -257,7 +259,7 @@ See :ref:`Required Values` for more details.
      - `Single line text <https://airtable.com/developers/web/api/field-model#simpletext>`__, `Long text <https://airtable.com/developers/web/api/field-model#multilinetext>`__
    * - :class:`~pyairtable.orm.fields.RequiredUrlField`
      - `Url <https://airtable.com/developers/web/api/field-model#urltext>`__
-.. [[[end]]] (checksum: 131138e1071ba71d4f46f05da4d57570)
+.. [[[end]]] (checksum: 43c56200ca513d3a0603bb5a6ddbb1ef)
 
 
 Formula, Rollup, and Lookup Fields
diff --git a/pyairtable/models/schema.py b/pyairtable/models/schema.py
index 4d02651..d549955 100644
--- a/pyairtable/models/schema.py
+++ b/pyairtable/models/schema.py
@@ -810,6 +810,14 @@ class LastModifiedTimeFieldOptions(AirtableModel):
     result: Optional[Union["DateFieldConfig", "DateTimeFieldConfig"]]
 
 
+class ManualSortFieldConfig(AirtableModel):
+    """
+    Field configuration for ``manualSort`` field type (not documented).
+    """
+
+    type: Literal["manualSort"]
+
+
 class MultilineTextFieldConfig(AirtableModel):
     """
     Field configuration for `Long text <https://airtable.com/developers/web/api/field-model#multilinetext>`__.
@@ -1074,6 +1082,7 @@ FieldConfig: TypeAlias = Union[
     FormulaFieldConfig,
     LastModifiedByFieldConfig,
     LastModifiedTimeFieldConfig,
+    ManualSortFieldConfig,
     MultilineTextFieldConfig,
     MultipleAttachmentsFieldConfig,
     MultipleCollaboratorsFieldConfig,
@@ -1196,6 +1205,12 @@ class LastModifiedTimeFieldSchema(_FieldSchemaBase, LastModifiedTimeFieldConfig)
     """
 
 
+class ManualSortFieldSchema(_FieldSchemaBase, ManualSortFieldConfig):
+    """
+    Field schema for ``manualSort`` field type (not documented).
+    """
+
+
 class MultilineTextFieldSchema(_FieldSchemaBase, MultilineTextFieldConfig):
     """
     Field schema for `Long text <https://airtable.com/developers/web/api/field-model#multilinetext>`__.
@@ -1317,6 +1332,7 @@ FieldSchema: TypeAlias = Union[
     FormulaFieldSchema,
     LastModifiedByFieldSchema,
     LastModifiedTimeFieldSchema,
+    ManualSortFieldSchema,
     MultilineTextFieldSchema,
     MultipleAttachmentsFieldSchema,
     MultipleCollaboratorsFieldSchema,
@@ -1335,7 +1351,7 @@ FieldSchema: TypeAlias = Union[
     UrlFieldSchema,
     UnknownFieldSchema,
 ]
-# [[[end]]] (checksum: afb669896323650954a082cb4b079c16)
+# [[[end]]] (checksum: ca159bc8c76b1d15a2a57f0e76fb8911)
 # fmt: on
 
 
diff --git a/pyairtable/orm/fields.py b/pyairtable/orm/fields.py
index e7ca445..a18cfa2 100644
--- a/pyairtable/orm/fields.py
+++ b/pyairtable/orm/fields.py
@@ -962,6 +962,14 @@ class LookupField(Generic[T], _ListField[T, T]):
     readonly = True
 
 
+class ManualSortField(TextField):
+    """
+    Field configuration for ``manualSort`` field type (not documented).
+    """
+
+    readonly = True
+
+
 class MultipleCollaboratorsField(_ValidatingListField[CollaboratorDict]):
     """
     Accepts a list of dicts in the format detailed in
@@ -1072,6 +1080,7 @@ for cls, match in sorted(classes.items()):
         "LastModifiedByField",
         "LastModifiedTimeField",
         "ExternalSyncSourceField",
+        "ManualSortField",
     }:
         continue
 
@@ -1414,6 +1423,7 @@ FIELD_TYPES_TO_CLASSES = {
     "lastModifiedBy": LastModifiedByField,
     "lastModifiedTime": LastModifiedTimeField,
     "lookup": LookupField,
+    "manualSort": ManualSortField,
     "multilineText": TextField,
     "multipleAttachments": AttachmentsField,
     "multipleCollaborators": MultipleCollaboratorsField,
@@ -1486,6 +1496,7 @@ __all__ = [
     "LastModifiedTimeField",
     "LinkField",
     "LookupField",
+    "ManualSortField",
     "MultipleCollaboratorsField",
     "MultipleSelectField",
     "NumberField",
@@ -1522,7 +1533,7 @@ __all__ = [
     "FIELD_CLASSES_TO_TYPES",
     "LinkSelf",
 ]
-# [[[end]]] (checksum: 21316c688401f32f59d597c496d48bf3)
+# [[[end]]] (checksum: 3ea404fb3814f0d053b93d3479ab5e13)
 
 
 # Delayed import to avoid circular dependency
diff --git a/pyairtable/orm/model.py b/pyairtable/orm/model.py
index d661d7f..65430ad 100644
--- a/pyairtable/orm/model.py
+++ b/pyairtable/orm/model.py
@@ -373,7 +373,7 @@ class Model:
             memoize: |kwarg_orm_memoize|
         """
         try:
-            instance = cast(SelfType, cls._memoized[record_id])
+            instance = cast(SelfType, cls._memoized[record_id])  # type: ignore[redundant-cast]
         except KeyError:
             instance = cls(id=record_id)
         if fetch and not instance._fetched:
@@ -421,7 +421,7 @@ class Model:
         if cls._memoized:
             for record_id in record_ids:
                 try:
-                    by_id[record_id] = cast(SelfType, cls._memoized[record_id])
+                    by_id[record_id] = cast(SelfType, cls._memoized[record_id])  # type: ignore[redundant-cast]
                 except KeyError:
                     pass
 


Test patch:
diff --git a/tests/sample_data/field_schema/ManualSortFieldSchema.json b/tests/sample_data/field_schema/ManualSortFieldSchema.json
new file mode 100644
index 0000000..d99cfa3
--- /dev/null
+++ b/tests/sample_data/field_schema/ManualSortFieldSchema.json
@@ -0,0 +1,6 @@
+{
+  "type": "manualSort",
+  "options": null,
+  "id": "fldqCjrs1UhXgHUIc",
+  "name": "Record Sort"
+}
diff --git a/tests/test_orm_fields.py b/tests/test_orm_fields.py
index 3b68aff..7ce54e3 100644
--- a/tests/test_orm_fields.py
+++ b/tests/test_orm_fields.py
@@ -271,6 +271,7 @@ def test_type_validation_LinkField():
         (f.LookupField, ["any", "values"]),
         (f.CreatedByField, fake_user()),
         (f.LastModifiedByField, fake_user()),
+        (f.ManualSortField, "fcca"),
         # If a 3-tuple, we should be able to convert API -> ORM values.
         (f.CreatedTimeField, DATETIME_S, DATETIME_V),
         (f.LastModifiedTimeField, DATETIME_S, DATETIME_V),
@@ -402,6 +403,7 @@ def test_writable_fields(test_case):
         f.LastModifiedByField,
         f.LastModifiedTimeField,
         f.LookupField,
+        f.ManualSortField,
         f.MultipleCollaboratorsField,
         f.MultipleSelectField,
         f.NumberField,