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.
- airtable_cli-0.1.0/.claude/settings.local.json +17 -0
- airtable_cli-0.1.0/.gitignore +12 -0
- airtable_cli-0.1.0/LICENSE +21 -0
- airtable_cli-0.1.0/PKG-INFO +270 -0
- airtable_cli-0.1.0/README.md +212 -0
- airtable_cli-0.1.0/airtable_cli/__init__.py +1 -0
- airtable_cli-0.1.0/airtable_cli/client.py +132 -0
- airtable_cli-0.1.0/airtable_cli/commands/__init__.py +0 -0
- airtable_cli-0.1.0/airtable_cli/commands/auth.py +64 -0
- airtable_cli-0.1.0/airtable_cli/commands/bases.py +79 -0
- airtable_cli-0.1.0/airtable_cli/commands/comments.py +174 -0
- airtable_cli-0.1.0/airtable_cli/commands/fields.py +179 -0
- airtable_cli-0.1.0/airtable_cli/commands/records.py +319 -0
- airtable_cli-0.1.0/airtable_cli/commands/tables.py +146 -0
- airtable_cli-0.1.0/airtable_cli/commands/webhooks.py +235 -0
- airtable_cli-0.1.0/airtable_cli/config.py +97 -0
- airtable_cli-0.1.0/airtable_cli/interactive/__init__.py +0 -0
- airtable_cli-0.1.0/airtable_cli/interactive/prompts.py +34 -0
- airtable_cli-0.1.0/airtable_cli/interactive/resolvers.py +58 -0
- airtable_cli-0.1.0/airtable_cli/main.py +60 -0
- airtable_cli-0.1.0/airtable_cli/models/__init__.py +0 -0
- airtable_cli-0.1.0/airtable_cli/models/base.py +24 -0
- airtable_cli-0.1.0/airtable_cli/models/comment.py +23 -0
- airtable_cli-0.1.0/airtable_cli/models/common.py +15 -0
- airtable_cli-0.1.0/airtable_cli/models/field.py +44 -0
- airtable_cli-0.1.0/airtable_cli/models/record.py +15 -0
- airtable_cli-0.1.0/airtable_cli/models/table.py +24 -0
- airtable_cli-0.1.0/airtable_cli/models/webhook.py +34 -0
- airtable_cli-0.1.0/airtable_cli/output/__init__.py +0 -0
- airtable_cli-0.1.0/airtable_cli/output/console.py +6 -0
- airtable_cli-0.1.0/airtable_cli/output/formatters.py +43 -0
- airtable_cli-0.1.0/homebrew/airtable-cli.rb +26 -0
- airtable_cli-0.1.0/pyproject.toml +57 -0
- airtable_cli-0.1.0/tests/__init__.py +0 -0
- airtable_cli-0.1.0/tests/test_cli.py +92 -0
- airtable_cli-0.1.0/tests/test_client.py +43 -0
- 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,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
|