CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-pyairtable

Python client for the Airtable API providing comprehensive database operations, ORM functionality, enterprise features, and testing utilities

Pending
Overview
Eval results
Files

orm.mddocs/

ORM System

Object-Relational Mapping functionality providing a Django-style model approach to Airtable tables. Includes field definitions, type validation, automatic conversions, and model-based operations for Python developers.

Capabilities

Model Definition

Base Model class for creating ORM-style table representations with field definitions and metadata configuration.

class Model:
    def __init__(self, **field_values):
        """
        Initialize model instance with field values.
        
        Parameters:
        - field_values: Keyword arguments mapping field names to values
        """
    
    def save(self) -> object:
        """
        Save model instance to Airtable (create or update).
        
        Returns:
        SaveResult object with operation details
        """
    
    def delete(self) -> bool:
        """
        Delete record from Airtable.
        
        Returns:
        True if deletion was successful
        """
    
    def to_record(self) -> dict:
        """
        Convert model instance to Airtable record dict.
        
        Returns:
        Dict with 'id', 'createdTime', and 'fields' keys
        """
    
    @classmethod
    def from_record(cls, record: dict) -> 'Model':
        """
        Create model instance from Airtable record.
        
        Parameters:
        - record: Record dict from Airtable API
        
        Returns:
        Model instance populated with record data
        """
    
    @classmethod
    def from_records(cls, records: list[dict]) -> list['Model']:
        """
        Create multiple model instances from record list.
        
        Parameters:
        - records: List of record dicts
        
        Returns:
        List of model instances
        """
    
    @classmethod
    def all(cls, **options) -> list['Model']:
        """
        Retrieve all records as model instances.
        
        Parameters:
        - options: Same as Table.all() options
        
        Returns:
        List of model instances
        """
    
    @classmethod
    def first(cls, **options) -> Optional['Model']:
        """
        Get first matching record as model instance.
        
        Parameters:
        - options: Same as Table.first() options
        
        Returns:
        Model instance or None
        """
    
    @classmethod
    def batch_save(cls, instances: list['Model']) -> list[object]:
        """
        Save multiple model instances efficiently.
        
        Parameters:
        - instances: List of model instances to save
        
        Returns:
        List of SaveResult objects
        """

class SaveResult:
    """Result object returned from save operations."""
    
    @property
    def created(self) -> bool:
        """True if record was created, False if updated."""
    
    @property
    def record(self) -> dict:
        """The saved record dict."""

Field Types

Comprehensive field type system with automatic validation, conversion, and type safety for different Airtable field types.

# Text fields
class TextField:
    def __init__(self, field_name: str):
        """Single line text field."""

class RichTextField:
    def __init__(self, field_name: str):
        """Rich text field with formatting."""

class EmailField:
    def __init__(self, field_name: str):
        """Email address field with validation."""

class UrlField:
    def __init__(self, field_name: str):
        """URL field with validation."""

class PhoneNumberField:
    def __init__(self, field_name: str):
        """Phone number field."""

# Numeric fields
class NumberField:
    def __init__(self, field_name: str):
        """Number field (integer or decimal)."""

class CurrencyField:
    def __init__(self, field_name: str):
        """Currency field with formatting."""

class PercentField:
    def __init__(self, field_name: str):
        """Percentage field."""

class DurationField:
    def __init__(self, field_name: str):
        """Duration field (time intervals)."""

class RatingField:
    def __init__(self, field_name: str):
        """Rating field (star ratings)."""

# Boolean and selection fields
class CheckboxField:
    def __init__(self, field_name: str):
        """Checkbox field (boolean values)."""

class SelectField:
    def __init__(self, field_name: str):
        """Single select field (dropdown)."""

class MultipleSelectField:
    def __init__(self, field_name: str):
        """Multiple select field (multiple choices)."""

# Date and time fields
class DateField:
    def __init__(self, field_name: str):
        """Date field (date only)."""

class DatetimeField:
    def __init__(self, field_name: str):
        """Datetime field (date and time)."""

class CreatedTimeField:
    def __init__(self, field_name: str):
        """Auto-populated creation time (read-only)."""

class LastModifiedTimeField:
    def __init__(self, field_name: str):
        """Auto-populated modification time (read-only)."""

# File and attachment fields
class AttachmentField:
    def __init__(self, field_name: str):
        """File attachment field."""

# Relationship fields
class LinkField:
    def __init__(self, field_name: str, linked_model: type = None):
        """
        Link to another table field.
        
        Parameters:
        - field_name: Name of link field
        - linked_model: Model class for linked table (optional)
        """

