Python client for the Airtable API providing comprehensive database operations, ORM functionality, enterprise features, and testing utilities
—
Mock Airtable APIs and helper functions for unit testing applications that use pyAirtable. Provides fake data generation, API simulation, and testing infrastructure without making real API calls.
Context manager that intercepts pyAirtable API calls and provides controllable mock responses for testing.
class MockAirtable:
def __init__(self, passthrough: bool = False):
"""
Initialize mock Airtable instance.
Parameters:
- passthrough: If True, unmocked methods make real requests
"""
def __enter__(self) -> 'MockAirtable':
"""Enter context manager and start mocking."""
def __exit__(self, *exc_info) -> None:
"""Exit context manager and stop mocking."""
def add_records(self, base_id: str, table_name: str, records: list[dict]) -> list[dict]:
"""
Add mock records to table.
Parameters:
- base_id: Base ID for records
- table_name: Table name for records
- records: List of record dicts or field dicts
Returns:
List of normalized record dicts with IDs
"""
def set_records(self, base_id: str, table_name: str, records: list[dict]) -> None:
"""
Replace all mock records for table.
Parameters:
- base_id: Base ID
- table_name: Table name
- records: List of record dicts to set
"""
def clear(self) -> None:
"""Clear all mock records."""
def set_passthrough(self, allowed: bool):
"""Context manager to temporarily enable/disable passthrough."""
def enable_passthrough(self):
"""Enable passthrough for real API calls."""
def disable_passthrough(self):
"""Disable passthrough (mock only)."""
def fake_record(fields: Optional[dict] = None, id: Optional[str] = None, **other_fields) -> dict:
"""
Generate fake record dict with proper structure.
Parameters:
- fields: Field values dict
- id: Record ID (auto-generated if not provided)
- other_fields: Additional field values as kwargs
Returns:
Complete record dict with id, createdTime, and fields
"""
def fake_user(value=None) -> dict:
"""
Generate fake user/collaborator dict.
Parameters:
- value: Name/identifier for user (auto-generated if not provided)
Returns:
User dict with id, email, and name
"""
def fake_attachment(url: str = "", filename: str = "") -> dict:
"""
Generate fake attachment dict.
Parameters:
- url: Attachment URL (auto-generated if not provided)
- filename: Filename (derived from URL if not provided)
Returns:
Attachment dict with id, url, filename, size, and type
"""
def fake_id(type: str = "rec", value=None) -> str:
"""
Generate fake Airtable-style ID.
Parameters:
- type: ID prefix (rec, app, tbl, etc.)
- value: Custom value to embed in ID
Returns:
Airtable-formatted ID string
"""from pyairtable import Api
from pyairtable.testing import MockAirtable, fake_record
def test_record_operations():
# Mock all pyAirtable API calls
with MockAirtable() as mock:
# Add test records
mock.add_records('app123', 'Contacts', [
{'Name': 'John Doe', 'Email': 'john@example.com'},
{'Name': 'Jane Smith', 'Email': 'jane@example.com'}
])
# Use normal pyAirtable code - no real API calls made
api = Api('fake_token')
table = api.table('app123', 'Contacts')
# Get all records (returns mock data)
records = table.all()
assert len(records) == 2
assert records[0]['fields']['Name'] == 'John Doe'
# Create record (adds to mock data)
new_record = table.create({'Name': 'Bob Wilson', 'Email': 'bob@example.com'})
assert new_record['id'].startswith('rec')
# Verify creation
all_records = table.all()
assert len(all_records) == 3import pytest
from pyairtable.testing import MockAirtable
@pytest.fixture(autouse=True)
def mock_airtable():
"""Auto-use fixture that mocks Airtable for all tests."""
with MockAirtable() as mock:
yield mock
def test_my_function(mock_airtable):
# Add test data
mock_airtable.add_records('base_id', 'table_name', [
{'Status': 'Active', 'Count': 5},
{'Status': 'Inactive', 'Count': 2}
])
# Test your function that uses pyAirtable
result = my_function_that_uses_airtable()
assert result == expected_valuefrom pyairtable.testing import MockAirtable, fake_record, fake_user, fake_attachment
def test_complex_data_structures():
with MockAirtable() as mock:
# Create records with various field types
test_records = [
fake_record({
'Name': 'Project Alpha',
'Status': 'In Progress',
'Collaborators': [fake_user('Alice'), fake_user('Bob')],
'Attachments': [
fake_attachment('https://example.com/doc.pdf', 'project_spec.pdf'),
fake_attachment('https://example.com/img.png', 'mockup.png')
],
'Priority': 5,
'Active': True
}, id='rec001'),
fake_record({
'Name': 'Project Beta',
'Status': 'Complete',
'Collaborators': [fake_user('Charlie')],
'Priority': 3,
'Active': False
}, id='rec002')
]
mock.set_records('app123', 'Projects', test_records)
# Test filtering and retrieval
api = Api('test_token')
table = api.table('app123', 'Projects')
active_projects = table.all(formula="{Active} = TRUE()")
assert len(active_projects) == 1
assert active_projects[0]['fields']['Name'] == 'Project Alpha'import requests
from pyairtable.testing import MockAirtable
def test_api_error_handling():
with MockAirtable(passthrough=False) as mock:
# Add limited test data
mock.add_records('app123', 'table1', [{'Name': 'Test'}])
api = Api('test_token')
table = api.table('app123', 'table1')
# This works - record exists in mock
record = table.get('rec000000000000001')
assert record['fields']['Name'] == 'Test'
# This raises KeyError - record not in mock data
with pytest.raises(KeyError):
table.get('rec999999999999999')
def test_with_real_api_fallback():
"""Test that combines mocking with real API calls."""
with MockAirtable() as mock:
# Enable passthrough for schema calls
with mock.enable_passthrough():
# This makes a real API call (if needed)
api = Api(os.environ['AIRTABLE_API_KEY'])
base = api.base('real_base_id')
schema = base.schema() # Real API call
# Back to mocking for data operations
mock.add_records('real_base_id', 'real_table', [
{'Field1': 'Mock Data'}
])
table = base.table('real_table')
records = table.all() # Uses mock data
assert records[0]['fields']['Field1'] == 'Mock Data'def test_batch_operations():
with MockAirtable() as mock:
# Test batch create
api = Api('test_token')
table = api.table('app123', 'Contacts')
batch_data = [
{'Name': f'User {i}', 'Email': f'user{i}@example.com'}
for i in range(1, 6)
]
created = table.batch_create(batch_data)
assert len(created) == 5
# Verify all records exist
all_records = table.all()
assert len(all_records) == 5
# Test batch update
updates = [
{'id': created[0]['id'], 'fields': {'Status': 'VIP'}},
{'id': created[1]['id'], 'fields': {'Status': 'Regular'}}
]
updated = table.batch_update(updates)
assert len(updated) == 2
assert updated[0]['fields']['Status'] == 'VIP'
# Test batch upsert
upsert_data = [
{'Name': 'User 1', 'Email': 'user1@example.com', 'Status': 'Premium'}, # Update
{'Name': 'User 6', 'Email': 'user6@example.com', 'Status': 'New'} # Create
]
result = table.batch_upsert(upsert_data, key_fields=['Name'])
assert len(result['updatedRecords']) == 1
assert len(result['createdRecords']) == 1from pyairtable.orm import Model, fields
from pyairtable.testing import MockAirtable, fake_meta
class TestContact(Model):
# Use fake_meta for testing
Meta = fake_meta(
base_id='app123',
table_name='Contacts',
api_key='test_token'
)
name = fields.TextField('Name')
email = fields.EmailField('Email')
active = fields.CheckboxField('Active')
def test_orm_with_mock():
with MockAirtable() as mock:
# Create and save model instance
contact = TestContact(
name='John Doe',
email='john@example.com',
active=True
)
result = contact.save()
assert result.created is True
assert contact.name == 'John Doe'
# Test retrieval
all_contacts = TestContact.all()
assert len(all_contacts) == 1
assert all_contacts[0].name == 'John Doe'
# Test update
contact.active = False
result = contact.save()
assert result.created is False # Update, not createInstall with Tessl CLI
npx tessl i tessl/pypi-pyairtable