chub-dev 0.1.0 → 0.1.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/bin/chub-mcp +2 -0
- package/dist/airtable/docs/database/javascript/DOC.md +1437 -0
- package/dist/airtable/docs/database/python/DOC.md +1735 -0
- package/dist/amplitude/docs/analytics/javascript/DOC.md +1282 -0
- package/dist/amplitude/docs/analytics/python/DOC.md +1199 -0
- package/dist/anthropic/docs/claude-api/javascript/DOC.md +503 -0
- package/dist/anthropic/docs/claude-api/python/DOC.md +389 -0
- package/dist/asana/docs/tasks/DOC.md +1396 -0
- package/dist/assemblyai/docs/transcription/DOC.md +1043 -0
- package/dist/atlassian/docs/confluence/javascript/DOC.md +1347 -0
- package/dist/atlassian/docs/confluence/python/DOC.md +1604 -0
- package/dist/auth0/docs/identity/javascript/DOC.md +968 -0
- package/dist/auth0/docs/identity/python/DOC.md +1199 -0
- package/dist/aws/docs/s3/javascript/DOC.md +1773 -0
- package/dist/aws/docs/s3/python/DOC.md +1807 -0
- package/dist/binance/docs/trading/javascript/DOC.md +1315 -0
- package/dist/binance/docs/trading/python/DOC.md +1454 -0
- package/dist/braintree/docs/gateway/javascript/DOC.md +1278 -0
- package/dist/braintree/docs/gateway/python/DOC.md +1179 -0
- package/dist/chromadb/docs/embeddings-db/javascript/DOC.md +1263 -0
- package/dist/chromadb/docs/embeddings-db/python/DOC.md +1707 -0
- package/dist/clerk/docs/auth/javascript/DOC.md +1220 -0
- package/dist/clerk/docs/auth/python/DOC.md +274 -0
- package/dist/cloudflare/docs/workers/javascript/DOC.md +918 -0
- package/dist/cloudflare/docs/workers/python/DOC.md +994 -0
- package/dist/cockroachdb/docs/distributed-db/DOC.md +1500 -0
- package/dist/cohere/docs/llm/DOC.md +1335 -0
- package/dist/datadog/docs/monitoring/javascript/DOC.md +1740 -0
- package/dist/datadog/docs/monitoring/python/DOC.md +1815 -0
- package/dist/deepgram/docs/speech/javascript/DOC.md +885 -0
- package/dist/deepgram/docs/speech/python/DOC.md +685 -0
- package/dist/deepl/docs/translation/javascript/DOC.md +887 -0
- package/dist/deepl/docs/translation/python/DOC.md +944 -0
- package/dist/deepseek/docs/llm/DOC.md +1220 -0
- package/dist/directus/docs/headless-cms/javascript/DOC.md +1128 -0
- package/dist/directus/docs/headless-cms/python/DOC.md +1276 -0
- package/dist/discord/docs/bot/javascript/DOC.md +1090 -0
- package/dist/discord/docs/bot/python/DOC.md +1130 -0
- package/dist/elasticsearch/docs/search/DOC.md +1634 -0
- package/dist/elevenlabs/docs/text-to-speech/javascript/DOC.md +336 -0
- package/dist/elevenlabs/docs/text-to-speech/python/DOC.md +552 -0
- package/dist/firebase/docs/auth/DOC.md +1015 -0
- package/dist/gemini/docs/genai/javascript/DOC.md +691 -0
- package/dist/gemini/docs/genai/python/DOC.md +555 -0
- package/dist/github/docs/octokit/DOC.md +1560 -0
- package/dist/google/docs/bigquery/javascript/DOC.md +1688 -0
- package/dist/google/docs/bigquery/python/DOC.md +1503 -0
- package/dist/hubspot/docs/crm/javascript/DOC.md +1805 -0
- package/dist/hubspot/docs/crm/python/DOC.md +2033 -0
- package/dist/huggingface/docs/transformers/DOC.md +948 -0
- package/dist/intercom/docs/messaging/javascript/DOC.md +1844 -0
- package/dist/intercom/docs/messaging/python/DOC.md +1797 -0
- package/dist/jira/docs/issues/javascript/DOC.md +1420 -0
- package/dist/jira/docs/issues/python/DOC.md +1492 -0
- package/dist/kafka/docs/streaming/javascript/DOC.md +1671 -0
- package/dist/kafka/docs/streaming/python/DOC.md +1464 -0
- package/dist/landingai-ade/docs/api/DOC.md +620 -0
- package/dist/landingai-ade/docs/sdk/python/DOC.md +489 -0
- package/dist/landingai-ade/docs/sdk/typescript/DOC.md +542 -0
- package/dist/landingai-ade/skills/SKILL.md +489 -0
- package/dist/launchdarkly/docs/feature-flags/javascript/DOC.md +1191 -0
- package/dist/launchdarkly/docs/feature-flags/python/DOC.md +1671 -0
- package/dist/linear/docs/tracker/DOC.md +1554 -0
- package/dist/livekit/docs/realtime/javascript/DOC.md +303 -0
- package/dist/livekit/docs/realtime/python/DOC.md +163 -0
- package/dist/mailchimp/docs/marketing/DOC.md +1420 -0
- package/dist/meilisearch/docs/search/DOC.md +1241 -0
- package/dist/microsoft/docs/onedrive/javascript/DOC.md +1421 -0
- package/dist/microsoft/docs/onedrive/python/DOC.md +1549 -0
- package/dist/mongodb/docs/atlas/DOC.md +2041 -0
- package/dist/notion/docs/workspace-api/javascript/DOC.md +1435 -0
- package/dist/notion/docs/workspace-api/python/DOC.md +1400 -0
- package/dist/okta/docs/identity/javascript/DOC.md +1171 -0
- package/dist/okta/docs/identity/python/DOC.md +1401 -0
- package/dist/openai/docs/chat/javascript/DOC.md +407 -0
- package/dist/openai/docs/chat/python/DOC.md +568 -0
- package/dist/paypal/docs/checkout/DOC.md +278 -0
- package/dist/pinecone/docs/sdk/javascript/DOC.md +984 -0
- package/dist/pinecone/docs/sdk/python/DOC.md +1395 -0
- package/dist/plaid/docs/banking/javascript/DOC.md +1163 -0
- package/dist/plaid/docs/banking/python/DOC.md +1203 -0
- package/dist/playwright-community/skills/login-flows/SKILL.md +108 -0
- package/dist/postmark/docs/transactional-email/DOC.md +1168 -0
- package/dist/prisma/docs/orm/javascript/DOC.md +1419 -0
- package/dist/prisma/docs/orm/python/DOC.md +1317 -0
- package/dist/qdrant/docs/vector-search/javascript/DOC.md +1221 -0
- package/dist/qdrant/docs/vector-search/python/DOC.md +1653 -0
- package/dist/rabbitmq/docs/message-queue/javascript/DOC.md +1193 -0
- package/dist/rabbitmq/docs/message-queue/python/DOC.md +1243 -0
- package/dist/razorpay/docs/payments/javascript/DOC.md +1219 -0
- package/dist/razorpay/docs/payments/python/DOC.md +1330 -0
- package/dist/redis/docs/key-value/javascript/DOC.md +1851 -0
- package/dist/redis/docs/key-value/python/DOC.md +2054 -0
- package/dist/registry.json +2817 -0
- package/dist/replicate/docs/model-hosting/DOC.md +1318 -0
- package/dist/resend/docs/email/DOC.md +1271 -0
- package/dist/salesforce/docs/crm/javascript/DOC.md +1241 -0
- package/dist/salesforce/docs/crm/python/DOC.md +1183 -0
- package/dist/search-index.json +1 -0
- package/dist/sendgrid/docs/email-api/javascript/DOC.md +371 -0
- package/dist/sendgrid/docs/email-api/python/DOC.md +656 -0
- package/dist/sentry/docs/error-tracking/javascript/DOC.md +1073 -0
- package/dist/sentry/docs/error-tracking/python/DOC.md +1309 -0
- package/dist/shopify/docs/storefront/DOC.md +457 -0
- package/dist/slack/docs/workspace/javascript/DOC.md +933 -0
- package/dist/slack/docs/workspace/python/DOC.md +271 -0
- package/dist/square/docs/payments/javascript/DOC.md +1855 -0
- package/dist/square/docs/payments/python/DOC.md +1728 -0
- package/dist/stripe/docs/api/DOC.md +1727 -0
- package/dist/stripe/docs/payments/DOC.md +1726 -0
- package/dist/stytch/docs/auth/javascript/DOC.md +1813 -0
- package/dist/stytch/docs/auth/python/DOC.md +1962 -0
- package/dist/supabase/docs/client/DOC.md +1606 -0
- package/dist/twilio/docs/messaging/python/DOC.md +469 -0
- package/dist/twilio/docs/messaging/typescript/DOC.md +946 -0
- package/dist/vercel/docs/platform/DOC.md +1940 -0
- package/dist/weaviate/docs/vector-db/javascript/DOC.md +1268 -0
- package/dist/weaviate/docs/vector-db/python/DOC.md +1388 -0
- package/dist/zendesk/docs/support/javascript/DOC.md +2150 -0
- package/dist/zendesk/docs/support/python/DOC.md +2297 -0
- package/package.json +22 -6
- package/skills/get-api-docs/SKILL.md +84 -0
- package/src/commands/annotate.js +83 -0
- package/src/commands/build.js +12 -1
- package/src/commands/feedback.js +150 -0
- package/src/commands/get.js +83 -42
- package/src/commands/search.js +7 -0
- package/src/index.js +43 -17
- package/src/lib/analytics.js +90 -0
- package/src/lib/annotations.js +57 -0
- package/src/lib/bm25.js +170 -0
- package/src/lib/cache.js +69 -6
- package/src/lib/config.js +8 -3
- package/src/lib/identity.js +99 -0
- package/src/lib/registry.js +103 -20
- package/src/lib/telemetry.js +86 -0
- package/src/mcp/server.js +177 -0
- package/src/mcp/tools.js +251 -0
|
@@ -0,0 +1,1735 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: database
|
|
3
|
+
description: "Airtable Python SDK (pyairtable) — use the official pyairtable package for Airtable API operations"
|
|
4
|
+
metadata:
|
|
5
|
+
languages: "python"
|
|
6
|
+
versions: "3.1.1"
|
|
7
|
+
updated-on: "2026-03-02"
|
|
8
|
+
source: maintainer
|
|
9
|
+
tags: "airtable,database,low-code,spreadsheet,api"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Airtable Python SDK (pyairtable) - Version 3.1.1
|
|
13
|
+
|
|
14
|
+
## Golden Rule
|
|
15
|
+
|
|
16
|
+
**ALWAYS use the official `pyairtable` package (version 3.1.1 or later)**
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install pyairtable
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**DO NOT use:**
|
|
23
|
+
- Deprecated packages like `airtable` (v0.4.8, last updated 2021)
|
|
24
|
+
- `airtable-python-wrapper` (v0.15.3, now renamed to pyairtable)
|
|
25
|
+
- `python-airtable` or other unofficial wrappers
|
|
26
|
+
- The old API key authentication method (deprecated as of February 1, 2024)
|
|
27
|
+
|
|
28
|
+
**ALWAYS use Personal Access Tokens (PATs) for authentication**, not the deprecated API keys.
|
|
29
|
+
|
|
30
|
+
**NOTE:** `pyairtable` is the current and actively maintained name for what was previously called `airtable-python-wrapper`.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install pyairtable
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Environment Variable Setup
|
|
39
|
+
|
|
40
|
+
Create a `.env` file:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
AIRTABLE_API_KEY=your_personal_access_token_here
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For production applications, use proper secret management systems.
|
|
47
|
+
|
|
48
|
+
## Authentication & Initialization
|
|
49
|
+
|
|
50
|
+
### Personal Access Token Setup
|
|
51
|
+
|
|
52
|
+
1. Visit https://airtable.com/create/tokens to create a Personal Access Token
|
|
53
|
+
2. Name your token (e.g., "My App Token")
|
|
54
|
+
3. Add required scopes:
|
|
55
|
+
- `data.records:read` - to read records
|
|
56
|
+
- `data.records:write` - to create/update/delete records
|
|
57
|
+
- `schema.bases:read` - to read base structure
|
|
58
|
+
- `schema.bases:write` - to modify base structure (optional)
|
|
59
|
+
4. Select base access level (specific bases or all workspace bases)
|
|
60
|
+
5. Copy the token immediately (shown only once)
|
|
61
|
+
|
|
62
|
+
### Basic Configuration
|
|
63
|
+
|
|
64
|
+
**Option 1: Direct Token Initialization**
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pyairtable import Api
|
|
68
|
+
|
|
69
|
+
api = Api('your_personal_access_token')
|
|
70
|
+
table = api.table('appExampleBaseId', 'tblExampleTableId')
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Option 2: Environment Variable**
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import os
|
|
77
|
+
from pyairtable import Api
|
|
78
|
+
|
|
79
|
+
api = Api(os.environ['AIRTABLE_API_KEY'])
|
|
80
|
+
table = api.table('appExampleBaseId', 'Table Name')
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Option 3: Using Base Instance**
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from pyairtable import Api
|
|
87
|
+
|
|
88
|
+
api = Api(os.environ['AIRTABLE_API_KEY'])
|
|
89
|
+
base = api.base('appExampleBaseId')
|
|
90
|
+
table = base.table('Table Name')
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Advanced Configuration
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from pyairtable import Api, retry_strategy
|
|
97
|
+
|
|
98
|
+
# Custom timeout (connect_timeout, read_timeout)
|
|
99
|
+
api = Api('token', timeout=(2, 5))
|
|
100
|
+
|
|
101
|
+
# Enable retry strategy for rate limiting
|
|
102
|
+
api = Api('token', retry_strategy=True)
|
|
103
|
+
|
|
104
|
+
# Custom retry strategy
|
|
105
|
+
custom_retry = retry_strategy(
|
|
106
|
+
total=10,
|
|
107
|
+
status_forcelist=(429, 500, 502, 503, 504),
|
|
108
|
+
backoff_factor=0.2
|
|
109
|
+
)
|
|
110
|
+
api = Api('token', retry_strategy=custom_retry)
|
|
111
|
+
|
|
112
|
+
# Disable retries
|
|
113
|
+
api = Api('token', retry_strategy=None)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Finding Your Base and Table IDs
|
|
117
|
+
|
|
118
|
+
1. Go to https://airtable.com/api
|
|
119
|
+
2. Select your base
|
|
120
|
+
3. Base ID format: `appXXXXXXXXXXXXXX`
|
|
121
|
+
4. Table ID format: `tblXXXXXXXXXXXXXX` (or use table name as string)
|
|
122
|
+
|
|
123
|
+
## Core API Surfaces
|
|
124
|
+
|
|
125
|
+
### Reading Records
|
|
126
|
+
|
|
127
|
+
#### Get Single Record by ID
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from pyairtable import Api
|
|
131
|
+
|
|
132
|
+
api = Api(os.environ['AIRTABLE_API_KEY'])
|
|
133
|
+
table = api.table('appBaseId', 'Tasks')
|
|
134
|
+
|
|
135
|
+
# Get single record
|
|
136
|
+
record = table.get('recwPQIfs4wKPyc9D')
|
|
137
|
+
|
|
138
|
+
print(f"Record ID: {record['id']}")
|
|
139
|
+
print(f"Name: {record['fields']['Name']}")
|
|
140
|
+
print(f"Status: {record['fields']['Status']}")
|
|
141
|
+
print(f"Created: {record['createdTime']}")
|
|
142
|
+
|
|
143
|
+
# Access all fields
|
|
144
|
+
fields = record['fields']
|
|
145
|
+
for field_name, value in fields.items():
|
|
146
|
+
print(f"{field_name}: {value}")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### Get All Records
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# Get all records from table
|
|
153
|
+
all_records = table.all()
|
|
154
|
+
|
|
155
|
+
for record in all_records:
|
|
156
|
+
print(f"ID: {record['id']}")
|
|
157
|
+
print(f"Name: {record['fields']['Name']}")
|
|
158
|
+
|
|
159
|
+
# With view parameter
|
|
160
|
+
view_records = table.all(view='Active Tasks')
|
|
161
|
+
|
|
162
|
+
# With specific fields only
|
|
163
|
+
limited_fields = table.all(fields=['Name', 'Status', 'Priority'])
|
|
164
|
+
|
|
165
|
+
# With max records limit
|
|
166
|
+
limited_records = table.all(max_records=50)
|
|
167
|
+
|
|
168
|
+
# Combine multiple parameters
|
|
169
|
+
filtered_records = table.all(
|
|
170
|
+
view='Active Tasks',
|
|
171
|
+
fields=['Name', 'Status'],
|
|
172
|
+
max_records=100
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Get First Matching Record
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
# Get first record matching formula
|
|
180
|
+
first_record = table.first(formula="{Status} = 'Active'")
|
|
181
|
+
|
|
182
|
+
if first_record:
|
|
183
|
+
print(f"Found: {first_record['fields']['Name']}")
|
|
184
|
+
else:
|
|
185
|
+
print("No matching record found")
|
|
186
|
+
|
|
187
|
+
# With view
|
|
188
|
+
first_in_view = table.first(view='High Priority')
|
|
189
|
+
|
|
190
|
+
# With sort
|
|
191
|
+
first_sorted = table.first(
|
|
192
|
+
formula="{Priority} = 'High'",
|
|
193
|
+
sort=['Due Date']
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Iterate Through Pages
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
# Iterate through paginated results
|
|
201
|
+
for page in table.iterate(page_size=100):
|
|
202
|
+
for record in page:
|
|
203
|
+
print(f"Processing: {record['fields']['Name']}")
|
|
204
|
+
|
|
205
|
+
# With formula filter
|
|
206
|
+
for page in table.iterate(
|
|
207
|
+
formula="{Status} = 'Active'",
|
|
208
|
+
page_size=50
|
|
209
|
+
):
|
|
210
|
+
process_records(page)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Filtering Records
|
|
214
|
+
|
|
215
|
+
#### Using Formula Parameter
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
# Simple equality
|
|
219
|
+
active_tasks = table.all(formula="{Status} = 'Active'")
|
|
220
|
+
|
|
221
|
+
# Multiple conditions with AND
|
|
222
|
+
high_priority = table.all(
|
|
223
|
+
formula="AND({Status} = 'Active', {Priority} = 'High')"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Multiple conditions with OR
|
|
227
|
+
urgent_or_blocked = table.all(
|
|
228
|
+
formula="OR({Status} = 'Urgent', {Status} = 'Blocked')"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Not empty check
|
|
232
|
+
with_titles = table.all(formula="NOT({Title} = '')")
|
|
233
|
+
|
|
234
|
+
# Greater than comparison
|
|
235
|
+
recent_tasks = table.all(formula="{Created} > '2025-01-01'")
|
|
236
|
+
|
|
237
|
+
# Dynamic formula with variables
|
|
238
|
+
email = 'user@example.com'
|
|
239
|
+
user_records = table.all(formula=f"{{Email}} = '{email}'")
|
|
240
|
+
|
|
241
|
+
# Complex nested formula
|
|
242
|
+
complex = table.all(
|
|
243
|
+
formula="""
|
|
244
|
+
AND(
|
|
245
|
+
{Status} = 'In Progress',
|
|
246
|
+
{Priority} = 'High',
|
|
247
|
+
{Assignee} != '',
|
|
248
|
+
{Due Date} <= TODAY()
|
|
249
|
+
)
|
|
250
|
+
"""
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# String search (case-insensitive)
|
|
254
|
+
search_term = 'urgent'
|
|
255
|
+
search_results = table.all(
|
|
256
|
+
formula=f"SEARCH(LOWER('{search_term}'), LOWER({{Notes}})) > 0"
|
|
257
|
+
)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Using Formula Helpers
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from pyairtable.formulas import match, EQUAL, AND, OR, NOT, IF
|
|
264
|
+
|
|
265
|
+
# Simple match
|
|
266
|
+
formula = match({'Status': 'Active'})
|
|
267
|
+
records = table.all(formula=formula)
|
|
268
|
+
|
|
269
|
+
# Multiple field match (AND)
|
|
270
|
+
formula = match({'Status': 'Active', 'Priority': 'High'})
|
|
271
|
+
records = table.all(formula=formula)
|
|
272
|
+
|
|
273
|
+
# Using formula operators
|
|
274
|
+
formula = AND(
|
|
275
|
+
EQUAL('Status', 'Active'),
|
|
276
|
+
EQUAL('Priority', 'High')
|
|
277
|
+
)
|
|
278
|
+
records = table.all(formula=str(formula))
|
|
279
|
+
|
|
280
|
+
# OR condition
|
|
281
|
+
formula = OR(
|
|
282
|
+
EQUAL('Status', 'Urgent'),
|
|
283
|
+
EQUAL('Status', 'Blocked')
|
|
284
|
+
)
|
|
285
|
+
records = table.all(formula=str(formula))
|
|
286
|
+
|
|
287
|
+
# NOT condition
|
|
288
|
+
formula = NOT(EQUAL('Status', 'Done'))
|
|
289
|
+
records = table.all(formula=str(formula))
|
|
290
|
+
|
|
291
|
+
# Complex formula with IF
|
|
292
|
+
from pyairtable.formulas import IF, GT
|
|
293
|
+
|
|
294
|
+
formula = IF(
|
|
295
|
+
GT('Price', 100),
|
|
296
|
+
'Expensive',
|
|
297
|
+
'Affordable'
|
|
298
|
+
)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Sorting Records
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
# Sort by single field (ascending)
|
|
305
|
+
sorted_asc = table.all(sort=['Name'])
|
|
306
|
+
|
|
307
|
+
# Sort by single field (descending)
|
|
308
|
+
sorted_desc = table.all(sort=['-Created'])
|
|
309
|
+
|
|
310
|
+
# Sort by multiple fields
|
|
311
|
+
multi_sort = table.all(
|
|
312
|
+
sort=['-Priority', 'Due Date', 'Name']
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Combine with filter and fields
|
|
316
|
+
filtered_sorted = table.all(
|
|
317
|
+
formula="{Status} = 'Active'",
|
|
318
|
+
sort=['-Priority', 'Due Date'],
|
|
319
|
+
fields=['Name', 'Status', 'Priority']
|
|
320
|
+
)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Creating Records
|
|
324
|
+
|
|
325
|
+
#### Create Single Record
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
# Basic create
|
|
329
|
+
new_record = table.create({
|
|
330
|
+
'Name': 'New Task',
|
|
331
|
+
'Status': 'To Do',
|
|
332
|
+
'Priority': 'Medium'
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
print(f"Created record: {new_record['id']}")
|
|
336
|
+
print(f"Name: {new_record['fields']['Name']}")
|
|
337
|
+
|
|
338
|
+
# Create with all field types
|
|
339
|
+
record = table.create({
|
|
340
|
+
'Name': 'Complete Task',
|
|
341
|
+
'Description': 'Long description here',
|
|
342
|
+
'Status': 'In Progress',
|
|
343
|
+
'Priority': 'High',
|
|
344
|
+
'Due Date': '2025-12-31',
|
|
345
|
+
'Completed': False,
|
|
346
|
+
'Tags': ['Important', 'Client'],
|
|
347
|
+
'Progress': 50
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
# With typecast (converts strings to appropriate types)
|
|
351
|
+
record_typecast = table.create({
|
|
352
|
+
'Name': 'Task with Typecast',
|
|
353
|
+
'Due Date': '2025-12-31', # String converted to date
|
|
354
|
+
'Count': '42' # String converted to number
|
|
355
|
+
}, typecast=True)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
#### Batch Create (Multiple Records)
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
# Create up to 10 records at once
|
|
362
|
+
records = table.batch_create([
|
|
363
|
+
{'Name': 'Task 1', 'Status': 'To Do'},
|
|
364
|
+
{'Name': 'Task 2', 'Status': 'In Progress'},
|
|
365
|
+
{'Name': 'Task 3', 'Status': 'Done'},
|
|
366
|
+
{'Name': 'Task 4', 'Status': 'To Do'},
|
|
367
|
+
{'Name': 'Task 5', 'Status': 'Review'}
|
|
368
|
+
])
|
|
369
|
+
|
|
370
|
+
print(f"Created {len(records)} records")
|
|
371
|
+
|
|
372
|
+
for record in records:
|
|
373
|
+
print(f"Created: {record['id']} - {record['fields']['Name']}")
|
|
374
|
+
|
|
375
|
+
# With typecast
|
|
376
|
+
records_typecast = table.batch_create([
|
|
377
|
+
{'Name': 'Task 1', 'Count': '10'},
|
|
378
|
+
{'Name': 'Task 2', 'Count': '20'}
|
|
379
|
+
], typecast=True)
|
|
380
|
+
|
|
381
|
+
# Create more than 10 records (automatic batching)
|
|
382
|
+
large_batch = [
|
|
383
|
+
{'Name': f'Task {i}', 'Status': 'To Do'}
|
|
384
|
+
for i in range(1, 51)
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
# Process in batches of 10
|
|
388
|
+
all_created = []
|
|
389
|
+
for i in range(0, len(large_batch), 10):
|
|
390
|
+
batch = large_batch[i:i+10]
|
|
391
|
+
created = table.batch_create(batch)
|
|
392
|
+
all_created.extend(created)
|
|
393
|
+
|
|
394
|
+
print(f"Created {len(all_created)} records total")
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Updating Records
|
|
398
|
+
|
|
399
|
+
#### Update Single Record (Partial Update)
|
|
400
|
+
|
|
401
|
+
```python
|
|
402
|
+
# Update specific fields only
|
|
403
|
+
updated = table.update('recXXXXXXXXXXXXXX', {
|
|
404
|
+
'Status': 'In Progress',
|
|
405
|
+
'Progress': 50
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
print(f"Updated: {updated['fields']['Status']}")
|
|
409
|
+
|
|
410
|
+
# Update multiple fields
|
|
411
|
+
updated = table.update('recXXXXXXXXXXXXXX', {
|
|
412
|
+
'Status': 'Done',
|
|
413
|
+
'Completed': True,
|
|
414
|
+
'Completed Date': '2025-10-25',
|
|
415
|
+
'Notes': 'Task completed successfully'
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
# With typecast
|
|
419
|
+
updated = table.update('recXXXXXXXXXXXXXX', {
|
|
420
|
+
'Count': '100',
|
|
421
|
+
'Due Date': '2025-12-31'
|
|
422
|
+
}, typecast=True)
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
#### Replace Record (Full Update)
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
# Replace entire record (fields not specified will be cleared)
|
|
429
|
+
replaced = table.update(
|
|
430
|
+
'recXXXXXXXXXXXXXX',
|
|
431
|
+
{
|
|
432
|
+
'Name': 'Completely New Task',
|
|
433
|
+
'Status': 'To Do'
|
|
434
|
+
},
|
|
435
|
+
replace=True
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# All other fields will be cleared/empty
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
#### Batch Update (Multiple Records)
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
# Update up to 10 records at once
|
|
445
|
+
updates = [
|
|
446
|
+
{
|
|
447
|
+
'id': 'recXXXXXXXXXXXXXX',
|
|
448
|
+
'fields': {'Status': 'Done'}
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
'id': 'recYYYYYYYYYYYYYY',
|
|
452
|
+
'fields': {'Status': 'In Progress', 'Progress': 75}
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
'id': 'recZZZZZZZZZZZZZZ',
|
|
456
|
+
'fields': {'Status': 'To Do'}
|
|
457
|
+
}
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
updated_records = table.batch_update(updates)
|
|
461
|
+
print(f"Updated {len(updated_records)} records")
|
|
462
|
+
|
|
463
|
+
# With typecast
|
|
464
|
+
updated_typecast = table.batch_update([
|
|
465
|
+
{
|
|
466
|
+
'id': 'recXXXXXXXXXXXXXX',
|
|
467
|
+
'fields': {'Count': '100'}
|
|
468
|
+
}
|
|
469
|
+
], typecast=True)
|
|
470
|
+
|
|
471
|
+
# Batch update more than 10 records
|
|
472
|
+
record_ids = ['rec1', 'rec2', 'rec3', ...] # List of many IDs
|
|
473
|
+
all_updated = []
|
|
474
|
+
|
|
475
|
+
for i in range(0, len(record_ids), 10):
|
|
476
|
+
batch_ids = record_ids[i:i+10]
|
|
477
|
+
batch_updates = [
|
|
478
|
+
{
|
|
479
|
+
'id': record_id,
|
|
480
|
+
'fields': {'Status': 'Archived'}
|
|
481
|
+
}
|
|
482
|
+
for record_id in batch_ids
|
|
483
|
+
]
|
|
484
|
+
updated = table.batch_update(batch_updates)
|
|
485
|
+
all_updated.extend(updated)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### Upsert Operations (Update or Insert)
|
|
489
|
+
|
|
490
|
+
```python
|
|
491
|
+
# Upsert based on key fields
|
|
492
|
+
upsert_result = table.batch_upsert(
|
|
493
|
+
[
|
|
494
|
+
{'Email': 'john@example.com', 'Name': 'John Doe', 'Status': 'Active'},
|
|
495
|
+
{'Email': 'jane@example.com', 'Name': 'Jane Smith', 'Status': 'Active'}
|
|
496
|
+
],
|
|
497
|
+
key_fields=['Email']
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
print(f"Created: {len(upsert_result['createdRecords'])}")
|
|
501
|
+
print(f"Updated: {len(upsert_result['updatedRecords'])}")
|
|
502
|
+
|
|
503
|
+
# Upsert with multiple key fields
|
|
504
|
+
upsert_result = table.batch_upsert(
|
|
505
|
+
[
|
|
506
|
+
{
|
|
507
|
+
'First Name': 'John',
|
|
508
|
+
'Last Name': 'Doe',
|
|
509
|
+
'Email': 'john@example.com',
|
|
510
|
+
'Company': 'Acme Inc'
|
|
511
|
+
}
|
|
512
|
+
],
|
|
513
|
+
key_fields=['First Name', 'Last Name']
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# With typecast
|
|
517
|
+
upsert_typecast = table.batch_upsert(
|
|
518
|
+
[{'Email': 'user@example.com', 'Count': '50'}],
|
|
519
|
+
key_fields=['Email'],
|
|
520
|
+
typecast=True
|
|
521
|
+
)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Deleting Records
|
|
525
|
+
|
|
526
|
+
#### Delete Single Record
|
|
527
|
+
|
|
528
|
+
```python
|
|
529
|
+
# Delete by record ID
|
|
530
|
+
deleted = table.delete('recwPQIfs4wKPyc9D')
|
|
531
|
+
|
|
532
|
+
print(f"Deleted record: {deleted['id']}")
|
|
533
|
+
print(f"Deleted: {deleted['deleted']}") # True
|
|
534
|
+
|
|
535
|
+
# Check if deletion was successful
|
|
536
|
+
if deleted['deleted']:
|
|
537
|
+
print("Record successfully deleted")
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
#### Batch Delete (Multiple Records)
|
|
541
|
+
|
|
542
|
+
```python
|
|
543
|
+
# Delete up to 10 records at once
|
|
544
|
+
deleted_records = table.batch_delete([
|
|
545
|
+
'recXXXXXXXXXXXXXX',
|
|
546
|
+
'recYYYYYYYYYYYYYY',
|
|
547
|
+
'recZZZZZZZZZZZZZZ'
|
|
548
|
+
])
|
|
549
|
+
|
|
550
|
+
print(f"Deleted {len(deleted_records)} records")
|
|
551
|
+
|
|
552
|
+
for record in deleted_records:
|
|
553
|
+
print(f"Deleted: {record['id']}")
|
|
554
|
+
|
|
555
|
+
# Delete more than 10 records
|
|
556
|
+
record_ids = ['rec1', 'rec2', 'rec3', ...] # Many record IDs
|
|
557
|
+
all_deleted = []
|
|
558
|
+
|
|
559
|
+
for i in range(0, len(record_ids), 10):
|
|
560
|
+
batch = record_ids[i:i+10]
|
|
561
|
+
deleted = table.batch_delete(batch)
|
|
562
|
+
all_deleted.extend(deleted)
|
|
563
|
+
|
|
564
|
+
print(f"Deleted {len(all_deleted)} records total")
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### Working with Different Field Types
|
|
568
|
+
|
|
569
|
+
#### Text Fields
|
|
570
|
+
|
|
571
|
+
```python
|
|
572
|
+
record = table.create({
|
|
573
|
+
'Single Line Text': 'Short text',
|
|
574
|
+
'Long Text': 'This is a much longer text\nwith multiple lines',
|
|
575
|
+
'Email': 'user@example.com',
|
|
576
|
+
'URL': 'https://example.com',
|
|
577
|
+
'Phone': '+1-555-0100'
|
|
578
|
+
})
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
#### Number Fields
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
record = table.create({
|
|
585
|
+
'Number': 42,
|
|
586
|
+
'Currency': 99.99,
|
|
587
|
+
'Percent': 0.75,
|
|
588
|
+
'Rating': 5
|
|
589
|
+
})
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
#### Date and Time Fields
|
|
593
|
+
|
|
594
|
+
```python
|
|
595
|
+
from datetime import datetime, date
|
|
596
|
+
|
|
597
|
+
record = table.create({
|
|
598
|
+
'Date': '2025-10-25',
|
|
599
|
+
'DateTime': '2025-10-25T14:30:00.000Z',
|
|
600
|
+
'Date Object': date(2025, 10, 25).isoformat(),
|
|
601
|
+
'DateTime Object': datetime.now().isoformat()
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
# Access date fields
|
|
605
|
+
date_value = record['fields']['Date']
|
|
606
|
+
print(f"Date: {date_value}")
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
#### Checkbox (Boolean) Fields
|
|
610
|
+
|
|
611
|
+
```python
|
|
612
|
+
record = table.create({
|
|
613
|
+
'Completed': True,
|
|
614
|
+
'Active': False
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
# Access checkbox
|
|
618
|
+
is_completed = record['fields']['Completed']
|
|
619
|
+
if is_completed:
|
|
620
|
+
print('Task is completed')
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
#### Single Select Fields
|
|
624
|
+
|
|
625
|
+
```python
|
|
626
|
+
record = table.create({
|
|
627
|
+
'Status': 'In Progress', # Must match exact option name
|
|
628
|
+
'Priority': 'High'
|
|
629
|
+
})
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
#### Multiple Select Fields
|
|
633
|
+
|
|
634
|
+
```python
|
|
635
|
+
record = table.create({
|
|
636
|
+
'Tags': ['Important', 'Urgent', 'Client Work']
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
# Access multiple select
|
|
640
|
+
tags = record['fields']['Tags']
|
|
641
|
+
print(f"Tags: {', '.join(tags)}")
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
#### Attachment Fields
|
|
645
|
+
|
|
646
|
+
```python
|
|
647
|
+
record = table.create({
|
|
648
|
+
'Attachments': [
|
|
649
|
+
{'url': 'https://example.com/image.jpg'},
|
|
650
|
+
{'url': 'https://example.com/document.pdf'}
|
|
651
|
+
]
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
# Access attachments
|
|
655
|
+
attachments = record['fields']['Attachments']
|
|
656
|
+
for attachment in attachments:
|
|
657
|
+
print(f"File: {attachment['filename']}")
|
|
658
|
+
print(f"URL: {attachment['url']}")
|
|
659
|
+
print(f"Size: {attachment['size']}")
|
|
660
|
+
print(f"Type: {attachment['type']}")
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
#### Upload Attachments
|
|
664
|
+
|
|
665
|
+
```python
|
|
666
|
+
# Upload from file path
|
|
667
|
+
result = table.upload_attachment(
|
|
668
|
+
'recAdw9EjV90xbZ',
|
|
669
|
+
'Attachments',
|
|
670
|
+
'/tmp/example.jpg'
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
# Upload from bytes/content
|
|
674
|
+
with open('/tmp/photo.jpg', 'rb') as f:
|
|
675
|
+
content = f.read()
|
|
676
|
+
|
|
677
|
+
result = table.upload_attachment(
|
|
678
|
+
'recAdw9EjV90xbZ',
|
|
679
|
+
'Attachments',
|
|
680
|
+
'photo.jpg',
|
|
681
|
+
content=content,
|
|
682
|
+
content_type='image/jpeg'
|
|
683
|
+
)
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
#### Linked Record Fields
|
|
687
|
+
|
|
688
|
+
```python
|
|
689
|
+
# Link to existing records by their IDs
|
|
690
|
+
record = table.create({
|
|
691
|
+
'Name': 'Task with Links',
|
|
692
|
+
'Related Tasks': [
|
|
693
|
+
'recXXXXXXXXXXXXXX',
|
|
694
|
+
'recYYYYYYYYYYYYYY'
|
|
695
|
+
]
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
# Access linked records
|
|
699
|
+
linked_records = record['fields']['Related Tasks']
|
|
700
|
+
print(f"Linked record IDs: {linked_records}")
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
#### Collaborator Fields
|
|
704
|
+
|
|
705
|
+
```python
|
|
706
|
+
record = table.create({
|
|
707
|
+
'Assignee': {
|
|
708
|
+
'id': 'usrXXXXXXXXXXXXXX',
|
|
709
|
+
'email': 'user@example.com'
|
|
710
|
+
},
|
|
711
|
+
'Collaborators': [
|
|
712
|
+
{'id': 'usrXXXXXXXXXXXXXX'},
|
|
713
|
+
{'id': 'usrYYYYYYYYYYYYYY'}
|
|
714
|
+
]
|
|
715
|
+
})
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Working with Comments
|
|
719
|
+
|
|
720
|
+
```python
|
|
721
|
+
# Get all comments on a record
|
|
722
|
+
comments = table.comments('recMNxslc6jG0XedV')
|
|
723
|
+
|
|
724
|
+
for comment in comments:
|
|
725
|
+
print(f"Author: {comment['author']['email']}")
|
|
726
|
+
print(f"Text: {comment['text']}")
|
|
727
|
+
print(f"Created: {comment['createdTime']}")
|
|
728
|
+
|
|
729
|
+
# Add comment
|
|
730
|
+
comment = table.add_comment(
|
|
731
|
+
'recMNxslc6jG0XedV',
|
|
732
|
+
'This is a comment'
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
# Add comment with user mention
|
|
736
|
+
comment = table.add_comment(
|
|
737
|
+
'recMNxslc6jG0XedV',
|
|
738
|
+
'Hello, @[usrVMNxslc6jG0Xed]! Please review this.'
|
|
739
|
+
)
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
## ORM (Object-Relational Mapping)
|
|
743
|
+
|
|
744
|
+
pyairtable provides ORM-style classes for type-safe database operations.
|
|
745
|
+
|
|
746
|
+
### Basic ORM Usage
|
|
747
|
+
|
|
748
|
+
```python
|
|
749
|
+
from pyairtable.orm import Model, fields
|
|
750
|
+
from pyairtable import Api
|
|
751
|
+
|
|
752
|
+
api = Api(os.environ['AIRTABLE_API_KEY'])
|
|
753
|
+
|
|
754
|
+
# Define model
|
|
755
|
+
class Task(Model):
|
|
756
|
+
name = fields.TextField('Name')
|
|
757
|
+
status = fields.SelectField('Status')
|
|
758
|
+
priority = fields.SelectField('Priority')
|
|
759
|
+
due_date = fields.DateField('Due Date')
|
|
760
|
+
completed = fields.CheckboxField('Completed')
|
|
761
|
+
tags = fields.MultipleSelectField('Tags')
|
|
762
|
+
|
|
763
|
+
class Meta:
|
|
764
|
+
base_id = 'appYourBaseId'
|
|
765
|
+
table_name = 'Tasks'
|
|
766
|
+
api_key = os.environ['AIRTABLE_API_KEY']
|
|
767
|
+
|
|
768
|
+
# Create record
|
|
769
|
+
task = Task(
|
|
770
|
+
name='New Task',
|
|
771
|
+
status='To Do',
|
|
772
|
+
priority='High',
|
|
773
|
+
due_date='2025-12-31'
|
|
774
|
+
)
|
|
775
|
+
task.save()
|
|
776
|
+
|
|
777
|
+
print(f"Created task: {task.id}")
|
|
778
|
+
|
|
779
|
+
# Retrieve all records
|
|
780
|
+
all_tasks = Task.all()
|
|
781
|
+
|
|
782
|
+
for task in all_tasks:
|
|
783
|
+
print(f"{task.name}: {task.status}")
|
|
784
|
+
|
|
785
|
+
# Find by ID
|
|
786
|
+
task = Task.from_id('recXXXXXXXXXXXXXX')
|
|
787
|
+
|
|
788
|
+
# Update record
|
|
789
|
+
task.status = 'Done'
|
|
790
|
+
task.completed = True
|
|
791
|
+
task.save()
|
|
792
|
+
|
|
793
|
+
# Delete record
|
|
794
|
+
task.delete()
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### ORM with Relationships
|
|
798
|
+
|
|
799
|
+
```python
|
|
800
|
+
from pyairtable.orm import Model, fields
|
|
801
|
+
|
|
802
|
+
class Project(Model):
|
|
803
|
+
name = fields.TextField('Name')
|
|
804
|
+
description = fields.TextField('Description')
|
|
805
|
+
tasks = fields.LinkField('Tasks', 'Task', lazy=True)
|
|
806
|
+
|
|
807
|
+
class Meta:
|
|
808
|
+
base_id = 'appYourBaseId'
|
|
809
|
+
table_name = 'Projects'
|
|
810
|
+
api_key = os.environ['AIRTABLE_API_KEY']
|
|
811
|
+
|
|
812
|
+
class Task(Model):
|
|
813
|
+
name = fields.TextField('Name')
|
|
814
|
+
status = fields.SelectField('Status')
|
|
815
|
+
project = fields.LinkField('Project', 'Project', lazy=True)
|
|
816
|
+
|
|
817
|
+
class Meta:
|
|
818
|
+
base_id = 'appYourBaseId'
|
|
819
|
+
table_name = 'Tasks'
|
|
820
|
+
api_key = os.environ['AIRTABLE_API_KEY']
|
|
821
|
+
|
|
822
|
+
# Create linked records
|
|
823
|
+
project = Project(name='New Project', description='Project description')
|
|
824
|
+
project.save()
|
|
825
|
+
|
|
826
|
+
task = Task(name='Task 1', status='To Do')
|
|
827
|
+
task.project = [project]
|
|
828
|
+
task.save()
|
|
829
|
+
|
|
830
|
+
# Access linked records
|
|
831
|
+
for task in project.tasks:
|
|
832
|
+
print(f"Task: {task.name}")
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
## Schema Management
|
|
836
|
+
|
|
837
|
+
### Get Base Schema
|
|
838
|
+
|
|
839
|
+
```python
|
|
840
|
+
from pyairtable import Api
|
|
841
|
+
|
|
842
|
+
api = Api(os.environ['AIRTABLE_API_KEY'])
|
|
843
|
+
base = api.base('appYourBaseId')
|
|
844
|
+
|
|
845
|
+
# Get complete base schema
|
|
846
|
+
schema = base.schema()
|
|
847
|
+
|
|
848
|
+
# List all tables
|
|
849
|
+
for table_schema in schema.tables:
|
|
850
|
+
print(f"Table: {table_schema.name}")
|
|
851
|
+
print(f"ID: {table_schema.id}")
|
|
852
|
+
|
|
853
|
+
# Get specific table schema
|
|
854
|
+
table_schema = schema.table('tblXXXXXXXXXXXXXX')
|
|
855
|
+
print(f"Table name: {table_schema.name}")
|
|
856
|
+
|
|
857
|
+
# List fields
|
|
858
|
+
for field in table_schema.fields:
|
|
859
|
+
print(f"Field: {field.name} ({field.type})")
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
### Get Table Schema
|
|
863
|
+
|
|
864
|
+
```python
|
|
865
|
+
table = api.table('appYourBaseId', 'Tasks')
|
|
866
|
+
|
|
867
|
+
# Get table schema
|
|
868
|
+
schema = table.schema()
|
|
869
|
+
|
|
870
|
+
print(f"Table name: {schema.name}")
|
|
871
|
+
print(f"Table ID: {schema.id}")
|
|
872
|
+
print(f"Primary field: {schema.primary_field_id}")
|
|
873
|
+
|
|
874
|
+
# List all fields
|
|
875
|
+
for field in schema.fields:
|
|
876
|
+
print(f"Field: {field.name}")
|
|
877
|
+
print(f"Type: {field.type}")
|
|
878
|
+
print(f"ID: {field.id}")
|
|
879
|
+
|
|
880
|
+
# Access field options
|
|
881
|
+
if hasattr(field, 'options'):
|
|
882
|
+
print(f"Options: {field.options}")
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
### Create Table
|
|
886
|
+
|
|
887
|
+
```python
|
|
888
|
+
base = api.base('appYourBaseId')
|
|
889
|
+
|
|
890
|
+
# Create new table with fields
|
|
891
|
+
new_table = base.create_table(
|
|
892
|
+
'Employees',
|
|
893
|
+
fields=[
|
|
894
|
+
{
|
|
895
|
+
'name': 'Name',
|
|
896
|
+
'type': 'singleLineText'
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
'name': 'Email',
|
|
900
|
+
'type': 'email'
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
'name': 'Department',
|
|
904
|
+
'type': 'singleSelect',
|
|
905
|
+
'options': {
|
|
906
|
+
'choices': [
|
|
907
|
+
{'name': 'Engineering'},
|
|
908
|
+
{'name': 'Sales'},
|
|
909
|
+
{'name': 'Marketing'}
|
|
910
|
+
]
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
'name': 'Start Date',
|
|
915
|
+
'type': 'date'
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
'name': 'Active',
|
|
919
|
+
'type': 'checkbox'
|
|
920
|
+
}
|
|
921
|
+
]
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
print(f"Created table: {new_table.id}")
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
### Create Field
|
|
928
|
+
|
|
929
|
+
```python
|
|
930
|
+
table = api.table('appYourBaseId', 'Tasks')
|
|
931
|
+
|
|
932
|
+
# Create single line text field
|
|
933
|
+
field = table.create_field('Description', 'singleLineText')
|
|
934
|
+
|
|
935
|
+
# Create single select field with options
|
|
936
|
+
field = table.create_field(
|
|
937
|
+
'Status',
|
|
938
|
+
'singleSelect',
|
|
939
|
+
options={
|
|
940
|
+
'choices': [
|
|
941
|
+
{'name': 'To Do'},
|
|
942
|
+
{'name': 'In Progress'},
|
|
943
|
+
{'name': 'Done'}
|
|
944
|
+
]
|
|
945
|
+
}
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
# Create multiple select field
|
|
949
|
+
field = table.create_field(
|
|
950
|
+
'Tags',
|
|
951
|
+
'multipleSelects',
|
|
952
|
+
options={
|
|
953
|
+
'choices': [
|
|
954
|
+
{'name': 'Important'},
|
|
955
|
+
{'name': 'Urgent'},
|
|
956
|
+
{'name': 'Low Priority'}
|
|
957
|
+
]
|
|
958
|
+
}
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
# Create number field
|
|
962
|
+
field = table.create_field(
|
|
963
|
+
'Progress',
|
|
964
|
+
'number',
|
|
965
|
+
options={'precision': 0}
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
# Create date field
|
|
969
|
+
field = table.create_field('Due Date', 'date')
|
|
970
|
+
|
|
971
|
+
# Create checkbox field
|
|
972
|
+
field = table.create_field('Completed', 'checkbox')
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
## Workspace and Base Management
|
|
976
|
+
|
|
977
|
+
### List All Bases
|
|
978
|
+
|
|
979
|
+
```python
|
|
980
|
+
api = Api(os.environ['AIRTABLE_API_KEY'])
|
|
981
|
+
|
|
982
|
+
# List all accessible bases
|
|
983
|
+
bases = api.bases()
|
|
984
|
+
|
|
985
|
+
for base in bases:
|
|
986
|
+
print(f"Base: {base.name}")
|
|
987
|
+
print(f"ID: {base.id}")
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
### Create Base
|
|
991
|
+
|
|
992
|
+
```python
|
|
993
|
+
# Create new base in workspace
|
|
994
|
+
new_base = api.create_base(
|
|
995
|
+
workspace_id='wspMhESAta6clCCwF',
|
|
996
|
+
name='My New Project Base',
|
|
997
|
+
tables=[
|
|
998
|
+
{
|
|
999
|
+
'name': 'Tasks',
|
|
1000
|
+
'fields': [
|
|
1001
|
+
{'name': 'Name', 'type': 'singleLineText'},
|
|
1002
|
+
{'name': 'Status', 'type': 'singleSelect', 'options': {
|
|
1003
|
+
'choices': [{'name': 'To Do'}, {'name': 'Done'}]
|
|
1004
|
+
}}
|
|
1005
|
+
]
|
|
1006
|
+
}
|
|
1007
|
+
]
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
print(f"Created base: {new_base.id}")
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
### Workspace Operations
|
|
1014
|
+
|
|
1015
|
+
```python
|
|
1016
|
+
workspace = api.workspace('wspmhESAta6clCCwF')
|
|
1017
|
+
|
|
1018
|
+
# Create base in workspace
|
|
1019
|
+
new_base = workspace.create_base(
|
|
1020
|
+
'New Project',
|
|
1021
|
+
tables=[
|
|
1022
|
+
{
|
|
1023
|
+
'name': 'Table 1',
|
|
1024
|
+
'fields': [{'name': 'Name', 'type': 'singleLineText'}]
|
|
1025
|
+
}
|
|
1026
|
+
]
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
# Get workspace collaborators
|
|
1030
|
+
collaborators = workspace.collaborators()
|
|
1031
|
+
|
|
1032
|
+
for collab in collaborators:
|
|
1033
|
+
print(f"User: {collab.user.email}")
|
|
1034
|
+
print(f"Permission: {collab.permission_level}")
|
|
1035
|
+
|
|
1036
|
+
# Move base to different workspace
|
|
1037
|
+
workspace.move_base('appCwFmhESAta6clC', 'wspTargetWorkspace')
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
## Webhooks
|
|
1041
|
+
|
|
1042
|
+
### List Webhooks
|
|
1043
|
+
|
|
1044
|
+
```python
|
|
1045
|
+
base = api.base('appYourBaseId')
|
|
1046
|
+
|
|
1047
|
+
# Get all webhooks
|
|
1048
|
+
webhooks = base.webhooks()
|
|
1049
|
+
|
|
1050
|
+
for webhook in webhooks:
|
|
1051
|
+
print(f"Webhook ID: {webhook.id}")
|
|
1052
|
+
print(f"URL: {webhook.notification_url}")
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
### Get Webhook
|
|
1056
|
+
|
|
1057
|
+
```python
|
|
1058
|
+
# Get specific webhook
|
|
1059
|
+
webhook = base.webhook('ach00000000000001')
|
|
1060
|
+
|
|
1061
|
+
print(f"Webhook URL: {webhook.notification_url}")
|
|
1062
|
+
print(f"Cursor: {webhook.cursor}")
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
### Create Webhook
|
|
1066
|
+
|
|
1067
|
+
```python
|
|
1068
|
+
# Create webhook
|
|
1069
|
+
webhook = base.add_webhook(
|
|
1070
|
+
'https://example.com/webhook',
|
|
1071
|
+
{
|
|
1072
|
+
'options': {
|
|
1073
|
+
'filters': {
|
|
1074
|
+
'dataTypes': ['tableData']
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
print(f"Created webhook: {webhook.id}")
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
## Enterprise Features
|
|
1084
|
+
|
|
1085
|
+
### Enterprise API Access
|
|
1086
|
+
|
|
1087
|
+
```python
|
|
1088
|
+
# Access enterprise features
|
|
1089
|
+
enterprise = api.enterprise('entUBq2RGdihxl3vU')
|
|
1090
|
+
|
|
1091
|
+
# Get enterprise info
|
|
1092
|
+
info = enterprise.info()
|
|
1093
|
+
print(f"Enterprise: {info.workspace_ids}")
|
|
1094
|
+
|
|
1095
|
+
# Iterate through audit log
|
|
1096
|
+
for page in enterprise.audit_log(sort_asc=True, page_size=50):
|
|
1097
|
+
for event in page.events:
|
|
1098
|
+
print(f"Event: {event.action}")
|
|
1099
|
+
print(f"Actor: {event.actor.email}")
|
|
1100
|
+
print(f"Timestamp: {event.timestamp}")
|
|
1101
|
+
|
|
1102
|
+
# User management
|
|
1103
|
+
users = enterprise.users(['usrID1', 'email@example.com'])
|
|
1104
|
+
|
|
1105
|
+
for user in users:
|
|
1106
|
+
print(f"User: {user.email}")
|
|
1107
|
+
print(f"State: {user.state}")
|
|
1108
|
+
|
|
1109
|
+
# Grant admin access
|
|
1110
|
+
enterprise.grant_admin('usrID1', 'usrID2')
|
|
1111
|
+
|
|
1112
|
+
# Remove from enterprise
|
|
1113
|
+
enterprise.remove_user(['usrID1'])
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
## Complete Examples
|
|
1117
|
+
|
|
1118
|
+
### Example 1: Task Management System
|
|
1119
|
+
|
|
1120
|
+
```python
|
|
1121
|
+
import os
|
|
1122
|
+
from pyairtable import Api
|
|
1123
|
+
from datetime import datetime, timedelta
|
|
1124
|
+
|
|
1125
|
+
api = Api(os.environ['AIRTABLE_API_KEY'])
|
|
1126
|
+
table = api.table('appTaskManager', 'Tasks')
|
|
1127
|
+
|
|
1128
|
+
def create_task(name, description, priority='Medium', assignee=None, due_date=None):
|
|
1129
|
+
"""Create a new task"""
|
|
1130
|
+
task_data = {
|
|
1131
|
+
'Name': name,
|
|
1132
|
+
'Description': description,
|
|
1133
|
+
'Status': 'To Do',
|
|
1134
|
+
'Priority': priority,
|
|
1135
|
+
'Created': datetime.now().isoformat()
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if assignee:
|
|
1139
|
+
task_data['Assignee'] = assignee
|
|
1140
|
+
|
|
1141
|
+
if due_date:
|
|
1142
|
+
task_data['Due Date'] = due_date
|
|
1143
|
+
|
|
1144
|
+
record = table.create(task_data)
|
|
1145
|
+
print(f"Created task: {record['id']}")
|
|
1146
|
+
return record
|
|
1147
|
+
|
|
1148
|
+
def get_active_tasks():
|
|
1149
|
+
"""Get all tasks that are not done or cancelled"""
|
|
1150
|
+
records = table.all(
|
|
1151
|
+
formula="AND({Status} != 'Done', {Status} != 'Cancelled')",
|
|
1152
|
+
sort=['-Priority', 'Due Date']
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
tasks = []
|
|
1156
|
+
for record in records:
|
|
1157
|
+
tasks.append({
|
|
1158
|
+
'id': record['id'],
|
|
1159
|
+
'name': record['fields']['Name'],
|
|
1160
|
+
'status': record['fields']['Status'],
|
|
1161
|
+
'priority': record['fields'].get('Priority', 'Medium'),
|
|
1162
|
+
'assignee': record['fields'].get('Assignee'),
|
|
1163
|
+
'due_date': record['fields'].get('Due Date')
|
|
1164
|
+
})
|
|
1165
|
+
|
|
1166
|
+
return tasks
|
|
1167
|
+
|
|
1168
|
+
def update_task_status(task_id, new_status):
|
|
1169
|
+
"""Update task status"""
|
|
1170
|
+
updated = table.update(task_id, {
|
|
1171
|
+
'Status': new_status,
|
|
1172
|
+
'Last Modified': datetime.now().isoformat()
|
|
1173
|
+
})
|
|
1174
|
+
|
|
1175
|
+
if new_status == 'Done':
|
|
1176
|
+
table.update(task_id, {
|
|
1177
|
+
'Completed': True,
|
|
1178
|
+
'Completed Date': datetime.now().isoformat()
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
print(f"Updated task {task_id} to {new_status}")
|
|
1182
|
+
return updated
|
|
1183
|
+
|
|
1184
|
+
def get_overdue_tasks():
|
|
1185
|
+
"""Get all overdue tasks"""
|
|
1186
|
+
today = datetime.now().date().isoformat()
|
|
1187
|
+
|
|
1188
|
+
records = table.all(
|
|
1189
|
+
formula=f"AND({{Status}} != 'Done', {{Due Date}} < '{today}')",
|
|
1190
|
+
sort=['Due Date']
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
return records
|
|
1194
|
+
|
|
1195
|
+
def bulk_update_status(task_ids, new_status):
|
|
1196
|
+
"""Update multiple tasks at once"""
|
|
1197
|
+
updates = [
|
|
1198
|
+
{
|
|
1199
|
+
'id': task_id,
|
|
1200
|
+
'fields': {
|
|
1201
|
+
'Status': new_status,
|
|
1202
|
+
'Last Modified': datetime.now().isoformat()
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
for task_id in task_ids
|
|
1206
|
+
]
|
|
1207
|
+
|
|
1208
|
+
# Process in batches of 10
|
|
1209
|
+
all_updated = []
|
|
1210
|
+
for i in range(0, len(updates), 10):
|
|
1211
|
+
batch = updates[i:i+10]
|
|
1212
|
+
updated = table.batch_update(batch)
|
|
1213
|
+
all_updated.extend(updated)
|
|
1214
|
+
|
|
1215
|
+
print(f"Updated {len(all_updated)} tasks")
|
|
1216
|
+
return all_updated
|
|
1217
|
+
|
|
1218
|
+
def delete_old_completed_tasks(days=30):
|
|
1219
|
+
"""Delete completed tasks older than specified days"""
|
|
1220
|
+
cutoff_date = (datetime.now() - timedelta(days=days)).date().isoformat()
|
|
1221
|
+
|
|
1222
|
+
records = table.all(
|
|
1223
|
+
formula=f"AND({{Status}} = 'Done', {{Completed Date}} < '{cutoff_date}')"
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
record_ids = [record['id'] for record in records]
|
|
1227
|
+
|
|
1228
|
+
# Delete in batches of 10
|
|
1229
|
+
for i in range(0, len(record_ids), 10):
|
|
1230
|
+
batch = record_ids[i:i+10]
|
|
1231
|
+
table.batch_delete(batch)
|
|
1232
|
+
|
|
1233
|
+
print(f"Deleted {len(record_ids)} old completed tasks")
|
|
1234
|
+
|
|
1235
|
+
def get_tasks_by_assignee(assignee_email):
|
|
1236
|
+
"""Get all tasks for a specific assignee"""
|
|
1237
|
+
records = table.all(
|
|
1238
|
+
formula=f"{{Assignee}} = '{assignee_email}'",
|
|
1239
|
+
sort=['-Priority', 'Due Date']
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
return records
|
|
1243
|
+
|
|
1244
|
+
# Usage examples
|
|
1245
|
+
if __name__ == '__main__':
|
|
1246
|
+
# Create new task
|
|
1247
|
+
task = create_task(
|
|
1248
|
+
name='Complete project documentation',
|
|
1249
|
+
description='Write comprehensive docs for the project',
|
|
1250
|
+
priority='High',
|
|
1251
|
+
due_date='2025-11-01'
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
# Get active tasks
|
|
1255
|
+
active = get_active_tasks()
|
|
1256
|
+
print(f"Found {len(active)} active tasks")
|
|
1257
|
+
|
|
1258
|
+
# Update task status
|
|
1259
|
+
# update_task_status(task['id'], 'In Progress')
|
|
1260
|
+
|
|
1261
|
+
# Get overdue tasks
|
|
1262
|
+
overdue = get_overdue_tasks()
|
|
1263
|
+
print(f"Found {len(overdue)} overdue tasks")
|
|
1264
|
+
|
|
1265
|
+
# Bulk update
|
|
1266
|
+
# task_ids = ['rec1', 'rec2', 'rec3']
|
|
1267
|
+
# bulk_update_status(task_ids, 'Done')
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
### Example 2: Contact Management with Upsert
|
|
1271
|
+
|
|
1272
|
+
```python
|
|
1273
|
+
import os
|
|
1274
|
+
from pyairtable import Api
|
|
1275
|
+
|
|
1276
|
+
api = Api(os.environ['AIRTABLE_API_KEY'])
|
|
1277
|
+
table = api.table('appContactManager', 'Contacts')
|
|
1278
|
+
|
|
1279
|
+
def find_contact_by_email(email):
|
|
1280
|
+
"""Find contact by email address"""
|
|
1281
|
+
record = table.first(formula=f"{{Email}} = '{email}'")
|
|
1282
|
+
return record
|
|
1283
|
+
|
|
1284
|
+
def upsert_contact(contact_data):
|
|
1285
|
+
"""Create or update contact based on email"""
|
|
1286
|
+
result = table.batch_upsert(
|
|
1287
|
+
[contact_data],
|
|
1288
|
+
key_fields=['Email']
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
if result['createdRecords']:
|
|
1292
|
+
print(f"Created new contact: {result['createdRecords'][0]['id']}")
|
|
1293
|
+
return {'action': 'created', 'record': result['createdRecords'][0]}
|
|
1294
|
+
else:
|
|
1295
|
+
print(f"Updated existing contact: {result['updatedRecords'][0]['id']}")
|
|
1296
|
+
return {'action': 'updated', 'record': result['updatedRecords'][0]}
|
|
1297
|
+
|
|
1298
|
+
def get_contacts_by_company(company_name):
|
|
1299
|
+
"""Get all contacts from a specific company"""
|
|
1300
|
+
records = table.all(
|
|
1301
|
+
formula=f"{{Company}} = '{company_name}'",
|
|
1302
|
+
sort=['Last Name', 'First Name']
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
contacts = []
|
|
1306
|
+
for record in records:
|
|
1307
|
+
contacts.append({
|
|
1308
|
+
'id': record['id'],
|
|
1309
|
+
'name': f"{record['fields']['First Name']} {record['fields']['Last Name']}",
|
|
1310
|
+
'email': record['fields']['Email'],
|
|
1311
|
+
'phone': record['fields'].get('Phone'),
|
|
1312
|
+
'company': record['fields']['Company']
|
|
1313
|
+
})
|
|
1314
|
+
|
|
1315
|
+
return contacts
|
|
1316
|
+
|
|
1317
|
+
def batch_import_contacts(contacts_list):
|
|
1318
|
+
"""Import multiple contacts with upsert"""
|
|
1319
|
+
result = table.batch_upsert(
|
|
1320
|
+
contacts_list,
|
|
1321
|
+
key_fields=['Email']
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
print(f"Created: {len(result['createdRecords'])}")
|
|
1325
|
+
print(f"Updated: {len(result['updatedRecords'])}")
|
|
1326
|
+
|
|
1327
|
+
return result
|
|
1328
|
+
|
|
1329
|
+
def export_all_contacts():
|
|
1330
|
+
"""Export all contacts to list"""
|
|
1331
|
+
all_contacts = []
|
|
1332
|
+
|
|
1333
|
+
for page in table.iterate(page_size=100):
|
|
1334
|
+
for record in page:
|
|
1335
|
+
all_contacts.append({
|
|
1336
|
+
'id': record['id'],
|
|
1337
|
+
'first_name': record['fields'].get('First Name'),
|
|
1338
|
+
'last_name': record['fields'].get('Last Name'),
|
|
1339
|
+
'email': record['fields'].get('Email'),
|
|
1340
|
+
'phone': record['fields'].get('Phone'),
|
|
1341
|
+
'company': record['fields'].get('Company'),
|
|
1342
|
+
'created': record['createdTime']
|
|
1343
|
+
})
|
|
1344
|
+
|
|
1345
|
+
return all_contacts
|
|
1346
|
+
|
|
1347
|
+
def tag_contacts(contact_ids, tags):
|
|
1348
|
+
"""Add tags to multiple contacts"""
|
|
1349
|
+
updates = [
|
|
1350
|
+
{
|
|
1351
|
+
'id': contact_id,
|
|
1352
|
+
'fields': {'Tags': tags}
|
|
1353
|
+
}
|
|
1354
|
+
for contact_id in contact_ids
|
|
1355
|
+
]
|
|
1356
|
+
|
|
1357
|
+
# Process in batches of 10
|
|
1358
|
+
all_updated = []
|
|
1359
|
+
for i in range(0, len(updates), 10):
|
|
1360
|
+
batch = updates[i:i+10]
|
|
1361
|
+
updated = table.batch_update(batch)
|
|
1362
|
+
all_updated.extend(updated)
|
|
1363
|
+
|
|
1364
|
+
return all_updated
|
|
1365
|
+
|
|
1366
|
+
# Usage examples
|
|
1367
|
+
if __name__ == '__main__':
|
|
1368
|
+
# Upsert single contact
|
|
1369
|
+
result = upsert_contact({
|
|
1370
|
+
'First Name': 'John',
|
|
1371
|
+
'Last Name': 'Doe',
|
|
1372
|
+
'Email': 'john.doe@example.com',
|
|
1373
|
+
'Phone': '+1-555-0100',
|
|
1374
|
+
'Company': 'Acme Inc'
|
|
1375
|
+
})
|
|
1376
|
+
|
|
1377
|
+
# Find contact by email
|
|
1378
|
+
contact = find_contact_by_email('john.doe@example.com')
|
|
1379
|
+
if contact:
|
|
1380
|
+
print(f"Found: {contact['fields']['First Name']} {contact['fields']['Last Name']}")
|
|
1381
|
+
|
|
1382
|
+
# Get contacts by company
|
|
1383
|
+
acme_contacts = get_contacts_by_company('Acme Inc')
|
|
1384
|
+
print(f"Found {len(acme_contacts)} contacts at Acme Inc")
|
|
1385
|
+
|
|
1386
|
+
# Batch import
|
|
1387
|
+
contacts_to_import = [
|
|
1388
|
+
{
|
|
1389
|
+
'First Name': 'Jane',
|
|
1390
|
+
'Last Name': 'Smith',
|
|
1391
|
+
'Email': 'jane@example.com',
|
|
1392
|
+
'Company': 'Tech Corp'
|
|
1393
|
+
},
|
|
1394
|
+
{
|
|
1395
|
+
'First Name': 'Bob',
|
|
1396
|
+
'Last Name': 'Johnson',
|
|
1397
|
+
'Email': 'bob@example.com',
|
|
1398
|
+
'Company': 'Startup LLC'
|
|
1399
|
+
}
|
|
1400
|
+
]
|
|
1401
|
+
batch_import_contacts(contacts_to_import)
|
|
1402
|
+
|
|
1403
|
+
# Export all contacts
|
|
1404
|
+
all_contacts = export_all_contacts()
|
|
1405
|
+
print(f"Exported {len(all_contacts)} contacts")
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
### Example 3: E-commerce Inventory Management
|
|
1409
|
+
|
|
1410
|
+
```python
|
|
1411
|
+
import os
|
|
1412
|
+
from pyairtable import Api
|
|
1413
|
+
from datetime import datetime
|
|
1414
|
+
|
|
1415
|
+
api = Api(os.environ['AIRTABLE_API_KEY'])
|
|
1416
|
+
products_table = api.table('appInventory', 'Products')
|
|
1417
|
+
orders_table = api.table('appInventory', 'Orders')
|
|
1418
|
+
|
|
1419
|
+
def check_inventory(product_id):
|
|
1420
|
+
"""Check product availability"""
|
|
1421
|
+
product = products_table.get(product_id)
|
|
1422
|
+
|
|
1423
|
+
return {
|
|
1424
|
+
'id': product['id'],
|
|
1425
|
+
'name': product['fields']['Name'],
|
|
1426
|
+
'sku': product['fields']['SKU'],
|
|
1427
|
+
'quantity': product['fields']['Quantity in Stock'],
|
|
1428
|
+
'available': product['fields']['Quantity in Stock'] > 0,
|
|
1429
|
+
'price': product['fields']['Price']
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
def update_stock_quantity(product_id, quantity_change):
|
|
1433
|
+
"""Update stock after purchase or restock"""
|
|
1434
|
+
product = products_table.get(product_id)
|
|
1435
|
+
current_stock = product['fields']['Quantity in Stock']
|
|
1436
|
+
new_stock = current_stock + quantity_change
|
|
1437
|
+
|
|
1438
|
+
if new_stock < 0:
|
|
1439
|
+
raise ValueError(f"Insufficient stock. Available: {current_stock}")
|
|
1440
|
+
|
|
1441
|
+
updated = products_table.update(product_id, {
|
|
1442
|
+
'Quantity in Stock': new_stock,
|
|
1443
|
+
'Last Updated': datetime.now().isoformat()
|
|
1444
|
+
})
|
|
1445
|
+
|
|
1446
|
+
print(f"Updated stock for {product['fields']['Name']}: {current_stock} -> {new_stock}")
|
|
1447
|
+
return updated
|
|
1448
|
+
|
|
1449
|
+
def get_low_stock_products(threshold=10):
|
|
1450
|
+
"""Get products below stock threshold"""
|
|
1451
|
+
records = products_table.all(
|
|
1452
|
+
formula=f"{{Quantity in Stock}} < {threshold}",
|
|
1453
|
+
sort=['Quantity in Stock']
|
|
1454
|
+
)
|
|
1455
|
+
|
|
1456
|
+
low_stock = []
|
|
1457
|
+
for record in records:
|
|
1458
|
+
low_stock.append({
|
|
1459
|
+
'id': record['id'],
|
|
1460
|
+
'name': record['fields']['Name'],
|
|
1461
|
+
'sku': record['fields']['SKU'],
|
|
1462
|
+
'quantity': record['fields']['Quantity in Stock'],
|
|
1463
|
+
'reorder_level': record['fields'].get('Reorder Level', threshold)
|
|
1464
|
+
})
|
|
1465
|
+
|
|
1466
|
+
return low_stock
|
|
1467
|
+
|
|
1468
|
+
def create_order(order_data):
|
|
1469
|
+
"""Create order and update inventory"""
|
|
1470
|
+
# Create order record
|
|
1471
|
+
order = orders_table.create({
|
|
1472
|
+
'Order Number': order_data['order_number'],
|
|
1473
|
+
'Customer Name': order_data['customer_name'],
|
|
1474
|
+
'Customer Email': order_data['customer_email'],
|
|
1475
|
+
'Status': 'Pending',
|
|
1476
|
+
'Total Amount': order_data['total_amount'],
|
|
1477
|
+
'Order Date': datetime.now().isoformat()
|
|
1478
|
+
})
|
|
1479
|
+
|
|
1480
|
+
# Update inventory for each product
|
|
1481
|
+
try:
|
|
1482
|
+
for item in order_data['items']:
|
|
1483
|
+
update_stock_quantity(item['product_id'], -item['quantity'])
|
|
1484
|
+
|
|
1485
|
+
# Update order status to confirmed
|
|
1486
|
+
orders_table.update(order['id'], {'Status': 'Confirmed'})
|
|
1487
|
+
|
|
1488
|
+
print(f"Created order: {order['id']}")
|
|
1489
|
+
return order
|
|
1490
|
+
except ValueError as e:
|
|
1491
|
+
# Rollback: delete order if inventory update fails
|
|
1492
|
+
orders_table.delete(order['id'])
|
|
1493
|
+
raise e
|
|
1494
|
+
|
|
1495
|
+
def get_orders_by_status(status):
|
|
1496
|
+
"""Get all orders with specific status"""
|
|
1497
|
+
records = orders_table.all(
|
|
1498
|
+
formula=f"{{Status}} = '{status}'",
|
|
1499
|
+
sort=['-Order Date']
|
|
1500
|
+
)
|
|
1501
|
+
|
|
1502
|
+
orders = []
|
|
1503
|
+
for record in records:
|
|
1504
|
+
orders.append({
|
|
1505
|
+
'id': record['id'],
|
|
1506
|
+
'order_number': record['fields']['Order Number'],
|
|
1507
|
+
'customer': record['fields']['Customer Name'],
|
|
1508
|
+
'total': record['fields']['Total Amount'],
|
|
1509
|
+
'date': record['fields']['Order Date']
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
return orders
|
|
1513
|
+
|
|
1514
|
+
def restock_products(restock_list):
|
|
1515
|
+
"""Bulk restock multiple products"""
|
|
1516
|
+
updates = []
|
|
1517
|
+
|
|
1518
|
+
for item in restock_list:
|
|
1519
|
+
product = products_table.get(item['product_id'])
|
|
1520
|
+
current_stock = product['fields']['Quantity in Stock']
|
|
1521
|
+
new_stock = current_stock + item['quantity']
|
|
1522
|
+
|
|
1523
|
+
updates.append({
|
|
1524
|
+
'id': item['product_id'],
|
|
1525
|
+
'fields': {
|
|
1526
|
+
'Quantity in Stock': new_stock,
|
|
1527
|
+
'Last Restocked': datetime.now().isoformat()
|
|
1528
|
+
}
|
|
1529
|
+
})
|
|
1530
|
+
|
|
1531
|
+
# Process in batches of 10
|
|
1532
|
+
all_updated = []
|
|
1533
|
+
for i in range(0, len(updates), 10):
|
|
1534
|
+
batch = updates[i:i+10]
|
|
1535
|
+
updated = products_table.batch_update(batch)
|
|
1536
|
+
all_updated.extend(updated)
|
|
1537
|
+
|
|
1538
|
+
print(f"Restocked {len(all_updated)} products")
|
|
1539
|
+
return all_updated
|
|
1540
|
+
|
|
1541
|
+
def get_sales_report(start_date, end_date):
|
|
1542
|
+
"""Get orders within date range"""
|
|
1543
|
+
records = orders_table.all(
|
|
1544
|
+
formula=f"AND({{Order Date}} >= '{start_date}', {{Order Date}} <= '{end_date}')",
|
|
1545
|
+
sort=['Order Date']
|
|
1546
|
+
)
|
|
1547
|
+
|
|
1548
|
+
total_sales = sum(
|
|
1549
|
+
record['fields']['Total Amount']
|
|
1550
|
+
for record in records
|
|
1551
|
+
if 'Total Amount' in record['fields']
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
return {
|
|
1555
|
+
'total_orders': len(records),
|
|
1556
|
+
'total_sales': total_sales,
|
|
1557
|
+
'orders': records
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
# Usage examples
|
|
1561
|
+
if __name__ == '__main__':
|
|
1562
|
+
# Check inventory
|
|
1563
|
+
inventory = check_inventory('recProductId123')
|
|
1564
|
+
print(f"{inventory['name']}: {inventory['quantity']} in stock")
|
|
1565
|
+
|
|
1566
|
+
# Get low stock products
|
|
1567
|
+
low_stock = get_low_stock_products(threshold=15)
|
|
1568
|
+
print(f"Found {len(low_stock)} products below threshold")
|
|
1569
|
+
|
|
1570
|
+
# Create order
|
|
1571
|
+
order_data = {
|
|
1572
|
+
'order_number': 'ORD-2025-001',
|
|
1573
|
+
'customer_name': 'John Doe',
|
|
1574
|
+
'customer_email': 'john@example.com',
|
|
1575
|
+
'total_amount': 299.99,
|
|
1576
|
+
'items': [
|
|
1577
|
+
{'product_id': 'recProduct1', 'quantity': 2},
|
|
1578
|
+
{'product_id': 'recProduct2', 'quantity': 1}
|
|
1579
|
+
]
|
|
1580
|
+
}
|
|
1581
|
+
# create_order(order_data)
|
|
1582
|
+
|
|
1583
|
+
# Get pending orders
|
|
1584
|
+
pending = get_orders_by_status('Pending')
|
|
1585
|
+
print(f"Found {len(pending)} pending orders")
|
|
1586
|
+
|
|
1587
|
+
# Restock products
|
|
1588
|
+
restock_list = [
|
|
1589
|
+
{'product_id': 'recProduct1', 'quantity': 50},
|
|
1590
|
+
{'product_id': 'recProduct2', 'quantity': 30}
|
|
1591
|
+
]
|
|
1592
|
+
# restock_products(restock_list)
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
## Rate Limits and Error Handling
|
|
1596
|
+
|
|
1597
|
+
Airtable enforces a rate limit of **5 requests per second per base**.
|
|
1598
|
+
|
|
1599
|
+
### Automatic Retry with Rate Limiting
|
|
1600
|
+
|
|
1601
|
+
```python
|
|
1602
|
+
from pyairtable import Api, retry_strategy
|
|
1603
|
+
|
|
1604
|
+
# Enable automatic retry for rate limits
|
|
1605
|
+
api = Api(
|
|
1606
|
+
os.environ['AIRTABLE_API_KEY'],
|
|
1607
|
+
retry_strategy=True
|
|
1608
|
+
)
|
|
1609
|
+
|
|
1610
|
+
# Custom retry configuration
|
|
1611
|
+
custom_retry = retry_strategy(
|
|
1612
|
+
total=10,
|
|
1613
|
+
status_forcelist=(429, 500, 502, 503, 504),
|
|
1614
|
+
backoff_factor=0.2
|
|
1615
|
+
)
|
|
1616
|
+
|
|
1617
|
+
api = Api(
|
|
1618
|
+
os.environ['AIRTABLE_API_KEY'],
|
|
1619
|
+
retry_strategy=custom_retry
|
|
1620
|
+
)
|
|
1621
|
+
```
|
|
1622
|
+
|
|
1623
|
+
### Manual Error Handling
|
|
1624
|
+
|
|
1625
|
+
```python
|
|
1626
|
+
from pyairtable import Api
|
|
1627
|
+
from pyairtable.api.types import APIError
|
|
1628
|
+
import time
|
|
1629
|
+
|
|
1630
|
+
api = Api(os.environ['AIRTABLE_API_KEY'])
|
|
1631
|
+
table = api.table('appBaseId', 'Tasks')
|
|
1632
|
+
|
|
1633
|
+
# Basic error handling
|
|
1634
|
+
try:
|
|
1635
|
+
record = table.get('recXXXXXXXXXXXXXX')
|
|
1636
|
+
except APIError as e:
|
|
1637
|
+
if e.status_code == 404:
|
|
1638
|
+
print('Record not found')
|
|
1639
|
+
elif e.status_code == 401:
|
|
1640
|
+
print('Authentication failed')
|
|
1641
|
+
elif e.status_code == 429:
|
|
1642
|
+
print('Rate limit exceeded')
|
|
1643
|
+
else:
|
|
1644
|
+
print(f'API error: {e}')
|
|
1645
|
+
except Exception as e:
|
|
1646
|
+
print(f'Unexpected error: {e}')
|
|
1647
|
+
|
|
1648
|
+
# Retry with exponential backoff
|
|
1649
|
+
def create_with_retry(record_data, max_retries=3):
|
|
1650
|
+
for attempt in range(max_retries):
|
|
1651
|
+
try:
|
|
1652
|
+
return table.create(record_data)
|
|
1653
|
+
except APIError as e:
|
|
1654
|
+
if e.status_code == 429 and attempt < max_retries - 1:
|
|
1655
|
+
wait_time = 2 ** attempt
|
|
1656
|
+
print(f"Rate limited. Retrying in {wait_time}s...")
|
|
1657
|
+
time.sleep(wait_time)
|
|
1658
|
+
else:
|
|
1659
|
+
raise
|
|
1660
|
+
|
|
1661
|
+
# Comprehensive error handling
|
|
1662
|
+
def safe_create(record_data):
|
|
1663
|
+
try:
|
|
1664
|
+
record = table.create(record_data)
|
|
1665
|
+
return {'success': True, 'record': record}
|
|
1666
|
+
except APIError as e:
|
|
1667
|
+
return {
|
|
1668
|
+
'success': False,
|
|
1669
|
+
'error': {
|
|
1670
|
+
'message': str(e),
|
|
1671
|
+
'status_code': e.status_code,
|
|
1672
|
+
'type': e.type
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
except Exception as e:
|
|
1676
|
+
return {
|
|
1677
|
+
'success': False,
|
|
1678
|
+
'error': {
|
|
1679
|
+
'message': str(e),
|
|
1680
|
+
'type': 'unknown'
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
```
|
|
1684
|
+
|
|
1685
|
+
## Common Formulas Reference
|
|
1686
|
+
|
|
1687
|
+
```python
|
|
1688
|
+
# Exact match
|
|
1689
|
+
formula = "{Status} = 'Active'"
|
|
1690
|
+
|
|
1691
|
+
# Not equal
|
|
1692
|
+
formula = "{Status} != 'Done'"
|
|
1693
|
+
|
|
1694
|
+
# Greater than / Less than
|
|
1695
|
+
formula = "{Price} > 100"
|
|
1696
|
+
formula = "{Stock} <= 10"
|
|
1697
|
+
|
|
1698
|
+
# String contains (case-insensitive)
|
|
1699
|
+
formula = "SEARCH('urgent', LOWER({Notes})) > 0"
|
|
1700
|
+
|
|
1701
|
+
# Is empty
|
|
1702
|
+
formula = "{Email} = ''"
|
|
1703
|
+
formula = "OR({Email} = BLANK())"
|
|
1704
|
+
|
|
1705
|
+
# Is not empty
|
|
1706
|
+
formula = "NOT({Email} = '')"
|
|
1707
|
+
formula = "{Email} != ''"
|
|
1708
|
+
|
|
1709
|
+
# AND condition
|
|
1710
|
+
formula = "AND({Status} = 'Active', {Priority} = 'High')"
|
|
1711
|
+
|
|
1712
|
+
# OR condition
|
|
1713
|
+
formula = "OR({Status} = 'Urgent', {Priority} = 'High')"
|
|
1714
|
+
|
|
1715
|
+
# Date comparisons
|
|
1716
|
+
formula = "{Created} > '2025-01-01'"
|
|
1717
|
+
formula = "{Due Date} < TODAY()"
|
|
1718
|
+
formula = "{Modified} >= DATEADD(TODAY(), -7, 'days')"
|
|
1719
|
+
|
|
1720
|
+
# Multiple conditions
|
|
1721
|
+
formula = "AND({Status} = 'Active', OR({Priority} = 'High', {Due Date} < TODAY()))"
|
|
1722
|
+
|
|
1723
|
+
# Check if field is in a list
|
|
1724
|
+
formula = "OR({Status} = 'Active', {Status} = 'In Progress', {Status} = 'Review')"
|
|
1725
|
+
|
|
1726
|
+
# Numeric range
|
|
1727
|
+
formula = "AND({Price} >= 10, {Price} <= 100)"
|
|
1728
|
+
|
|
1729
|
+
# Using variables
|
|
1730
|
+
email = 'user@example.com'
|
|
1731
|
+
formula = f"{{Email}} = '{email}'"
|
|
1732
|
+
|
|
1733
|
+
search_term = 'urgent'
|
|
1734
|
+
formula = f"SEARCH(LOWER('{search_term}'), LOWER({{Notes}})) > 0"
|
|
1735
|
+
```
|