# Computed fields (read-only)
class FormulaField:
    def __init__(self, field_name: str):
        """Formula field (computed, read-only)."""

class LookupField:
    def __init__(self, field_name: str):
        """Lookup field (from linked records, read-only)."""

class RollupField:
    def __init__(self, field_name: str):
        """Rollup field (aggregates from linked records, read-only)."""

class CountField:
    def __init__(self, field_name: str):
        """Count field (counts linked records, read-only)."""

# System fields
class CreatedByField:
    def __init__(self, field_name: str):
        """User who created record (read-only)."""

class LastModifiedByField:
    def __init__(self, field_name: str):
        """User who last modified record (read-only)."""

class CollaboratorField:
    def __init__(self, field_name: str):
        """Single collaborator field."""

class MultipleCollaboratorsField:
    def __init__(self, field_name: str):
        """Multiple collaborators field."""

# Specialized fields
class AutoNumberField:
    def __init__(self, field_name: str):
        """Auto-incrementing number field (read-only)."""

class BarcodeField:
    def __init__(self, field_name: str):
        """Barcode field."""

class ButtonField:
    def __init__(self, field_name: str):
        """Button field (triggers actions)."""

Usage Examples

Basic Model Definition

from pyairtable.orm import Model, fields

class Contact(Model):
    # Required Meta class with table configuration
    class Meta:
        base_id = 'app1234567890abcde'
        table_name = 'Contacts'
        api_key = 'your_access_token'
        
    # Field definitions
    name = fields.TextField('Name')
    email = fields.EmailField('Email')
    phone = fields.PhoneNumberField('Phone')
    company = fields.TextField('Company') 
    active = fields.CheckboxField('Active')
    rating = fields.RatingField('Rating')
    notes = fields.RichTextField('Notes')
    created = fields.CreatedTimeField('Created')

# Create and save records
contact = Contact(
    name='John Doe',
    email='john@example.com',
    company='Acme Corp',
    active=True,
    rating=5
)

# Save to Airtable (creates new record)
result = contact.save()
print(f"Created record: {result.record['id']}")

# Update and save again
contact.phone = '+1-555-0123'
contact.notes = 'Updated contact info'
result = contact.save()  # Updates existing record
print(f"Updated: {result.created}")  # False for update

Advanced Model Features

from datetime import date, datetime
from pyairtable.orm import Model, fields

class Employee(Model):
    class Meta:
        base_id = 'app1234567890abcde'
        table_name = 'Employees'
        api_key = 'your_access_token'
        timeout = (5, 30)  # Connection and read timeouts
        typecast = True    # Enable automatic type conversion
        
    # Personal info
    first_name = fields.TextField('First Name')
    last_name = fields.TextField('Last Name')
    email = fields.EmailField('Email')
    phone = fields.PhoneNumberField('Phone')
    
    # Employment details
    employee_id = fields.AutoNumberField('Employee ID')
    department = fields.SelectField('Department')
    position = fields.TextField('Position')
    salary = fields.CurrencyField('Salary')
    start_date = fields.DateField('Start Date')
    
    # Status and metrics
    active = fields.CheckboxField('Active')
    performance_rating = fields.RatingField('Performance')
    skills = fields.MultipleSelectField('Skills')
    
    # Relationships
    manager = fields.LinkField('Manager', linked_model='Employee')
    projects = fields.LinkField('Projects')  # Links to Project model
    
    # Computed fields (read-only)
    years_employed = fields.FormulaField('Years Employed')
    total_projects = fields.CountField('Total Projects')
    
    # System fields
    created_time = fields.CreatedTimeField('Created')
    modified_time = fields.LastModifiedTimeField('Last Modified')
    created_by = fields.CreatedByField('Created By')

# Create employee with related data
employee = Employee(
    first_name='Jane',
    last_name='Smith',
    email='jane.smith@company.com',
    department='Engineering',
    position='Senior Developer',
    salary=120000,
    start_date=date(2023, 1, 15),
    active=True,
    skills=['Python', 'JavaScript', 'React']
)

result = employee.save()

Querying and Retrieval

# Get all employees
all_employees = Employee.all()

# Filter employees
active_employees = Employee.all(
    formula="AND({Active} = TRUE(), {Department} = 'Engineering')"
)

# Get first matching employee
first_engineer = Employee.first(
    formula="{Department} = 'Engineering'",
    sort=[{'field': 'Start Date', 'direction': 'asc'}]
)

