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.
Files changed (139) hide show
  1. package/README.md +55 -0
  2. package/bin/chub-mcp +2 -0
  3. package/dist/airtable/docs/database/javascript/DOC.md +1437 -0
  4. package/dist/airtable/docs/database/python/DOC.md +1735 -0
  5. package/dist/amplitude/docs/analytics/javascript/DOC.md +1282 -0
  6. package/dist/amplitude/docs/analytics/python/DOC.md +1199 -0
  7. package/dist/anthropic/docs/claude-api/javascript/DOC.md +503 -0
  8. package/dist/anthropic/docs/claude-api/python/DOC.md +389 -0
  9. package/dist/asana/docs/tasks/DOC.md +1396 -0
  10. package/dist/assemblyai/docs/transcription/DOC.md +1043 -0
  11. package/dist/atlassian/docs/confluence/javascript/DOC.md +1347 -0
  12. package/dist/atlassian/docs/confluence/python/DOC.md +1604 -0
  13. package/dist/auth0/docs/identity/javascript/DOC.md +968 -0
  14. package/dist/auth0/docs/identity/python/DOC.md +1199 -0
  15. package/dist/aws/docs/s3/javascript/DOC.md +1773 -0
  16. package/dist/aws/docs/s3/python/DOC.md +1807 -0
  17. package/dist/binance/docs/trading/javascript/DOC.md +1315 -0
  18. package/dist/binance/docs/trading/python/DOC.md +1454 -0
  19. package/dist/braintree/docs/gateway/javascript/DOC.md +1278 -0
  20. package/dist/braintree/docs/gateway/python/DOC.md +1179 -0
  21. package/dist/chromadb/docs/embeddings-db/javascript/DOC.md +1263 -0
  22. package/dist/chromadb/docs/embeddings-db/python/DOC.md +1707 -0
  23. package/dist/clerk/docs/auth/javascript/DOC.md +1220 -0
  24. package/dist/clerk/docs/auth/python/DOC.md +274 -0
  25. package/dist/cloudflare/docs/workers/javascript/DOC.md +918 -0
  26. package/dist/cloudflare/docs/workers/python/DOC.md +994 -0
  27. package/dist/cockroachdb/docs/distributed-db/DOC.md +1500 -0
  28. package/dist/cohere/docs/llm/DOC.md +1335 -0
  29. package/dist/datadog/docs/monitoring/javascript/DOC.md +1740 -0
  30. package/dist/datadog/docs/monitoring/python/DOC.md +1815 -0
  31. package/dist/deepgram/docs/speech/javascript/DOC.md +885 -0
  32. package/dist/deepgram/docs/speech/python/DOC.md +685 -0
  33. package/dist/deepl/docs/translation/javascript/DOC.md +887 -0
  34. package/dist/deepl/docs/translation/python/DOC.md +944 -0
  35. package/dist/deepseek/docs/llm/DOC.md +1220 -0
  36. package/dist/directus/docs/headless-cms/javascript/DOC.md +1128 -0
  37. package/dist/directus/docs/headless-cms/python/DOC.md +1276 -0
  38. package/dist/discord/docs/bot/javascript/DOC.md +1090 -0
  39. package/dist/discord/docs/bot/python/DOC.md +1130 -0
  40. package/dist/elasticsearch/docs/search/DOC.md +1634 -0
  41. package/dist/elevenlabs/docs/text-to-speech/javascript/DOC.md +336 -0
  42. package/dist/elevenlabs/docs/text-to-speech/python/DOC.md +552 -0
  43. package/dist/firebase/docs/auth/DOC.md +1015 -0
  44. package/dist/gemini/docs/genai/javascript/DOC.md +691 -0
  45. package/dist/gemini/docs/genai/python/DOC.md +555 -0
  46. package/dist/github/docs/octokit/DOC.md +1560 -0
  47. package/dist/google/docs/bigquery/javascript/DOC.md +1688 -0
  48. package/dist/google/docs/bigquery/python/DOC.md +1503 -0
  49. package/dist/hubspot/docs/crm/javascript/DOC.md +1805 -0
  50. package/dist/hubspot/docs/crm/python/DOC.md +2033 -0
  51. package/dist/huggingface/docs/transformers/DOC.md +948 -0
  52. package/dist/intercom/docs/messaging/javascript/DOC.md +1844 -0
  53. package/dist/intercom/docs/messaging/python/DOC.md +1797 -0
  54. package/dist/jira/docs/issues/javascript/DOC.md +1420 -0
  55. package/dist/jira/docs/issues/python/DOC.md +1492 -0
  56. package/dist/kafka/docs/streaming/javascript/DOC.md +1671 -0
  57. package/dist/kafka/docs/streaming/python/DOC.md +1464 -0
  58. package/dist/landingai-ade/docs/api/DOC.md +620 -0
  59. package/dist/landingai-ade/docs/sdk/python/DOC.md +489 -0
  60. package/dist/landingai-ade/docs/sdk/typescript/DOC.md +542 -0
  61. package/dist/landingai-ade/skills/SKILL.md +489 -0
  62. package/dist/launchdarkly/docs/feature-flags/javascript/DOC.md +1191 -0
  63. package/dist/launchdarkly/docs/feature-flags/python/DOC.md +1671 -0
  64. package/dist/linear/docs/tracker/DOC.md +1554 -0
  65. package/dist/livekit/docs/realtime/javascript/DOC.md +303 -0
  66. package/dist/livekit/docs/realtime/python/DOC.md +163 -0
  67. package/dist/mailchimp/docs/marketing/DOC.md +1420 -0
  68. package/dist/meilisearch/docs/search/DOC.md +1241 -0
  69. package/dist/microsoft/docs/onedrive/javascript/DOC.md +1421 -0
  70. package/dist/microsoft/docs/onedrive/python/DOC.md +1549 -0
  71. package/dist/mongodb/docs/atlas/DOC.md +2041 -0
  72. package/dist/notion/docs/workspace-api/javascript/DOC.md +1435 -0
  73. package/dist/notion/docs/workspace-api/python/DOC.md +1400 -0
  74. package/dist/okta/docs/identity/javascript/DOC.md +1171 -0
  75. package/dist/okta/docs/identity/python/DOC.md +1401 -0
  76. package/dist/openai/docs/chat/javascript/DOC.md +407 -0
  77. package/dist/openai/docs/chat/python/DOC.md +568 -0
  78. package/dist/paypal/docs/checkout/DOC.md +278 -0
  79. package/dist/pinecone/docs/sdk/javascript/DOC.md +984 -0
  80. package/dist/pinecone/docs/sdk/python/DOC.md +1395 -0
  81. package/dist/plaid/docs/banking/javascript/DOC.md +1163 -0
  82. package/dist/plaid/docs/banking/python/DOC.md +1203 -0
  83. package/dist/playwright-community/skills/login-flows/SKILL.md +108 -0
  84. package/dist/postmark/docs/transactional-email/DOC.md +1168 -0
  85. package/dist/prisma/docs/orm/javascript/DOC.md +1419 -0
  86. package/dist/prisma/docs/orm/python/DOC.md +1317 -0
  87. package/dist/qdrant/docs/vector-search/javascript/DOC.md +1221 -0
  88. package/dist/qdrant/docs/vector-search/python/DOC.md +1653 -0
  89. package/dist/rabbitmq/docs/message-queue/javascript/DOC.md +1193 -0
  90. package/dist/rabbitmq/docs/message-queue/python/DOC.md +1243 -0
  91. package/dist/razorpay/docs/payments/javascript/DOC.md +1219 -0
  92. package/dist/razorpay/docs/payments/python/DOC.md +1330 -0
  93. package/dist/redis/docs/key-value/javascript/DOC.md +1851 -0
  94. package/dist/redis/docs/key-value/python/DOC.md +2054 -0
  95. package/dist/registry.json +2817 -0
  96. package/dist/replicate/docs/model-hosting/DOC.md +1318 -0
  97. package/dist/resend/docs/email/DOC.md +1271 -0
  98. package/dist/salesforce/docs/crm/javascript/DOC.md +1241 -0
  99. package/dist/salesforce/docs/crm/python/DOC.md +1183 -0
  100. package/dist/search-index.json +1 -0
  101. package/dist/sendgrid/docs/email-api/javascript/DOC.md +371 -0
  102. package/dist/sendgrid/docs/email-api/python/DOC.md +656 -0
  103. package/dist/sentry/docs/error-tracking/javascript/DOC.md +1073 -0
  104. package/dist/sentry/docs/error-tracking/python/DOC.md +1309 -0
  105. package/dist/shopify/docs/storefront/DOC.md +457 -0
  106. package/dist/slack/docs/workspace/javascript/DOC.md +933 -0
  107. package/dist/slack/docs/workspace/python/DOC.md +271 -0
  108. package/dist/square/docs/payments/javascript/DOC.md +1855 -0
  109. package/dist/square/docs/payments/python/DOC.md +1728 -0
  110. package/dist/stripe/docs/api/DOC.md +1727 -0
  111. package/dist/stripe/docs/payments/DOC.md +1726 -0
  112. package/dist/stytch/docs/auth/javascript/DOC.md +1813 -0
  113. package/dist/stytch/docs/auth/python/DOC.md +1962 -0
  114. package/dist/supabase/docs/client/DOC.md +1606 -0
  115. package/dist/twilio/docs/messaging/python/DOC.md +469 -0
  116. package/dist/twilio/docs/messaging/typescript/DOC.md +946 -0
  117. package/dist/vercel/docs/platform/DOC.md +1940 -0
  118. package/dist/weaviate/docs/vector-db/javascript/DOC.md +1268 -0
  119. package/dist/weaviate/docs/vector-db/python/DOC.md +1388 -0
  120. package/dist/zendesk/docs/support/javascript/DOC.md +2150 -0
  121. package/dist/zendesk/docs/support/python/DOC.md +2297 -0
  122. package/package.json +22 -6
  123. package/skills/get-api-docs/SKILL.md +84 -0
  124. package/src/commands/annotate.js +83 -0
  125. package/src/commands/build.js +12 -1
  126. package/src/commands/feedback.js +150 -0
  127. package/src/commands/get.js +83 -42
  128. package/src/commands/search.js +7 -0
  129. package/src/index.js +43 -17
  130. package/src/lib/analytics.js +90 -0
  131. package/src/lib/annotations.js +57 -0
  132. package/src/lib/bm25.js +170 -0
  133. package/src/lib/cache.js +69 -6
  134. package/src/lib/config.js +8 -3
  135. package/src/lib/identity.js +99 -0
  136. package/src/lib/registry.js +103 -20
  137. package/src/lib/telemetry.js +86 -0
  138. package/src/mcp/server.js +177 -0
  139. 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
+ ```