airtable-cli 0.1.0__tar.gz

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 (37) hide show
  1. airtable_cli-0.1.0/.claude/settings.local.json +17 -0
  2. airtable_cli-0.1.0/.gitignore +12 -0
  3. airtable_cli-0.1.0/LICENSE +21 -0
  4. airtable_cli-0.1.0/PKG-INFO +270 -0
  5. airtable_cli-0.1.0/README.md +212 -0
  6. airtable_cli-0.1.0/airtable_cli/__init__.py +1 -0
  7. airtable_cli-0.1.0/airtable_cli/client.py +132 -0
  8. airtable_cli-0.1.0/airtable_cli/commands/__init__.py +0 -0
  9. airtable_cli-0.1.0/airtable_cli/commands/auth.py +64 -0
  10. airtable_cli-0.1.0/airtable_cli/commands/bases.py +79 -0
  11. airtable_cli-0.1.0/airtable_cli/commands/comments.py +174 -0
  12. airtable_cli-0.1.0/airtable_cli/commands/fields.py +179 -0
  13. airtable_cli-0.1.0/airtable_cli/commands/records.py +319 -0
  14. airtable_cli-0.1.0/airtable_cli/commands/tables.py +146 -0
  15. airtable_cli-0.1.0/airtable_cli/commands/webhooks.py +235 -0
  16. airtable_cli-0.1.0/airtable_cli/config.py +97 -0
  17. airtable_cli-0.1.0/airtable_cli/interactive/__init__.py +0 -0
  18. airtable_cli-0.1.0/airtable_cli/interactive/prompts.py +34 -0
  19. airtable_cli-0.1.0/airtable_cli/interactive/resolvers.py +58 -0
  20. airtable_cli-0.1.0/airtable_cli/main.py +60 -0
  21. airtable_cli-0.1.0/airtable_cli/models/__init__.py +0 -0
  22. airtable_cli-0.1.0/airtable_cli/models/base.py +24 -0
  23. airtable_cli-0.1.0/airtable_cli/models/comment.py +23 -0
  24. airtable_cli-0.1.0/airtable_cli/models/common.py +15 -0
  25. airtable_cli-0.1.0/airtable_cli/models/field.py +44 -0
  26. airtable_cli-0.1.0/airtable_cli/models/record.py +15 -0
  27. airtable_cli-0.1.0/airtable_cli/models/table.py +24 -0
  28. airtable_cli-0.1.0/airtable_cli/models/webhook.py +34 -0
  29. airtable_cli-0.1.0/airtable_cli/output/__init__.py +0 -0
  30. airtable_cli-0.1.0/airtable_cli/output/console.py +6 -0
  31. airtable_cli-0.1.0/airtable_cli/output/formatters.py +43 -0
  32. airtable_cli-0.1.0/homebrew/airtable-cli.rb +26 -0
  33. airtable_cli-0.1.0/pyproject.toml +57 -0
  34. airtable_cli-0.1.0/tests/__init__.py +0 -0
  35. airtable_cli-0.1.0/tests/test_cli.py +92 -0
  36. airtable_cli-0.1.0/tests/test_client.py +43 -0
  37. airtable_cli-0.1.0/tests/test_config.py +50 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:airtable.com)",