# Pagination
recent_employees = Employee.all(
    sort=[{'field': 'Created', 'direction': 'desc'}],
    max_records=50
)

# Convert existing records to models
from pyairtable import Api

api = Api('your_token')
table = api.table('base_id', 'Employees')
records = table.all()

# Convert to model instances
employees = Employee.from_records(records)
for emp in employees:
    print(f"{emp.first_name} {emp.last_name} - {emp.department}")

Batch Operations

# Create multiple employees
new_employees = [
    Employee(
        first_name='Alice',
        last_name='Johnson',
        email='alice@company.com',
        department='Marketing',
        active=True
    ),
    Employee(
        first_name='Bob',
        last_name='Wilson',
        email='bob@company.com', 
        department='Sales',
        active=True
    )
]

# Batch save for efficiency
results = Employee.batch_save(new_employees)
print(f"Created {len(results)} employees")

# Update multiple employees
for emp in Employee.all(formula="{Department} = 'Engineering'"):
    emp.performance_rating = 5
    
# Save all changes
updated_employees = [emp for emp in Employee.all() if emp.performance_rating == 5]
Employee.batch_save(updated_employees)

Working with Relationships

class Project(Model):
    class Meta:
        base_id = 'app1234567890abcde'
        table_name = 'Projects'
        api_key = 'your_access_token'
        
    name = fields.TextField('Name')
    description = fields.RichTextField('Description')
    status = fields.SelectField('Status')
    start_date = fields.DateField('Start Date')
    end_date = fields.DateField('End Date')
    
    # Link to employees
    team_members = fields.LinkField('Team Members', linked_model=Employee)
    project_manager = fields.LinkField('Project Manager', linked_model=Employee)
    
    # Computed fields
    team_size = fields.CountField('Team Size')
    total_salary_cost = fields.RollupField('Total Salary Cost')

# Create project with team members
project = Project(
    name='New Product Launch',
    description='Launch our new mobile app',
    status='In Progress',
    start_date=date(2024, 1, 1)
)

# Save project first
project.save()

# Add team members (assumes employees already exist)
engineer1 = Employee.first(formula="{Email} = 'jane.smith@company.com'")
engineer2 = Employee.first(formula="{Email} = 'alice@company.com'")

if engineer1 and engineer2:
    project.team_members = [engineer1.id, engineer2.id]  # Use record IDs
    project.project_manager = engineer1.id
    project.save()

Field Validation and Error Handling

from pyairtable.exceptions import PyAirtableError

try:
    # Invalid email will raise validation error
    contact = Contact(
        name='Test User',
        email='invalid-email-format',  # Invalid email
        active=True
    )
    contact.save()
    
except PyAirtableError as e:
    print(f"Validation error: {e}")

# Type conversion with typecast
employee = Employee(
    first_name='John',
    last_name='Doe',
    salary='85000',  # String will be converted to number
    start_date='2023-06-15',  # String will be converted to date
    active='true'  # String will be converted to boolean
)

# Save with automatic type conversion
result = employee.save()

Custom Model Methods

class Employee(Model):
    # ... field definitions ...
    
    @property
    def full_name(self) -> str:
        """Get employee's full name."""
        return f"{self.first_name} {self.last_name}"
    
    @property
    def is_senior(self) -> bool:
        """Check if employee is in senior position."""
        senior_titles = ['Senior', 'Lead', 'Principal', 'Manager', 'Director']
        return any(title in self.position for title in senior_titles)
    
    def give_raise(self, amount: float) -> None:
        """Give employee a salary raise."""
        self.salary += amount
        
    def transfer_department(self, new_department: str) -> None:
        """Transfer employee to new department."""
        old_dept = self.department
        self.department = new_department
        self.notes = f"Transferred from {old_dept} to {new_department}"
    
    @classmethod
    def get_by_email(cls, email: str) -> Optional['Employee']:
        """Find employee by email address."""
        return cls.first(formula=f"{{Email}} = '{email}'")
    
    @classmethod
    def active_employees(cls) -> list['Employee']:
        """Get all active employees."""
        return cls.all(formula="{Active} = TRUE()")

# Usage
employee = Employee.get_by_email('jane.smith@company.com')
if employee:
    print(f"Found: {employee.full_name}")
    if employee.is_senior:
        employee.give_raise(5000)
        employee.save()

Install with Tessl CLI

npx tessl i tessl/pypi-pyairtable

docs

attachments.md

cli.md

comments.md

core-api.md

enterprise.md

formulas.md

index.md

orm.md

record-operations.md

testing.md

webhooks.md

tile.json