quickbase-api 0.1.0__py3-none-any.whl
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.
- quickbase_api/__init__.py +68 -0
- quickbase_api/api.py +564 -0
- quickbase_api/exceptions.py +19 -0
- quickbase_api/legacy_method.py +66 -0
- quickbase_api/rest_method.py +75 -0
- quickbase_api/session.py +32 -0
- quickbase_api-0.1.0.dist-info/METADATA +265 -0
- quickbase_api-0.1.0.dist-info/RECORD +10 -0
- quickbase_api-0.1.0.dist-info/WHEEL +4 -0
- quickbase_api-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""A simple Python wrapper for the Quickbase API.
|
|
2
|
+
|
|
3
|
+
Example::
|
|
4
|
+
|
|
5
|
+
import quickbase_api
|
|
6
|
+
|
|
7
|
+
# Option 1: Using the factory function (reads from environment variables)
|
|
8
|
+
client = quickbase_api.client()
|
|
9
|
+
|
|
10
|
+
# Option 2: Passing credentials directly
|
|
11
|
+
client = quickbase_api.client(
|
|
12
|
+
realm="mycompany.quickbase.com",
|
|
13
|
+
user_token="my-token",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Option 3: Using the class directly
|
|
17
|
+
from quickbase_api import QuickbaseClient
|
|
18
|
+
client = QuickbaseClient("mycompany.quickbase.com", "my-token")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
|
|
25
|
+
from quickbase_api.api import QuickbaseClient
|
|
26
|
+
from quickbase_api.exceptions import (
|
|
27
|
+
QuickbaseAPIError,
|
|
28
|
+
QuickbaseError,
|
|
29
|
+
QuickbaseNotFoundError,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"QuickbaseClient",
|
|
36
|
+
"QuickbaseAPIError",
|
|
37
|
+
"QuickbaseError",
|
|
38
|
+
"QuickbaseNotFoundError",
|
|
39
|
+
"client",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def client(realm: str = None, user_token: str = None) -> QuickbaseClient:
|
|
44
|
+
"""Create a QuickbaseClient, falling back to environment variables.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
realm: The Quickbase realm hostname. If not provided, reads from
|
|
48
|
+
the ``QUICKBASE_REALM`` environment variable.
|
|
49
|
+
user_token: The Quickbase user token. If not provided, reads from
|
|
50
|
+
the ``QUICKBASE_USER_TOKEN`` environment variable.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A configured QuickbaseClient instance.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
KeyError: If a credential is not provided and the corresponding
|
|
57
|
+
environment variable is not set.
|
|
58
|
+
|
|
59
|
+
Example::
|
|
60
|
+
|
|
61
|
+
# Set environment variables, then:
|
|
62
|
+
import quickbase_api
|
|
63
|
+
client = quickbase_api.client()
|
|
64
|
+
"""
|
|
65
|
+
return QuickbaseClient(
|
|
66
|
+
realm=realm or os.environ["QUICKBASE_REALM"],
|
|
67
|
+
user_token=user_token or os.environ["QUICKBASE_USER_TOKEN"],
|
|
68
|
+
)
|
quickbase_api/api.py
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
# FILE: api.py
|
|
2
|
+
|
|
3
|
+
"""Quickbase API client providing high-level methods for apps, tables, fields, and records."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from quickbase_api.exceptions import QuickbaseAPIError, QuickbaseNotFoundError
|
|
11
|
+
from quickbase_api.legacy_method import LegacyMethod
|
|
12
|
+
from quickbase_api.rest_method import RestMethod
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class QuickbaseClient:
|
|
18
|
+
"""High-level client for interacting with the Quickbase API.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
realm: The Quickbase realm hostname (e.g., 'mycompany.quickbase.com').
|
|
22
|
+
user_token: The Quickbase user token for authentication.
|
|
23
|
+
|
|
24
|
+
Example::
|
|
25
|
+
|
|
26
|
+
from quickbase_api import QuickbaseClient
|
|
27
|
+
|
|
28
|
+
client = QuickbaseClient("mycompany.quickbase.com", "my-token")
|
|
29
|
+
app = client.get_app("bqrg4xyza")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, realm: str, user_token: str):
|
|
33
|
+
self.realm = realm
|
|
34
|
+
self.user_token = user_token
|
|
35
|
+
self._rm = RestMethod(realm, user_token)
|
|
36
|
+
self._lm = LegacyMethod(realm, user_token)
|
|
37
|
+
|
|
38
|
+
def _require_success(self, resp_json: dict, resp_code: int, context: str) -> dict:
|
|
39
|
+
"""Raise QuickbaseAPIError if the response code indicates failure.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
resp_json: The parsed response body.
|
|
43
|
+
resp_code: The HTTP status code.
|
|
44
|
+
context: A human-readable description of the operation for error messages.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The response body if successful.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
QuickbaseAPIError: If the status code is 400 or above.
|
|
51
|
+
"""
|
|
52
|
+
if resp_code >= 400:
|
|
53
|
+
raise QuickbaseAPIError(resp_code, resp_json)
|
|
54
|
+
return resp_json
|
|
55
|
+
|
|
56
|
+
# ── Apps ──────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
def create_app(
|
|
59
|
+
self,
|
|
60
|
+
name: str,
|
|
61
|
+
description: str = None,
|
|
62
|
+
assign_token: bool = None,
|
|
63
|
+
) -> str:
|
|
64
|
+
"""Create a new Quickbase app.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
name: The name for the new app.
|
|
68
|
+
description: Optional description for the app.
|
|
69
|
+
assign_token: If True, assigns the user token to the new app.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The app ID of the newly created app.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
QuickbaseAPIError: If the API request fails.
|
|
76
|
+
"""
|
|
77
|
+
body = {"name": name}
|
|
78
|
+
if description is not None:
|
|
79
|
+
body["description"] = description
|
|
80
|
+
if assign_token is not None:
|
|
81
|
+
body["assignToken"] = assign_token
|
|
82
|
+
|
|
83
|
+
resp_json, resp_code = self._rm.post("apps", data=body)
|
|
84
|
+
self._require_success(resp_json, resp_code, f"create app '{name}'")
|
|
85
|
+
logger.info("Created app '%s' with ID: %s", resp_json["name"], resp_json["id"])
|
|
86
|
+
return resp_json["id"]
|
|
87
|
+
|
|
88
|
+
def delete_app(self, app_name: str, app_id: str) -> dict:
|
|
89
|
+
"""Delete a Quickbase app.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
app_name: The name of the app (required for confirmation).
|
|
93
|
+
app_id: The ID of the app to delete.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The API response body.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
QuickbaseAPIError: If the API request fails.
|
|
100
|
+
"""
|
|
101
|
+
resp_json, resp_code = self._rm.delete(
|
|
102
|
+
f"apps/{app_id}",
|
|
103
|
+
data={"name": app_name},
|
|
104
|
+
)
|
|
105
|
+
self._require_success(resp_json, resp_code, f"delete app '{app_name}'")
|
|
106
|
+
logger.info("Deleted app '%s' (%s)", app_name, app_id)
|
|
107
|
+
return resp_json
|
|
108
|
+
|
|
109
|
+
def get_app(self, app_id: str) -> dict:
|
|
110
|
+
"""Get metadata for a Quickbase app.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
app_id: The ID of the app.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
A dictionary of app metadata.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
QuickbaseAPIError: If the API request fails.
|
|
120
|
+
"""
|
|
121
|
+
resp_json, resp_code = self._rm.get(f"apps/{app_id}")
|
|
122
|
+
self._require_success(resp_json, resp_code, f"get app '{app_id}'")
|
|
123
|
+
return resp_json
|
|
124
|
+
|
|
125
|
+
# ── Tables ────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
def create_table(self, app_id: str, properties: dict) -> str:
|
|
128
|
+
"""Create a new table in a Quickbase app.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
app_id: The ID of the app to create the table in.
|
|
132
|
+
properties: A dictionary of table properties (must include 'name').
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
The table ID of the newly created table.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
QuickbaseAPIError: If the API request fails.
|
|
139
|
+
"""
|
|
140
|
+
resp_json, resp_code = self._rm.post(
|
|
141
|
+
"tables",
|
|
142
|
+
params={"appId": app_id},
|
|
143
|
+
data=properties,
|
|
144
|
+
)
|
|
145
|
+
self._require_success(resp_json, resp_code, f"create table in app '{app_id}'")
|
|
146
|
+
logger.info("Created table '%s' with ID: %s", resp_json["name"], resp_json["id"])
|
|
147
|
+
return resp_json["id"]
|
|
148
|
+
|
|
149
|
+
def get_tables(self, app_id: str) -> list[dict]:
|
|
150
|
+
"""Get all tables for a Quickbase app.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
app_id: The ID of the app.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
A list of table metadata dictionaries.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
QuickbaseAPIError: If the API request fails.
|
|
160
|
+
"""
|
|
161
|
+
resp_json, resp_code = self._rm.get("tables", params={"appId": app_id})
|
|
162
|
+
self._require_success(resp_json, resp_code, f"get tables for app '{app_id}'")
|
|
163
|
+
return resp_json
|
|
164
|
+
|
|
165
|
+
def get_table_id(self, app_id: str, table_name: str) -> str:
|
|
166
|
+
"""Find a table ID by its name.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
app_id: The ID of the app containing the table.
|
|
170
|
+
table_name: The exact name of the table to find.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The table ID.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
QuickbaseNotFoundError: If no table with the given name is found.
|
|
177
|
+
"""
|
|
178
|
+
tables = self.get_tables(app_id)
|
|
179
|
+
for table in tables:
|
|
180
|
+
if table["name"] == table_name:
|
|
181
|
+
return table["id"]
|
|
182
|
+
raise QuickbaseNotFoundError(f"No table named '{table_name}' in app '{app_id}'")
|
|
183
|
+
|
|
184
|
+
def delete_table(self, app_id: str, table_id: str) -> dict:
|
|
185
|
+
"""Delete a table from a Quickbase app.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
app_id: The ID of the app containing the table.
|
|
189
|
+
table_id: The ID of the table to delete.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
The API response body.
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
QuickbaseAPIError: If the API request fails.
|
|
196
|
+
"""
|
|
197
|
+
resp_json, resp_code = self._rm.delete(
|
|
198
|
+
f"tables/{table_id}",
|
|
199
|
+
params={"appId": app_id},
|
|
200
|
+
)
|
|
201
|
+
self._require_success(resp_json, resp_code, f"delete table '{table_id}'")
|
|
202
|
+
logger.info("Deleted table %s from app %s", table_id, app_id)
|
|
203
|
+
return resp_json
|
|
204
|
+
|
|
205
|
+
# ── Reports ───────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
def get_reports(self, table_id: str) -> list[dict]:
|
|
208
|
+
"""Get all reports for a table.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
table_id: The ID of the table.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
A list of report metadata dictionaries.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
QuickbaseAPIError: If the API request fails.
|
|
218
|
+
"""
|
|
219
|
+
resp_json, resp_code = self._rm.get("reports", params={"tableId": table_id})
|
|
220
|
+
self._require_success(resp_json, resp_code, f"get reports for table '{table_id}'")
|
|
221
|
+
return resp_json
|
|
222
|
+
|
|
223
|
+
def get_report(self, table_id: str, report_id: str) -> dict:
|
|
224
|
+
"""Get metadata for a specific report.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
table_id: The ID of the table containing the report.
|
|
228
|
+
report_id: The ID of the report.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
A dictionary of report metadata.
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
QuickbaseAPIError: If the API request fails.
|
|
235
|
+
"""
|
|
236
|
+
resp_json, resp_code = self._rm.get(
|
|
237
|
+
f"reports/{report_id}",
|
|
238
|
+
params={"tableId": table_id},
|
|
239
|
+
)
|
|
240
|
+
self._require_success(resp_json, resp_code, f"get report '{report_id}'")
|
|
241
|
+
return resp_json
|
|
242
|
+
|
|
243
|
+
def run_report(
|
|
244
|
+
self,
|
|
245
|
+
table_id: str,
|
|
246
|
+
report_id: str,
|
|
247
|
+
skip: int = None,
|
|
248
|
+
top: int = None,
|
|
249
|
+
) -> dict:
|
|
250
|
+
"""Run a report and return its results.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
table_id: The ID of the table containing the report.
|
|
254
|
+
report_id: The ID of the report to run.
|
|
255
|
+
skip: Number of records to skip (for pagination).
|
|
256
|
+
top: Maximum number of records to return.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
The report results.
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
QuickbaseAPIError: If the API request fails.
|
|
263
|
+
"""
|
|
264
|
+
params = {"tableId": table_id}
|
|
265
|
+
if skip is not None:
|
|
266
|
+
params["skip"] = skip
|
|
267
|
+
if top is not None:
|
|
268
|
+
params["top"] = top
|
|
269
|
+
|
|
270
|
+
resp_json, resp_code = self._rm.post(
|
|
271
|
+
f"reports/{report_id}/run",
|
|
272
|
+
params=params,
|
|
273
|
+
)
|
|
274
|
+
self._require_success(resp_json, resp_code, f"run report '{report_id}'")
|
|
275
|
+
return resp_json
|
|
276
|
+
|
|
277
|
+
# ── Fields ────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
def create_field(self, table_id: str, properties: dict) -> dict:
|
|
280
|
+
"""Create a new field in a table.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
table_id: The ID of the table.
|
|
284
|
+
properties: Field properties (must include 'label' and 'fieldType').
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
The API response body with the created field's metadata.
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
QuickbaseAPIError: If the API request fails.
|
|
291
|
+
"""
|
|
292
|
+
resp_json, resp_code = self._rm.post(
|
|
293
|
+
"fields",
|
|
294
|
+
params={"tableId": table_id},
|
|
295
|
+
data=properties,
|
|
296
|
+
)
|
|
297
|
+
self._require_success(resp_json, resp_code, f"create field in table '{table_id}'")
|
|
298
|
+
logger.info(
|
|
299
|
+
"Created field '%s' (ID: %s) in table %s",
|
|
300
|
+
resp_json["label"],
|
|
301
|
+
resp_json["id"],
|
|
302
|
+
table_id,
|
|
303
|
+
)
|
|
304
|
+
return resp_json
|
|
305
|
+
|
|
306
|
+
def create_fields(self, table_id: str, fields: list[dict]) -> dict:
|
|
307
|
+
"""Create multiple fields in a table.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
table_id: The ID of the table.
|
|
311
|
+
fields: A list of field property dictionaries.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
A dict with 'succeeded' and 'failed' lists.
|
|
315
|
+
"""
|
|
316
|
+
results = {"succeeded": [], "failed": []}
|
|
317
|
+
for field in fields:
|
|
318
|
+
try:
|
|
319
|
+
created = self.create_field(table_id, field)
|
|
320
|
+
results["succeeded"].append(created)
|
|
321
|
+
except QuickbaseAPIError as e:
|
|
322
|
+
logger.warning("Failed to create field %s: %s", field, e)
|
|
323
|
+
results["failed"].append({"field": field, "error": str(e)})
|
|
324
|
+
|
|
325
|
+
logger.info(
|
|
326
|
+
"%d of %d fields created for table %s",
|
|
327
|
+
len(results["succeeded"]),
|
|
328
|
+
len(fields),
|
|
329
|
+
table_id,
|
|
330
|
+
)
|
|
331
|
+
return results
|
|
332
|
+
|
|
333
|
+
def get_table_fields(self, table_id: str) -> list[dict]:
|
|
334
|
+
"""Get all fields for a table.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
table_id: The ID of the table.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
A list of field metadata dictionaries.
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
QuickbaseAPIError: If the API request fails.
|
|
344
|
+
"""
|
|
345
|
+
resp_json, resp_code = self._rm.get("fields", params={"tableId": table_id})
|
|
346
|
+
self._require_success(resp_json, resp_code, f"get fields for table '{table_id}'")
|
|
347
|
+
return resp_json
|
|
348
|
+
|
|
349
|
+
def get_field_id(self, table_id: str, field_label: str) -> str:
|
|
350
|
+
"""Find a field ID by its label.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
table_id: The ID of the table.
|
|
354
|
+
field_label: The exact label of the field to find.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
The field ID as a string.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
QuickbaseNotFoundError: If no field with the given label is found.
|
|
361
|
+
"""
|
|
362
|
+
fields = self.get_table_fields(table_id)
|
|
363
|
+
for field in fields:
|
|
364
|
+
if field["label"] == field_label:
|
|
365
|
+
return str(field["id"])
|
|
366
|
+
raise QuickbaseNotFoundError(f"No field labeled '{field_label}' in table '{table_id}'")
|
|
367
|
+
|
|
368
|
+
def get_field_label_id_map(self, table_id: str) -> dict[str, str]:
|
|
369
|
+
"""Build a mapping of field labels to field IDs.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
table_id: The ID of the table.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
A dict mapping field label strings to field ID strings.
|
|
376
|
+
"""
|
|
377
|
+
fields = self.get_table_fields(table_id)
|
|
378
|
+
return {f["label"]: str(f["id"]) for f in fields}
|
|
379
|
+
|
|
380
|
+
def get_key_field_id(self, app_id: str, table_id: str) -> str:
|
|
381
|
+
"""Get the key field ID for a table.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
app_id: The ID of the app.
|
|
385
|
+
table_id: The ID of the table.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
The key field ID as a string.
|
|
389
|
+
|
|
390
|
+
Raises:
|
|
391
|
+
QuickbaseAPIError: If the API request fails.
|
|
392
|
+
"""
|
|
393
|
+
resp_json, resp_code = self._rm.get(
|
|
394
|
+
f"tables/{table_id}",
|
|
395
|
+
params={"appId": app_id},
|
|
396
|
+
)
|
|
397
|
+
self._require_success(resp_json, resp_code, f"get key field for table '{table_id}'")
|
|
398
|
+
return str(resp_json["keyFieldId"])
|
|
399
|
+
|
|
400
|
+
def set_key_field(self, table_id: str, field_id: str) -> bool:
|
|
401
|
+
"""Set the key field for a table using the legacy API.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
table_id: The ID of the table.
|
|
405
|
+
field_id: The ID of the field to set as the key field.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
True if the key field was set successfully.
|
|
409
|
+
|
|
410
|
+
Raises:
|
|
411
|
+
QuickbaseAPIError: If the API request fails.
|
|
412
|
+
"""
|
|
413
|
+
url = f"https://{self.realm}/db/{table_id}"
|
|
414
|
+
params = {"a": "API_SetKeyField", "fid": field_id}
|
|
415
|
+
|
|
416
|
+
resp_json, resp_code = self._lm.post(url, params=params)
|
|
417
|
+
if resp_code != 200:
|
|
418
|
+
raise QuickbaseAPIError(resp_code, resp_json)
|
|
419
|
+
|
|
420
|
+
logger.info(
|
|
421
|
+
"Set key field to %s for table %s",
|
|
422
|
+
field_id,
|
|
423
|
+
table_id,
|
|
424
|
+
)
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
def set_required_field(self, table_id: str, field_id: str) -> bool:
|
|
428
|
+
"""Mark a field as required.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
table_id: The ID of the table.
|
|
432
|
+
field_id: The ID of the field to mark as required.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
True if the field was successfully marked as required.
|
|
436
|
+
|
|
437
|
+
Raises:
|
|
438
|
+
QuickbaseAPIError: If the API request fails.
|
|
439
|
+
"""
|
|
440
|
+
resp_json, resp_code = self._rm.post(
|
|
441
|
+
f"fields/{field_id}",
|
|
442
|
+
params={"tableId": table_id},
|
|
443
|
+
data={"required": True},
|
|
444
|
+
)
|
|
445
|
+
self._require_success(resp_json, resp_code, f"set field '{field_id}' as required")
|
|
446
|
+
return True
|
|
447
|
+
|
|
448
|
+
# ── Records ───────────────────────────────────────────────────────────
|
|
449
|
+
|
|
450
|
+
def upsert_records(self, table_id: str, data: list[dict]) -> dict:
|
|
451
|
+
"""Insert or update records in a table.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
table_id: The ID of the table.
|
|
455
|
+
data: A list of record dictionaries, keyed by field ID::
|
|
456
|
+
|
|
457
|
+
[
|
|
458
|
+
{
|
|
459
|
+
"6": {"value": "Some text"},
|
|
460
|
+
"7": {"value": 42},
|
|
461
|
+
"8": {"value": "2024-01-15T08:00:00Z"},
|
|
462
|
+
}
|
|
463
|
+
]
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
A dict with keys: processed, created, updated, unchanged,
|
|
467
|
+
errored, errored_details.
|
|
468
|
+
|
|
469
|
+
Raises:
|
|
470
|
+
QuickbaseAPIError: If the API request fails entirely (4xx/5xx).
|
|
471
|
+
"""
|
|
472
|
+
resp_json, resp_code = self._rm.post(
|
|
473
|
+
"records",
|
|
474
|
+
data={"to": table_id, "data": data},
|
|
475
|
+
)
|
|
476
|
+
if resp_code not in (200, 207):
|
|
477
|
+
raise QuickbaseAPIError(resp_code, resp_json)
|
|
478
|
+
|
|
479
|
+
metadata = resp_json["metadata"]
|
|
480
|
+
line_errors = metadata.get("lineErrors", {})
|
|
481
|
+
|
|
482
|
+
result = {
|
|
483
|
+
"processed": metadata["totalNumberOfRecordsProcessed"] - len(line_errors),
|
|
484
|
+
"created": len(metadata["createdRecordIds"]),
|
|
485
|
+
"updated": len(metadata["updatedRecordIds"]),
|
|
486
|
+
"unchanged": len(metadata["unchangedRecordIds"]),
|
|
487
|
+
"errored": len(line_errors),
|
|
488
|
+
"errored_details": line_errors,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if line_errors:
|
|
492
|
+
logger.warning(
|
|
493
|
+
"Upsert to table %s had %d line errors: %s",
|
|
494
|
+
table_id,
|
|
495
|
+
len(line_errors),
|
|
496
|
+
line_errors,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
return result
|
|
500
|
+
|
|
501
|
+
def delete_records(self, table_id: str, where: str) -> int:
|
|
502
|
+
"""Delete records from a table.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
table_id: The ID of the table.
|
|
506
|
+
where: A Quickbase query string (e.g., "{6.EX.'123'}").
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
The number of records deleted.
|
|
510
|
+
|
|
511
|
+
Raises:
|
|
512
|
+
QuickbaseAPIError: If the API request fails.
|
|
513
|
+
"""
|
|
514
|
+
resp_json, resp_code = self._rm.delete(
|
|
515
|
+
"records",
|
|
516
|
+
data={"from": table_id, "where": where},
|
|
517
|
+
)
|
|
518
|
+
self._require_success(resp_json, resp_code, f"delete records from table '{table_id}'")
|
|
519
|
+
deleted = resp_json["numberDeleted"]
|
|
520
|
+
logger.info("Deleted %d records from table %s", deleted, table_id)
|
|
521
|
+
return deleted
|
|
522
|
+
|
|
523
|
+
def query_for_data(
|
|
524
|
+
self,
|
|
525
|
+
table_id: str,
|
|
526
|
+
select: list[int] = None,
|
|
527
|
+
where: str = None,
|
|
528
|
+
sort_by: list[dict] = None,
|
|
529
|
+
group_by: list[dict] = None,
|
|
530
|
+
options: dict = None,
|
|
531
|
+
) -> dict:
|
|
532
|
+
"""Query for record data from a table.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
table_id: The ID of the table to query.
|
|
536
|
+
select: List of field IDs to return. If omitted, returns
|
|
537
|
+
fields from the default report.
|
|
538
|
+
where: A Quickbase query string (e.g., "{12.EX.'VPF'}").
|
|
539
|
+
sort_by: Sort order, e.g., [{"fieldId": 6, "order": "ASC"}].
|
|
540
|
+
group_by: Grouping, e.g., [{"fieldId": 6, "grouping": "equal-values"}].
|
|
541
|
+
options: Additional options, e.g.,
|
|
542
|
+
{"skip": 0, "top": 100, "compareWithAppLocalTime": False}.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
The query response including data and metadata.
|
|
546
|
+
|
|
547
|
+
Raises:
|
|
548
|
+
QuickbaseAPIError: If the API request fails.
|
|
549
|
+
"""
|
|
550
|
+
body = {"from": table_id}
|
|
551
|
+
if select is not None:
|
|
552
|
+
body["select"] = select
|
|
553
|
+
if where is not None:
|
|
554
|
+
body["where"] = where
|
|
555
|
+
if sort_by is not None:
|
|
556
|
+
body["sortBy"] = sort_by
|
|
557
|
+
if group_by is not None:
|
|
558
|
+
body["groupBy"] = group_by
|
|
559
|
+
if options is not None:
|
|
560
|
+
body["options"] = options
|
|
561
|
+
|
|
562
|
+
resp_json, resp_code = self._rm.post("records/query", data=body)
|
|
563
|
+
self._require_success(resp_json, resp_code, f"query table '{table_id}'")
|
|
564
|
+
return resp_json
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Custom exceptions for the quickbase-api package."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class QuickbaseError(Exception):
|
|
5
|
+
"""Base exception for all Quickbase errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class QuickbaseAPIError(QuickbaseError):
|
|
9
|
+
"""Raised when the Quickbase API returns an unexpected status code."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, status_code: int, response_body: dict):
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
self.response_body = response_body
|
|
14
|
+
message = response_body.get("message", response_body)
|
|
15
|
+
super().__init__(f"Quickbase API error {status_code}: {message}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QuickbaseNotFoundError(QuickbaseError):
|
|
19
|
+
"""Raised when a requested resource (table, field, etc.) is not found."""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Client for Quickbase's legacy XML/URL-based API endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from quickbase_api.session import create_session
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LegacyMethod:
|
|
13
|
+
"""Low-level HTTP client for legacy Quickbase API endpoints.
|
|
14
|
+
|
|
15
|
+
Used for older API actions (e.g., API_SetKeyField) that are not
|
|
16
|
+
available through the modern REST API.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, realm: str, user_token: str):
|
|
20
|
+
self.realm = realm
|
|
21
|
+
self.session = create_session(realm, user_token)
|
|
22
|
+
|
|
23
|
+
def make_request(
|
|
24
|
+
self,
|
|
25
|
+
method: str,
|
|
26
|
+
url: str,
|
|
27
|
+
params: dict = None,
|
|
28
|
+
) -> tuple[dict, int]:
|
|
29
|
+
"""Execute an HTTP request against a legacy Quickbase endpoint.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
method: HTTP method (GET, POST, DELETE).
|
|
33
|
+
url: Full URL for the legacy endpoint.
|
|
34
|
+
params: Optional query string parameters.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
A tuple of (response_body, status_code).
|
|
38
|
+
"""
|
|
39
|
+
response = self.session.request(method=method, url=url, params=params)
|
|
40
|
+
try:
|
|
41
|
+
body = response.json()
|
|
42
|
+
except ValueError:
|
|
43
|
+
body = {"raw_response": response.text}
|
|
44
|
+
|
|
45
|
+
if response.status_code >= 400:
|
|
46
|
+
logger.error(
|
|
47
|
+
"Legacy API error %d on %s %s: %s",
|
|
48
|
+
response.status_code,
|
|
49
|
+
method,
|
|
50
|
+
url,
|
|
51
|
+
body,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return body, response.status_code
|
|
55
|
+
|
|
56
|
+
def get(self, url: str, params: dict = None) -> tuple[dict, int]:
|
|
57
|
+
"""Send a GET request."""
|
|
58
|
+
return self.make_request("GET", url, params=params)
|
|
59
|
+
|
|
60
|
+
def post(self, url: str, params: dict = None) -> tuple[dict, int]:
|
|
61
|
+
"""Send a POST request."""
|
|
62
|
+
return self.make_request("POST", url, params=params)
|
|
63
|
+
|
|
64
|
+
def delete(self, url: str, params: dict = None) -> tuple[dict, int]:
|
|
65
|
+
"""Send a DELETE request."""
|
|
66
|
+
return self.make_request("DELETE", url, params=params)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Client for Quickbase's RESTful JSON API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from quickbase_api.session import create_session
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
BASE_URL = "https://api.quickbase.com/v1"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RestMethod:
|
|
15
|
+
"""Low-level HTTP client for the Quickbase REST API.
|
|
16
|
+
|
|
17
|
+
Handles URL construction, request execution, and response parsing
|
|
18
|
+
for all REST API endpoints.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, realm: str, user_token: str):
|
|
22
|
+
self.session = create_session(realm, user_token)
|
|
23
|
+
|
|
24
|
+
def make_request(
|
|
25
|
+
self,
|
|
26
|
+
method: str,
|
|
27
|
+
endpoint: str,
|
|
28
|
+
params: dict = None,
|
|
29
|
+
data: dict = None,
|
|
30
|
+
) -> tuple[dict, int]:
|
|
31
|
+
"""Execute an HTTP request against the Quickbase REST API.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
method: HTTP method (GET, POST, DELETE).
|
|
35
|
+
endpoint: API endpoint path (appended to base URL).
|
|
36
|
+
params: Optional query string parameters.
|
|
37
|
+
data: Optional JSON request body.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A tuple of (response_body, status_code).
|
|
41
|
+
"""
|
|
42
|
+
url = f"{BASE_URL}/{endpoint}"
|
|
43
|
+
response = self.session.request(
|
|
44
|
+
method=method,
|
|
45
|
+
url=url,
|
|
46
|
+
params=params,
|
|
47
|
+
json=data,
|
|
48
|
+
)
|
|
49
|
+
try:
|
|
50
|
+
body = response.json()
|
|
51
|
+
except ValueError:
|
|
52
|
+
body = {"raw_response": response.text}
|
|
53
|
+
|
|
54
|
+
if response.status_code >= 400:
|
|
55
|
+
logger.error(
|
|
56
|
+
"REST API error %d on %s %s: %s",
|
|
57
|
+
response.status_code,
|
|
58
|
+
method,
|
|
59
|
+
endpoint,
|
|
60
|
+
body,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return body, response.status_code
|
|
64
|
+
|
|
65
|
+
def get(self, endpoint: str, params: dict = None) -> tuple[dict, int]:
|
|
66
|
+
"""Send a GET request."""
|
|
67
|
+
return self.make_request("GET", endpoint, params=params)
|
|
68
|
+
|
|
69
|
+
def post(self, endpoint: str, params: dict = None, data: dict = None) -> tuple[dict, int]:
|
|
70
|
+
"""Send a POST request."""
|
|
71
|
+
return self.make_request("POST", endpoint, params=params, data=data)
|
|
72
|
+
|
|
73
|
+
def delete(self, endpoint: str, params: dict = None, data: dict = None) -> tuple[dict, int]:
|
|
74
|
+
"""Send a DELETE request."""
|
|
75
|
+
return self.make_request("DELETE", endpoint, params=params, data=data)
|
quickbase_api/session.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Shared HTTP session factory for Quickbase API clients."""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from requests.adapters import HTTPAdapter, Retry
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_session(realm: str, user_token: str) -> requests.Session:
|
|
8
|
+
"""Create a configured requests session with retry logic.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
realm: The Quickbase realm hostname (e.g., 'mycompany.quickbase.com').
|
|
12
|
+
user_token: The Quickbase user token for authentication.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
A configured requests.Session with retries and auth headers.
|
|
16
|
+
"""
|
|
17
|
+
session = requests.Session()
|
|
18
|
+
retries = Retry(
|
|
19
|
+
total=5,
|
|
20
|
+
backoff_factor=1,
|
|
21
|
+
status_forcelist=[429, 502, 503, 504],
|
|
22
|
+
respect_retry_after_header=True,
|
|
23
|
+
)
|
|
24
|
+
session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
25
|
+
session.headers.update(
|
|
26
|
+
{
|
|
27
|
+
"QB-Realm-Hostname": realm,
|
|
28
|
+
"Authorization": f"QB-USER-TOKEN {user_token}",
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
return session
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quickbase-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple Python wrapper for the Quickbase API
|
|
5
|
+
Project-URL: Homepage, https://github.com/tbrezler/quickbase-api
|
|
6
|
+
Project-URL: Issues, https://github.com/tbrezler/quickbase-api/issues
|
|
7
|
+
Project-URL: Documentation, https://github.com/tbrezler/quickbase-api#readme
|
|
8
|
+
Author-email: Tyler Brezler <tbrezler@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
|
+
Keywords: api,database,quickbase,rest,wrapper
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: requests>=2.25.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: responses>=0.20; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# quickbase-api
|
|
32
|
+
|
|
33
|
+
A simple Python wrapper for the [Quickbase API](https://developer.quickbase.com/).
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/quickbase-api/)
|
|
36
|
+
[](https://pypi.org/project/quickbase-api/)
|
|
37
|
+
[](https://opensource.org/licenses/MIT)
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install quickbase-api
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import quickbase_api
|
|
49
|
+
|
|
50
|
+
# Option 1: Pass credentials directly
|
|
51
|
+
client = quickbase_api.client(
|
|
52
|
+
realm="mycompany.quickbase.com",
|
|
53
|
+
user_token="your-user-token",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Option 2: Use environment variables
|
|
57
|
+
# export QUICKBASE_REALM=mycompany.quickbase.com
|
|
58
|
+
# export QUICKBASE_USER_TOKEN=your-user-token
|
|
59
|
+
client = quickbase_api.client()
|
|
60
|
+
|
|
61
|
+
# Query records from a table
|
|
62
|
+
records = client.query_for_data(
|
|
63
|
+
table_id="bqrg4xyza",
|
|
64
|
+
select=[3, 6, 7],
|
|
65
|
+
where="{6.EX.'Active'}",
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Authentication
|
|
70
|
+
|
|
71
|
+
This library uses [Quickbase user tokens](https://developer.quickbase.com/auth) for
|
|
72
|
+
authentication. You can pass your credentials directly or set environment variables:
|
|
73
|
+
|
|
74
|
+
| Environment Variable | Description |
|
|
75
|
+
|--------------------------|--------------------------------------------------------|
|
|
76
|
+
| `QUICKBASE_REALM` | Your Quickbase realm (e.g., `mycompany.quickbase.com`) |
|
|
77
|
+
| `QUICKBASE_USER_TOKEN` | Your Quickbase user token |
|
|
78
|
+
|
|
79
|
+
> **Security note:** Never commit tokens to source control. Use environment variables
|
|
80
|
+
> or a secrets manager.
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
### Apps
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# Create an app
|
|
88
|
+
app_id = client.create_app(
|
|
89
|
+
name="My App",
|
|
90
|
+
description="An example app",
|
|
91
|
+
assign_token=True,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Get app metadata
|
|
95
|
+
app = client.get_app(app_id)
|
|
96
|
+
|
|
97
|
+
# Delete an app
|
|
98
|
+
client.delete_app("My App", app_id)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Tables
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# Create a table
|
|
105
|
+
table_id = client.create_table(app_id, {
|
|
106
|
+
"name": "Customers",
|
|
107
|
+
"description": "Customer records",
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
# List all tables in an app
|
|
111
|
+
tables = client.get_tables(app_id)
|
|
112
|
+
|
|
113
|
+
# Find a table ID by name
|
|
114
|
+
table_id = client.get_table_id(app_id, "Customers")
|
|
115
|
+
|
|
116
|
+
# Delete a table
|
|
117
|
+
client.delete_table(app_id, table_id)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Fields
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# Create a field
|
|
124
|
+
client.create_field(table_id, {
|
|
125
|
+
"label": "Full Name",
|
|
126
|
+
"fieldType": "text",
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
# Create multiple fields at once
|
|
130
|
+
results = client.create_fields(table_id, [
|
|
131
|
+
{"label": "Email", "fieldType": "email"},
|
|
132
|
+
{"label": "Age", "fieldType": "numeric"},
|
|
133
|
+
{"label": "Active", "fieldType": "checkbox"},
|
|
134
|
+
])
|
|
135
|
+
print(f"Created: {len(results['succeeded'])}, Failed: {len(results['failed'])}")
|
|
136
|
+
|
|
137
|
+
# List all fields in a table
|
|
138
|
+
fields = client.get_table_fields(table_id)
|
|
139
|
+
|
|
140
|
+
# Find a field ID by label
|
|
141
|
+
field_id = client.get_field_id(table_id, "Full Name")
|
|
142
|
+
|
|
143
|
+
# Get a mapping of all field labels to IDs
|
|
144
|
+
field_map = client.get_field_label_id_map(table_id)
|
|
145
|
+
# {"Full Name": "6", "Email": "7", "Age": "8", "Active": "9"}
|
|
146
|
+
|
|
147
|
+
# Set a key field (uses legacy API)
|
|
148
|
+
client.set_key_field(table_id, field_id)
|
|
149
|
+
|
|
150
|
+
# Mark a field as required
|
|
151
|
+
client.set_required_field(table_id, field_id)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Records
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Upsert (insert or update) records
|
|
158
|
+
result = client.upsert_records(table_id, [
|
|
159
|
+
{
|
|
160
|
+
"6": {"value": "Jane Smith"},
|
|
161
|
+
"7": {"value": "jane@example.com"},
|
|
162
|
+
"8": {"value": 32},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"6": {"value": "Bob Jones"},
|
|
166
|
+
"7": {"value": "bob@example.com"},
|
|
167
|
+
"8": {"value": 45},
|
|
168
|
+
},
|
|
169
|
+
])
|
|
170
|
+
print(f"Created: {result['created']}, Updated: {result['updated']}")
|
|
171
|
+
|
|
172
|
+
# Check for partial failures
|
|
173
|
+
if result["errored"] > 0:
|
|
174
|
+
print(f"Errors: {result['errored_details']}")
|
|
175
|
+
|
|
176
|
+
# Query for records
|
|
177
|
+
records = client.query_for_data(
|
|
178
|
+
table_id=table_id,
|
|
179
|
+
select=[6, 7, 8],
|
|
180
|
+
where="{8.GT.30}",
|
|
181
|
+
sort_by=[{"fieldId": 6, "order": "ASC"}],
|
|
182
|
+
options={"skip": 0, "top": 100},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Delete records
|
|
186
|
+
deleted = client.delete_records(table_id, where="{8.LT.18}")
|
|
187
|
+
print(f"Deleted {deleted} records")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Reports
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# List all reports for a table
|
|
194
|
+
reports = client.get_reports(table_id)
|
|
195
|
+
|
|
196
|
+
# Get a specific report
|
|
197
|
+
report = client.get_report(table_id, report_id="1")
|
|
198
|
+
|
|
199
|
+
# Run a report
|
|
200
|
+
results = client.run_report(table_id, report_id="1", skip=0, top=100)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Error Handling
|
|
204
|
+
|
|
205
|
+
The library raises specific exceptions that you can catch and handle:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from quickbase_api import QuickbaseAPIError, QuickbaseNotFoundError
|
|
209
|
+
|
|
210
|
+
# Handle API errors
|
|
211
|
+
try:
|
|
212
|
+
app = client.get_app("invalid-id")
|
|
213
|
+
except QuickbaseAPIError as e:
|
|
214
|
+
print(f"API error {e.status_code}: {e.response_body}")
|
|
215
|
+
|
|
216
|
+
# Handle not-found lookups
|
|
217
|
+
try:
|
|
218
|
+
table_id = client.get_table_id(app_id, "Nonexistent Table")
|
|
219
|
+
except QuickbaseNotFoundError as e:
|
|
220
|
+
print(f"Not found: {e}")
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Exception hierarchy:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
QuickbaseError # Base exception
|
|
227
|
+
├── QuickbaseAPIError # HTTP errors from the API
|
|
228
|
+
└── QuickbaseNotFoundError # Resource not found in lookup methods
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Logging
|
|
232
|
+
|
|
233
|
+
This library uses Python's built-in `logging` module. To see log output,
|
|
234
|
+
configure logging in your application:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
import logging
|
|
238
|
+
|
|
239
|
+
# See all quickbase-api log messages
|
|
240
|
+
logging.basicConfig(level=logging.INFO)
|
|
241
|
+
|
|
242
|
+
# Or configure just the quickbase_api logger
|
|
243
|
+
logging.getLogger("quickbase_api").setLevel(logging.DEBUG)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Configuration
|
|
247
|
+
|
|
248
|
+
### Retry Behavior
|
|
249
|
+
|
|
250
|
+
The library automatically retries failed requests up to 5 times with
|
|
251
|
+
exponential backoff for the following HTTP status codes:
|
|
252
|
+
|
|
253
|
+
- `429` — Rate limited
|
|
254
|
+
- `502` — Bad gateway
|
|
255
|
+
- `503` — Service unavailable
|
|
256
|
+
- `504` — Gateway timeout
|
|
257
|
+
|
|
258
|
+
## Requirements
|
|
259
|
+
|
|
260
|
+
- Python 3.9+
|
|
261
|
+
- [requests](https://requests.readthedocs.io/) >= 2.25.0
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
quickbase_api/__init__.py,sha256=zkWcjGbkVh3_MJbfTY7_hCGsA9bTObXF2ktFx0XTcY0,1790
|
|
2
|
+
quickbase_api/api.py,sha256=t-1DFUpufgsGLjm62TFodLHd6_Ch8yS9Bpotq2pN2pE,18922
|
|
3
|
+
quickbase_api/exceptions.py,sha256=DZcoNLoRVDadwZW_wZuqRn8lOfvGV3pIBqS3Z8bTQj8,667
|
|
4
|
+
quickbase_api/legacy_method.py,sha256=4_tAD1gToTzBnbhHRc8WwgLCW8teVzXOzHekEaCdsNk,2003
|
|
5
|
+
quickbase_api/rest_method.py,sha256=H4WhGG6KVxDdGFhipWvOa1sJ-E0i47cxJnX9NWf1rXg,2270
|
|
6
|
+
quickbase_api/session.py,sha256=R2kaUebgYhgZDXQx_xF-vS2VNHkejE6xkK-EtNhZJh0,982
|
|
7
|
+
quickbase_api-0.1.0.dist-info/METADATA,sha256=u48PTsFwYTJnwDBB8HdJRcIrAjrK-xgGiB8TKxl2hGI,7019
|
|
8
|
+
quickbase_api-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
quickbase_api-0.1.0.dist-info/licenses/LICENSE.txt,sha256=gMsqXSha8Ek0_Q99VdMgsO-Vr1mmWSqSIHmM9WxJodA,1070
|
|
10
|
+
quickbase_api-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tyler Brezler
|
|
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.
|