5
+ "WebFetch(domain:developer.airtable.com)",
6
+ "WebSearch",
7
+ "WebFetch(domain:support.airtable.com)",
8
+ "Bash(pip3:*)",
9
+ "Bash(python3:*)",
10
+ "Bash(pip install:*)",
11
+ "Bash(/Users/xavier/Library/Python/3.12/bin/airtable:*)",
12
+ "Bash(/Users/xavier/Library/Python/3.12/bin/pytest:*)",
13
+ "Bash(AIRTABLE_PAT=pat1kF01sMFQAPbKK.a59d4400c7223470936868e3abb0f4d096c7fe819e0f7d64081a4aa7fc358cac /Users/xavier/Library/Python/3.12/bin/airtable:*)",
14
+ "Bash(curl:*)"
15
+ ]
16
+ }
17
+ }
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .env
9
+ *.log
10
+ .pytest_cache/
11
+ .coverage
12
+ htmlcov/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xavier
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,270 @@
1
+ Metadata-Version: 2.4
2
+ Name: airtable-cli
3
+ Version: 0.1.0
4
+ Summary: A CLI tool to interact with the full Airtable Web API
5
+ Project-URL: Homepage, https://github.com/professor-eggs/airtable-cli
6
+ Project-URL: Repository, https://github.com/professor-eggs/airtable-cli
7
+ Project-URL: Bug Tracker, https://github.com/professor-eggs/airtable-cli/issues
8
+ Author-email: professor-eggs <0xavier0@gmail.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Xavier
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: airtable,api,cli,database,no-code
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Database
43
+ Classifier: Topic :: Utilities
44
+ Requires-Python: >=3.10
45
+ Requires-Dist: httpx>=0.27.0
46
+ Requires-Dist: inquirerpy>=0.3.4
47
+ Requires-Dist: pydantic>=2.7.0
48
+ Requires-Dist: pyyaml>=6.0
49
+ Requires-Dist: rich>=13.7.0
50
+ Requires-Dist: tomli-w>=1.0.0
51
+ Requires-Dist: tomli>=2.0.0; python_version < '3.11'
52
+ Requires-Dist: typer>=0.12.0
53
+ Provides-Extra: dev
54
+ Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
55
+ Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
56
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
57
+ Description-Content-Type: text/markdown
58
+
59
+ # airtable-cli
60
+
61
+ A command-line tool for interacting with the [Airtable Web API](https://airtable.com/developers/web/api/introduction). Manage bases, tables, fields, records, comments, and webhooks from your terminal — with interactive fuzzy-search prompts or fully non-interactive scripting mode.
62
+
63
+ ## Requirements
64
+
65
+ - Python 3.10+
66
+
67
+ ## Installation
68
+
69
+ **via pip** (simplest):
70
+ ```bash
71
+ pip install airtable-cli
72
+ ```
73
+
74
+ **via pipx** (recommended — isolates the tool from your system Python):
75
+ ```bash
76
+ pipx install airtable-cli
77
+ ```
78
+
79
+ **via Homebrew** (macOS/Linux):
80
+ ```bash
81
+ brew tap professor-eggs/airtable-cli
82
+ brew install airtable-cli
83
+ ```
84
+
85
+ **from source** (development):
86
+ ```bash
87
+ git clone https://github.com/professor-eggs/airtable-cli
88
+ cd airtable-cli
89
+ pip install -e ".[dev]"
90
+ ```
91
+
92
+ ## Authentication
93
+
94
+ Get a Personal Access Token from [airtable.com/create/tokens](https://airtable.com/create/tokens), then configure the CLI:
95
+
96
+ ```bash
97
+ airtable auth configure --token patXXXXXXXXXXXXXX
98
+ ```
99
+
100
+ You can also set the token via environment variable (takes precedence over the config file):
101
+
102
+ ```bash
103
+ export AIRTABLE_PAT=patXXXXXXXXXXXXXX
104
+ ```
105
+
106
+ The config file is stored at `~/.config/airtable-cli/config.toml`.
107
+
108
+ ## Usage
109
+
110
+ ```
111
+ airtable [--base TEXT] [--output table|json|yaml] [--no-interactive] [--version]
112
+ ```
113
+
114
+ | Flag | Description |
115
+ |------|-------------|
116
+ | `--base` | Override the default base ID for this invocation (env: `AIRTABLE_BASE_ID`) |
117
+ | `--output`, `-o` | Output format: `table` (default), `json`, or `yaml` |
118
+ | `--no-interactive` | Disable all prompts — required args must be passed as flags |
119
+ | `--version` | Print version and exit |
120
+
121
+ ## Command Reference
122
+
123
+ ### `auth`
124
+
125
+ ```bash
126
+ airtable auth configure [--token TEXT] [--default-base TEXT]
127
+ airtable auth show
128
+ airtable auth revoke
129
+ ```
130
+
131
+ ### `bases`
132
+
133
+ ```bash
134
+ airtable bases list
135
+ airtable bases schema [--base TEXT]
136
+ ```
137
+
138
+ ### `tables`
139
+
140
+ ```bash
141
+ airtable tables list [--base TEXT]
142
+ airtable tables get [--base TEXT] [--table TEXT]
143
+ airtable tables create [--base TEXT] [--name TEXT] [--description TEXT] [--fields JSON]
144
+ ```
145
+
146
+ ### `fields`
147
+
148
+ ```bash
149
+ airtable fields list [--base TEXT] [--table TEXT]
150
+ airtable fields create [--base TEXT] [--table TEXT] [--name TEXT] [--type TEXT] [--options JSON]
151
+ airtable fields update [--base TEXT] [--table TEXT] [--field TEXT] [--name TEXT] [--options JSON]
152
+ ```
153
+
154
+ ### `records`
155
+
156
+ ```bash
157
+ airtable records list [--base TEXT] [--table TEXT] [--view TEXT] [--filter TEXT]
158
+ [--sort FIELD:asc|desc]... [--max-records INT]
159
+ [--fields TEXT]... [--all-pages] [--cell-format json|string]
160
+ airtable records get [--base TEXT] [--table TEXT] [--record TEXT]
161
+ airtable records create [--base TEXT] [--table TEXT] [--fields JSON] [--fields-file PATH] [--typecast]
162
+ airtable records update [--base TEXT] [--table TEXT] [--record TEXT]...
163
+ [--fields JSON] [--fields-file PATH] [--mode patch|put] [--typecast]
164
+ airtable records delete [--base TEXT] [--table TEXT] [--record TEXT]... [--confirm/--no-confirm]
165
+ ```
166
+
167
+ ### `comments`
168
+
169
+ ```bash
170
+ airtable comments list [--base TEXT] [--table TEXT] [--record TEXT]
171
+ airtable comments create [--base TEXT] [--table TEXT] [--record TEXT] [--text TEXT]
172
+ airtable comments delete [--base TEXT] [--table TEXT] [--record TEXT] [--comment TEXT] [--confirm/--no-confirm]
173
+ ```
174
+
175
+ ### `webhooks`
176
+
177
+ ```bash
178
+ airtable webhooks list [--base TEXT]
179
+ airtable webhooks create [--base TEXT] [--url TEXT] [--filters JSON] [--specification JSON]
180
+ airtable webhooks update [--base TEXT] [--webhook TEXT] [--enable/--disable] [--refresh-expiry]
181
+ airtable webhooks delete [--base TEXT] [--webhook TEXT] [--confirm/--no-confirm]
182
+ airtable webhooks payloads [--base TEXT] [--webhook TEXT] [--cursor INT]
183
+ ```
184
+
185
+ ## Examples
186
+
187
+ ```bash
188
+ # Configure auth and set a default base
189
+ airtable auth configure --token patXXX --default-base appXXXXXXXX
190
+
191
+ # List all bases as JSON
192
+ airtable --output json bases list
193
+
194
+ # List tables in a base
195
+ airtable --base appXXXXXXXX tables list
196
+
197
+ # List all records across all pages
198
+ airtable --base appXXXXXXXX records list --table tblXXXXXXXX --all-pages
199
+
200
+ # Filter and sort records
201
+ airtable --base appXXX records list --table tblXXX \
202
+ --filter "Status = 'Active'" \
203
+ --sort "Name:asc" \
204
+ --output json
205
+
206
+ # Create a single record
207
+ airtable --base appXXX records create --table tblXXX \
208
+ --fields '{"Name": "Alice", "Status": "Active"}'
209
+
210
+ # Bulk create from a JSON file
211
+ echo '[{"Name": "Foo"}, {"Name": "Bar"}]' > records.json
212
+ airtable --base appXXX records create --table tblXXX --fields-file records.json
213
+
214
+ # Update a record
215
+ airtable --base appXXX records update --table tblXXX \
216
+ --record recXXXXXXXXXXXXXX \
217
+ --fields '{"Status": "Done"}'
218
+
219
+ # Delete records without confirmation prompt
220
+ airtable --no-interactive --base appXXX records delete \
221
+ --table tblXXX \
222
+ --record recAAA --record recBBB \
223
+ --no-confirm
224
+
225
+ # Create a webhook
226
+ airtable --base appXXX webhooks create --url https://example.com/hook
227
+
228
+ # Disable a webhook
229
+ airtable --base appXXX webhooks update --webhook whdXXX --disable
230
+
231
+ # Script-friendly: pipe JSON output
232
+ airtable --no-interactive --output json --base appXXX records list --table tblXXX \
233
+ | jq '.[].fields.Name'
234
+ ```
235
+
236
+ ## Interactive Mode
237
+
238
+ When a required argument is missing and the terminal is interactive (TTY), the CLI automatically prompts with fuzzy-search pickers — for example, selecting a base by name or picking a record from a list. Pass `--no-interactive` to suppress all prompts and require every argument as a flag.
239
+
240
+ ## Configuration File
241
+
242
+ `~/.config/airtable-cli/config.toml`
243
+
244
+ ```toml
245
+ [auth]
246
+ token = "patXXXXXXXXXXXXXX"
247
+
248
+ [defaults]
249
+ base_id = "appXXXXXXXX"
250
+
251
+ [output]
252
+ format = "table"
253
+ color = true
254
+ ```
255
+
256
+ Environment variables `AIRTABLE_PAT` and `AIRTABLE_BASE_ID` take precedence over the config file.
257
+
258
+ ## Rate Limiting
259
+
260
+ The client uses a token bucket (5 requests/second) and automatically retries on HTTP 429 responses with exponential backoff (up to 3 retries).
261
+
262
+ ## Development
263
+
264
+ ```bash
265
+ # Install with dev dependencies
266
+ pip install -e ".[dev]"
267
+
268
+ # Run tests
269
+ pytest tests/ -v
270
+ ```
@@ -0,0 +1,212 @@
1
+ # airtable-cli
2
+
3
+ A command-line tool for interacting with the [Airtable Web API](https://airtable.com/developers/web/api/introduction). Manage bases, tables, fields, records, comments, and webhooks from your terminal — with interactive fuzzy-search prompts or fully non-interactive scripting mode.
4
+
5
+ ## Requirements
6
+
7
+ - Python 3.10+
8
+
9
+ ## Installation
10
+
11
+ **via pip** (simplest):
12
+ ```bash
13
+ pip install airtable-cli
14
+ ```
15
+
16
+ **via pipx** (recommended — isolates the tool from your system Python):
17
+ ```bash
18
+ pipx install airtable-cli
19
+ ```
20
+
21
+ **via Homebrew** (macOS/Linux):
22
+ ```bash
23
+ brew tap professor-eggs/airtable-cli
24
+ brew install airtable-cli
25
+ ```
26
+
27
+ **from source** (development):
28
+ ```bash
29
+ git clone https://github.com/professor-eggs/airtable-cli
30
+ cd airtable-cli
31
+ pip install -e ".[dev]"
32
+ ```
33
+
34
+ ## Authentication
35
+
36
+ Get a Personal Access Token from [airtable.com/create/tokens](https://airtable.com/create/tokens), then configure the CLI:
37
+
38
+ ```bash
39
+ airtable auth configure --token patXXXXXXXXXXXXXX
40
+ ```
41
+
42
+ You can also set the token via environment variable (takes precedence over the config file):
43
+
44
+ ```bash
45
+ export AIRTABLE_PAT=patXXXXXXXXXXXXXX
46
+ ```
47
+
48
+ The config file is stored at `~/.config/airtable-cli/config.toml`.
49
+
50
+ ## Usage
51
+
52
+ ```
53
+ airtable [--base TEXT] [--output table|json|yaml] [--no-interactive] [--version]
54
+ ```
55
+
56
+ | Flag | Description |
57
+ |------|-------------|
58
+ | `--base` | Override the default base ID for this invocation (env: `AIRTABLE_BASE_ID`) |
59
+ | `--output`, `-o` | Output format: `table` (default), `json`, or `yaml` |
60
+ | `--no-interactive` | Disable all prompts — required args must be passed as flags |
61
+ | `--version` | Print version and exit |
62
+
63
+ ## Command Reference
64
+
65
+ ### `auth`
66
+
67
+ ```bash
68
+ airtable auth configure [--token TEXT] [--default-base TEXT]
69
+ airtable auth show
70
+ airtable auth revoke
71
+ ```
72
+
73
+ ### `bases`
74
+
75
+ ```bash
76
+ airtable bases list
77
+ airtable bases schema [--base TEXT]
78
+ ```
79
+
80
+ ### `tables`
81
+
82
+ ```bash
83
+ airtable tables list [--base TEXT]
84
+ airtable tables get [--base TEXT] [--table TEXT]
85
+ airtable tables create [--base TEXT] [--name TEXT] [--description TEXT] [--fields JSON]
86
+ ```
87
+
88
+ ### `fields`
89
+
90
+ ```bash
91
+ airtable fields list [--base TEXT] [--table TEXT]
92
+ airtable fields create [--base TEXT] [--table TEXT] [--name TEXT] [--type TEXT] [--options JSON]
93
+ airtable fields update [--base TEXT] [--table TEXT] [--field TEXT] [--name TEXT] [--options JSON]
94
+ ```
95
+
96
+ ### `records`
97
+
98
+ ```bash
99
+ airtable records list [--base TEXT] [--table TEXT] [--view TEXT] [--filter TEXT]
100
+ [--sort FIELD:asc|desc]... [--max-records INT]
101
+ [--fields TEXT]... [--all-pages] [--cell-format json|string]
102
+ airtable records get [--base TEXT] [--table TEXT] [--record TEXT]
103
+ airtable records create [--base TEXT] [--table TEXT] [--fields JSON] [--fields-file PATH] [--typecast]
104
+ airtable records update [--base TEXT] [--table TEXT] [--record TEXT]...
105
+ [--fields JSON] [--fields-file PATH] [--mode patch|put] [--typecast]
106
+ airtable records delete [--base TEXT] [--table TEXT] [--record TEXT]... [--confirm/--no-confirm]
107
+ ```
108
+
109
+ ### `comments`
110
+
111
+ ```bash
112
+ airtable comments list [--base TEXT] [--table TEXT] [--record TEXT]
113
+ airtable comments create [--base TEXT] [--table TEXT] [--record TEXT] [--text TEXT]
114
+ airtable comments delete [--base TEXT] [--table TEXT] [--record TEXT] [--comment TEXT] [--confirm/--no-confirm]
115
+ ```
116
+
117
+ ### `webhooks`
118
+
119
+ ```bash
120
+ airtable webhooks list [--base TEXT]
121
+ airtable webhooks create [--base TEXT] [--url TEXT] [--filters JSON] [--specification JSON]
122
+ airtable webhooks update [--base TEXT] [--webhook TEXT] [--enable/--disable] [--refresh-expiry]
123
+ airtable webhooks delete [--base TEXT] [--webhook TEXT] [--confirm/--no-confirm]
124
+ airtable webhooks payloads [--base TEXT] [--webhook TEXT] [--cursor INT]
125
+ ```
126
+
127
+ ## Examples
128
+
129
+ ```bash
130
+ # Configure auth and set a default base
131
+ airtable auth configure --token patXXX --default-base appXXXXXXXX
132
+
133
+ # List all bases as JSON
134
+ airtable --output json bases list
135
+
136
+ # List tables in a base
137
+ airtable --base appXXXXXXXX tables list
138
+
139
+ # List all records across all pages
140
+ airtable --base appXXXXXXXX records list --table tblXXXXXXXX --all-pages
141
+
142
+ # Filter and sort records
143
+ airtable --base appXXX records list --table tblXXX \
144
+ --filter "Status = 'Active'" \
145
+ --sort "Name:asc" \
146
+ --output json
147
+
148
+ # Create a single record
149
+ airtable --base appXXX records create --table tblXXX \
150
+ --fields '{"Name": "Alice", "Status": "Active"}'
151
+
152
+ # Bulk create from a JSON file
153
+ echo '[{"Name": "Foo"}, {"Name": "Bar"}]' > records.json
154
+ airtable --base appXXX records create --table tblXXX --fields-file records.json
155
+
156
+ # Update a record
157
+ airtable --base appXXX records update --table tblXXX \
158
+ --record recXXXXXXXXXXXXXX \
159
+ --fields '{"Status": "Done"}'
160
+
161
+ # Delete records without confirmation prompt
162
+ airtable --no-interactive --base appXXX records delete \
163
+ --table tblXXX \
164
+ --record recAAA --record recBBB \
165
+ --no-confirm
166
+
167
+ # Create a webhook
168
+ airtable --base appXXX webhooks create --url https://example.com/hook
169
+
170
+ # Disable a webhook
171
+ airtable --base appXXX webhooks update --webhook whdXXX --disable
172
+
173
+ # Script-friendly: pipe JSON output
174
+ airtable --no-interactive --output json --base appXXX records list --table tblXXX \
175
+ | jq '.[].fields.Name'
176
+ ```
177
+
178
+ ## Interactive Mode
179
+
180
+ When a required argument is missing and the terminal is interactive (TTY), the CLI automatically prompts with fuzzy-search pickers — for example, selecting a base by name or picking a record from a list. Pass `--no-interactive` to suppress all prompts and require every argument as a flag.
181
+
182
+ ## Configuration File
183
+
184
+ `~/.config/airtable-cli/config.toml`
185
+
186
+ ```toml
187
+ [auth]
188
+ token = "patXXXXXXXXXXXXXX"
189
+
190
+ [defaults]
191
+ base_id = "appXXXXXXXX"
192
+
193
+ [output]
194
+ format = "table"
195
+ color = true
196
+ ```
197
+
198
+ Environment variables `AIRTABLE_PAT` and `AIRTABLE_BASE_ID` take precedence over the config file.
199
+
200
+ ## Rate Limiting
201
+
202
+ The client uses a token bucket (5 requests/second) and automatically retries on HTTP 429 responses with exponential backoff (up to 3 retries).
203
+
204
+ ## Development
205
+
206
+ ```bash
207
+ # Install with dev dependencies
208
+ pip install -e ".[dev]"
209
+
210
+ # Run tests
211
+ pytest tests/ -v
212
+ ```
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,132 @@
1
+ """httpx wrapper with auth, rate limiting (token bucket), pagination, and error handling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections.abc import Generator
7
+ from typing import Any, Optional
8
+
9
+ import httpx
10
+ from rich.console import Console
11
+
12
+ _console = Console(stderr=True)
13
+
14
+ BASE_URL_V0 = "https://api.airtable.com/v0"
15
+ BASE_URL_META = "https://api.airtable.com/v0/meta"
16
+ BASE_URL_BASES = "https://api.airtable.com/v0/bases"
17
+
18
+ MAX_RETRIES = 3
19
+ RATE_LIMIT = 5 # requests per second
20
+
21
+
22
+ class AirtableAPIError(Exception):
23
+ def __init__(self, status_code: int, error_type: str, message: str) -> None:
24
+ self.status_code = status_code
25
+ self.error_type = error_type
26
+ self.message = message
27
+ super().__init__(f"[{status_code}] {error_type}: {message}")
28
+
29
+
30
+ class _TokenBucket:
31
+ """Simple token bucket for rate limiting."""
32
+
33
+ def __init__(self, rate: float) -> None:
34
+ self.rate = rate
35
+ self.tokens = rate
36
+ self.last = time.monotonic()
37
+
38
+ def consume(self) -> None:
39
+ now = time.monotonic()
40
+ elapsed = now - self.last
41
+ self.tokens = min(self.rate, self.tokens + elapsed * self.rate)
42
+ self.last = now
43
+ if self.tokens < 1:
44
+ sleep_time = (1 - self.tokens) / self.rate
45
+ time.sleep(sleep_time)
46
+ self.tokens = 0
47
+ else:
48
+ self.tokens -= 1
49
+
50
+
51
+ _bucket = _TokenBucket(RATE_LIMIT)
52
+
53
+
54
+ def _get_client(token: str) -> httpx.Client:
55
+ return httpx.Client(
56
+ headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
57
+ timeout=30.0,
58
+ )
59
+
60
+
61
+ def _handle_response(response: httpx.Response) -> dict[str, Any]:
62
+ if response.is_success:
63
+ if response.content:
64
+ return response.json()
65
+ return {}
66
+ try:
67
+ body = response.json()
68
+ err = body.get("error", {})
69
+ error_type = err.get("type", "UNKNOWN_ERROR")
70
+ message = err.get("message", response.text)
71
+ except Exception:
72
+ error_type = "UNKNOWN_ERROR"
73
+ message = response.text
74
+ raise AirtableAPIError(response.status_code, error_type, message)
75
+
76
+
77
+ def _request(
78
+ method: str,
79
+ url: str,
80
+ token: str,
81
+ *,
82
+ params: Optional[dict] = None,
83
+ json: Optional[dict | list] = None,
84
+ ) -> dict[str, Any]:
85
+ _bucket.consume()
86
+ backoff = 1.0
87
+ for attempt in range(MAX_RETRIES):
88
+ with _get_client(token) as client:
89
+ response = client.request(method, url, params=params, json=json)
90
+ if response.status_code == 429:
91
+ if attempt < MAX_RETRIES - 1:
92
+ time.sleep(backoff)
93
+ backoff *= 2
94
+ continue
95
+ return _handle_response(response)
96
+ return _handle_response(response) # type: ignore[reportPossiblyUnbound]
97
+
98
+
99
+ def get(url: str, token: str, params: Optional[dict] = None) -> dict[str, Any]:
100
+ return _request("GET", url, token, params=params)
101
+
102
+
103
+ def post(url: str, token: str, json: dict | list) -> dict[str, Any]:
104
+ return _request("POST", url, token, json=json)
105
+
106
+
107
+ def patch(url: str, token: str, json: dict | list) -> dict[str, Any]:
108
+ return _request("PATCH", url, token, json=json)
109
+
110
+
111
+ def put(url: str, token: str, json: dict | list) -> dict[str, Any]:
112
+ return _request("PUT", url, token, json=json)
113
+
114
+
115
+ def delete(url: str, token: str, params: Optional[dict] = None) -> dict[str, Any]:
116
+ return _request("DELETE", url, token, params=params)
117
+
118
+
119
+ def paginate(
120
+ url: str,
121
+ token: str,
122
+ params: Optional[dict] = None,
123
+ ) -> Generator[dict[str, Any], None, None]:
124
+ """Yield successive pages until no offset is returned."""
125
+ params = dict(params or {})
126
+ while True:
127
+ data = get(url, token, params=params)
128
+ yield data
129
+ offset = data.get("offset")
130
+ if not offset:
131
+ break
132
+ params["offset"] = offset
File without changes