pyquickbase 0.1.0a1__tar.gz → 0.2.0a1__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.
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/.github/workflows/ci.yml +2 -0
- pyquickbase-0.2.0a1/.github/workflows/docs.yml +37 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/.gitignore +1 -0
- pyquickbase-0.2.0a1/CHANGELOG.md +65 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/CONTRIBUTING.md +5 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/PKG-INFO +14 -9
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/README.md +12 -8
- pyquickbase-0.2.0a1/docs/api-notes.md +303 -0
- pyquickbase-0.2.0a1/docs/changelog.md +1 -0
- pyquickbase-0.2.0a1/docs/getting-started.md +68 -0
- pyquickbase-0.2.0a1/docs/guides/clients.md +74 -0
- pyquickbase-0.2.0a1/docs/guides/codegen.md +102 -0
- pyquickbase-0.2.0a1/docs/guides/errors.md +66 -0
- pyquickbase-0.2.0a1/docs/guides/files.md +97 -0
- pyquickbase-0.2.0a1/docs/guides/formulas.md +75 -0
- pyquickbase-0.2.0a1/docs/guides/reading.md +163 -0
- pyquickbase-0.2.0a1/docs/guides/schema.md +47 -0
- pyquickbase-0.2.0a1/docs/guides/writing.md +83 -0
- pyquickbase-0.2.0a1/docs/index.md +45 -0
- pyquickbase-0.2.0a1/docs/reference.md +147 -0
- pyquickbase-0.2.0a1/mkdocs.yml +74 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/pyproject.toml +10 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/__init__.py +44 -4
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_async/_client.py +3 -1
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_async/_resources.py +279 -8
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_async/_transport.py +14 -1
- pyquickbase-0.2.0a1/src/pyquickbase/_cli.py +87 -0
- pyquickbase-0.2.0a1/src/pyquickbase/_codegen.py +190 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_config.py +5 -2
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_models.py +155 -5
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_response.py +5 -1
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_sync/_client.py +3 -1
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_sync/_resources.py +277 -8
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_sync/_transport.py +14 -1
- pyquickbase-0.2.0a1/src/pyquickbase/_typed.py +154 -0
- pyquickbase-0.2.0a1/src/pyquickbase/_version.py +1 -0
- pyquickbase-0.2.0a1/src/pyquickbase/_writes.py +196 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/errors.py +12 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/tests/_async/test_client.py +51 -2
- pyquickbase-0.2.0a1/tests/_async/test_files.py +255 -0
- pyquickbase-0.2.0a1/tests/_async/test_formulas.py +167 -0
- pyquickbase-0.2.0a1/tests/_async/test_reports.py +297 -0
- pyquickbase-0.2.0a1/tests/_async/test_typed.py +237 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/tests/_sync/test_client.py +51 -2
- pyquickbase-0.2.0a1/tests/_sync/test_files.py +252 -0
- pyquickbase-0.2.0a1/tests/_sync/test_formulas.py +166 -0
- pyquickbase-0.2.0a1/tests/_sync/test_reports.py +292 -0
- pyquickbase-0.2.0a1/tests/_sync/test_typed.py +236 -0
- pyquickbase-0.2.0a1/tests/test_codegen.py +500 -0
- pyquickbase-0.2.0a1/tests/test_docs.py +38 -0
- pyquickbase-0.2.0a1/tests/test_files.py +135 -0
- pyquickbase-0.2.0a1/tests/test_formulas.py +80 -0
- pyquickbase-0.2.0a1/uv.lock +1149 -0
- pyquickbase-0.1.0a1/CHANGELOG.md +0 -28
- pyquickbase-0.1.0a1/docs/api-notes.md +0 -97
- pyquickbase-0.1.0a1/src/pyquickbase/_version.py +0 -1
- pyquickbase-0.1.0a1/src/pyquickbase/_writes.py +0 -104
- pyquickbase-0.1.0a1/uv.lock +0 -463
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/.env.example +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/.github/workflows/release.yml +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/.python-version +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/LICENSE +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/SECURITY.md +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/scripts/generate_sync.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_async/__init__.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_auth.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_query.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_retry.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_schema.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/_sync/__init__.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/src/pyquickbase/py.typed +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/tests/__init__.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/tests/_async/test_resources.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/tests/_async/test_writes.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/tests/_sync/test_resources.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/tests/_sync/test_writes.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/tests/conftest.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/tests/test_core.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/tests/test_query.py +0 -0
- {pyquickbase-0.1.0a1 → pyquickbase-0.2.0a1}/tests/test_writes.py +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: pages
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
20
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
21
|
+
- run: uv run --locked --group docs mkdocs build
|
|
22
|
+
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
|
23
|
+
with:
|
|
24
|
+
path: site
|
|
25
|
+
|
|
26
|
+
deploy:
|
|
27
|
+
needs: build
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
permissions:
|
|
30
|
+
pages: write
|
|
31
|
+
id-token: write
|
|
32
|
+
environment:
|
|
33
|
+
name: github-pages
|
|
34
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
35
|
+
steps:
|
|
36
|
+
- id: deployment
|
|
37
|
+
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0a1] - 2026-11-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- A documentation site.
|
|
14
|
+
- Reports: `table.reports()`, `table.report()`, and `Report` with `run()`, `run_page()`, and
|
|
15
|
+
`info()`, plus `ReportInfo`, `UnknownReportError`, and `UnsupportedReportError`. Only table reports
|
|
16
|
+
can be read as records.
|
|
17
|
+
- File attachments: `FileUpload` for writes, plus `table.download_file()` and
|
|
18
|
+
`table.delete_file_version()`.
|
|
19
|
+
- `pyquickbase codegen`, which generates a typed class for each table, and `app.typed()`, which reads
|
|
20
|
+
and writes through those classes, including `query()`, `insert()`, `update()`, and `upsert()`. A
|
|
21
|
+
class that no longer matches its table raises `SchemaMismatchError`.
|
|
22
|
+
- Formulas: `table.run_formula()`, which returns a `FormulaResult`, `formula=` on `create_field()` and
|
|
23
|
+
`update_field()`, and `Field.formula`.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `read_only` and `allowed_writes` clients now allow running formulas, with `POST /formula/run`.
|
|
28
|
+
- A blank formula now raises `InvalidValueError` on `create_field()` and `update_field()`, including
|
|
29
|
+
through `properties`, because Quickbase accepts it on a formula field and empties the field's values.
|
|
30
|
+
- A file attachment field now accepts only a `FileUpload`, and raises `InvalidValueError` for anything
|
|
31
|
+
else, including `None` and hand-built dictionaries, because Quickbase silently accepts those and
|
|
32
|
+
keeps or loses the file.
|
|
33
|
+
- Writes are also split by size, to stay under Quickbase's 40 MiB request limit, so `batch_size` is
|
|
34
|
+
now an upper limit.
|
|
35
|
+
- `read_only` and `allowed_writes` clients now allow running reports, with
|
|
36
|
+
`POST /reports/{id}/run`. This adds to the report's usage count in Quickbase, but changes no
|
|
37
|
+
records.
|
|
38
|
+
- `Record.id` now explains what's missing when a report's records have no Record ID#. It still
|
|
39
|
+
raises `KeyError`.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- A rate limit error now includes Quickbase's message, which arrives in a different format.
|
|
44
|
+
- A README example that was missing an import.
|
|
45
|
+
|
|
46
|
+
## [0.1.0a1] - 2026-11-02
|
|
47
|
+
|
|
48
|
+
First alpha release.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- `Quickbase` and `AsyncQuickbase` clients with `request()` for calling any endpoint.
|
|
53
|
+
- `RetryPolicy`, with retries for HTTP 429 and connection failures.
|
|
54
|
+
- The `QuickbaseError` exception hierarchy.
|
|
55
|
+
- `qb.app()` and `app.table()`, with app, table, field, and relationship metadata and a schema cache.
|
|
56
|
+
- `F` for building queries by field label, with values escaped for you.
|
|
57
|
+
- Checks that reject broken `where` strings that Quickbase would otherwise accept.
|
|
58
|
+
- `table.query()` for automatic paging, plus `query_page()` and `count()`.
|
|
59
|
+
- `Record`, with access by label or ID and date and timestamp conversion.
|
|
60
|
+
- `read_only=True` on the clients, which refuses any request that could change data.
|
|
61
|
+
- `allowed_writes` on the clients, which limits them to reads plus the writes you list.
|
|
62
|
+
- `parse_where()` for inspecting a hand-written `where` string.
|
|
63
|
+
- `table.insert()`, `update()`, `upsert()`, and `delete()`, with per-row error reporting in `WriteError`.
|
|
64
|
+
- `app.create_table()`, `table.create_field()`, `update_field()`, `delete_fields()`,
|
|
65
|
+
`create_relationship()`, and `drop()`.
|
|
@@ -14,6 +14,11 @@ uv run pytest --cov
|
|
|
14
14
|
|
|
15
15
|
Tests use mocked HTTP and need no Quickbase account.
|
|
16
16
|
|
|
17
|
+
## Docs
|
|
18
|
+
|
|
19
|
+
Preview the docs site with `uv run --group docs mkdocs serve`. Every public name needs an entry in
|
|
20
|
+
`docs/reference.md`, and a test checks this.
|
|
21
|
+
|
|
17
22
|
## Sync and async code
|
|
18
23
|
|
|
19
24
|
**Only edit the async code.** `src/pyquickbase/_sync/` and `tests/_sync/` are generated from their
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyquickbase
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0a1
|
|
4
4
|
Summary: An unofficial, typed Python SDK for the Quickbase API, with sync and async clients.
|
|
5
|
+
Project-URL: Documentation, https://matthew-williams-solutions.github.io/pyquickbase/
|
|
5
6
|
Project-URL: Repository, https://github.com/matthew-williams-solutions/pyquickbase
|
|
6
7
|
Project-URL: Issues, https://github.com/matthew-williams-solutions/pyquickbase/issues
|
|
7
8
|
Project-URL: Changelog, https://github.com/matthew-williams-solutions/pyquickbase/blob/main/CHANGELOG.md
|
|
@@ -28,6 +29,8 @@ Description-Content-Type: text/markdown
|
|
|
28
29
|
An unofficial, typed Python SDK for the [Quickbase](https://www.quickbase.com/) API, with matching
|
|
29
30
|
sync and async clients.
|
|
30
31
|
|
|
32
|
+
**[Read the documentation](https://matthew-williams-solutions.github.io/pyquickbase/)**
|
|
33
|
+
|
|
31
34
|
> **Status: early alpha.** Anything may change before 1.0. This project is not affiliated with or endorsed by Quickbase, Inc.
|
|
32
35
|
|
|
33
36
|
## What works today
|
|
@@ -42,16 +45,16 @@ sync and async clients.
|
|
|
42
45
|
- Tokens kept out of `repr`s, exceptions, and logs, and never sent to other hosts or over plain HTTP
|
|
43
46
|
- Creating, updating, and deleting records, with errors reported for each failed row
|
|
44
47
|
- Creating and changing tables, fields, and relationships
|
|
48
|
+
- Running table reports, with automatic paging
|
|
49
|
+
- Uploading, downloading, and deleting file attachments
|
|
50
|
+
- Running formulas, and creating formula fields
|
|
51
|
+
- `pyquickbase codegen`, which generates typed classes from your tables
|
|
45
52
|
- `qb.request()` for calling any Quickbase endpoint directly
|
|
46
53
|
|
|
47
|
-
## Coming next
|
|
48
|
-
|
|
49
|
-
Reports, formulas, file attachments, temporary tokens, and generated typed tables.
|
|
50
|
-
|
|
51
54
|
## Usage
|
|
52
55
|
|
|
53
56
|
```bash
|
|
54
|
-
pip install pyquickbase==0.
|
|
57
|
+
pip install pyquickbase==0.2.0a1
|
|
55
58
|
```
|
|
56
59
|
|
|
57
60
|
This is an alpha release, so pin the exact version.
|
|
@@ -129,6 +132,7 @@ Rows are dictionaries keyed by field label or ID. Python dates, timezone-aware d
|
|
|
129
132
|
| `table.delete(where)` | Deletes matching records and returns the count. `where` is required |
|
|
130
133
|
|
|
131
134
|
```python
|
|
135
|
+
from datetime import date
|
|
132
136
|
from pyquickbase import F, WriteError
|
|
133
137
|
|
|
134
138
|
result = tasks.insert([{"Title": "Pour slab", "Status": "Not Started", "Due Date": date.today()}])
|
|
@@ -188,9 +192,10 @@ Quickbase may already have applied it, so check the data before trying again.
|
|
|
188
192
|
|
|
189
193
|
## Limiting what a client can do
|
|
190
194
|
|
|
191
|
-
Pass `read_only=True` to guarantee a client can't change data. It only sends GET requests
|
|
192
|
-
queries, including through `qb.request()`, and raises `ReadOnlyError`
|
|
193
|
-
contacting Quickbase.
|
|
195
|
+
Pass `read_only=True` to guarantee a client can't change data. It only sends GET requests, record
|
|
196
|
+
queries, report runs, and formula runs, including through `qb.request()`, and raises `ReadOnlyError`
|
|
197
|
+
for anything else without contacting Quickbase. Running a report adds to its usage count in Quickbase,
|
|
198
|
+
but changes no records.
|
|
194
199
|
|
|
195
200
|
To allow specific writes and nothing else, pass `allowed_writes={("POST", "/records")}` instead.
|
|
196
201
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
An unofficial, typed Python SDK for the [Quickbase](https://www.quickbase.com/) API, with matching
|
|
4
4
|
sync and async clients.
|
|
5
5
|
|
|
6
|
+
**[Read the documentation](https://matthew-williams-solutions.github.io/pyquickbase/)**
|
|
7
|
+
|
|
6
8
|
> **Status: early alpha.** Anything may change before 1.0. This project is not affiliated with or endorsed by Quickbase, Inc.
|
|
7
9
|
|
|
8
10
|
## What works today
|
|
@@ -17,16 +19,16 @@ sync and async clients.
|
|
|
17
19
|
- Tokens kept out of `repr`s, exceptions, and logs, and never sent to other hosts or over plain HTTP
|
|
18
20
|
- Creating, updating, and deleting records, with errors reported for each failed row
|
|
19
21
|
- Creating and changing tables, fields, and relationships
|
|
22
|
+
- Running table reports, with automatic paging
|
|
23
|
+
- Uploading, downloading, and deleting file attachments
|
|
24
|
+
- Running formulas, and creating formula fields
|
|
25
|
+
- `pyquickbase codegen`, which generates typed classes from your tables
|
|
20
26
|
- `qb.request()` for calling any Quickbase endpoint directly
|
|
21
27
|
|
|
22
|
-
## Coming next
|
|
23
|
-
|
|
24
|
-
Reports, formulas, file attachments, temporary tokens, and generated typed tables.
|
|
25
|
-
|
|
26
28
|
## Usage
|
|
27
29
|
|
|
28
30
|
```bash
|
|
29
|
-
pip install pyquickbase==0.
|
|
31
|
+
pip install pyquickbase==0.2.0a1
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
This is an alpha release, so pin the exact version.
|
|
@@ -104,6 +106,7 @@ Rows are dictionaries keyed by field label or ID. Python dates, timezone-aware d
|
|
|
104
106
|
| `table.delete(where)` | Deletes matching records and returns the count. `where` is required |
|
|
105
107
|
|
|
106
108
|
```python
|
|
109
|
+
from datetime import date
|
|
107
110
|
from pyquickbase import F, WriteError
|
|
108
111
|
|
|
109
112
|
result = tasks.insert([{"Title": "Pour slab", "Status": "Not Started", "Due Date": date.today()}])
|
|
@@ -163,9 +166,10 @@ Quickbase may already have applied it, so check the data before trying again.
|
|
|
163
166
|
|
|
164
167
|
## Limiting what a client can do
|
|
165
168
|
|
|
166
|
-
Pass `read_only=True` to guarantee a client can't change data. It only sends GET requests
|
|
167
|
-
queries, including through `qb.request()`, and raises `ReadOnlyError`
|
|
168
|
-
contacting Quickbase.
|
|
169
|
+
Pass `read_only=True` to guarantee a client can't change data. It only sends GET requests, record
|
|
170
|
+
queries, report runs, and formula runs, including through `qb.request()`, and raises `ReadOnlyError`
|
|
171
|
+
for anything else without contacting Quickbase. Running a report adds to its usage count in Quickbase,
|
|
172
|
+
but changes no records.
|
|
169
173
|
|
|
170
174
|
To allow specific writes and nothing else, pass `allowed_writes={("POST", "/records")}` instead.
|
|
171
175
|
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Quickbase API notes
|
|
2
|
+
|
|
3
|
+
What this SDK relies on, and how far each point has been confirmed. Much of the developer portal
|
|
4
|
+
renders in the browser and has no published spec that we've found, so check anything marked
|
|
5
|
+
"unverified" against a real app before depending on it.
|
|
6
|
+
|
|
7
|
+
## Confirmed
|
|
8
|
+
|
|
9
|
+
| Behavior | Source |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Headers: `QB-Realm-Hostname` (full hostname), `Authorization: QB-USER-TOKEN <token>` | Accepted by the live API |
|
|
12
|
+
| Error body: JSON with `message` and `description` | Live API, 2026-11-02 |
|
|
13
|
+
| `qb-api-ray` response header holds a request ID | Live API, 2026-11-02 |
|
|
14
|
+
| 100 requests per 10 seconds per user token for most endpoints; some differ | [Rate limiting docs](https://help.quickbase.com/docs/rate-limiting-overview.md) |
|
|
15
|
+
| On a 429, read `retry-after`; retry only for 429 | Same page |
|
|
16
|
+
| Operators: CT, XCT, HAS, XHAS, EX, TV, XTV, XEX, SW, XSW, BF, OBF, AF, OAF, IR, XIR, LT, LTE, GT, GTE, uppercase only | [Query docs](https://help.quickbase.com/docs/components-of-a-query) |
|
|
17
|
+
| Query dates: `yyyy-MM-dd` or milliseconds, not date-time strings | Same page |
|
|
18
|
+
| Query `metadata` has `numFields`, `numRecords`, `skip`, `totalRecords`, and `top` when it was sent | Live API, 2026-11-02 |
|
|
19
|
+
| Omitting `options` or sending `top: 0` returns all matches in a small table; a `skip` past the end returns no records | Live API, 2026-11-02 |
|
|
20
|
+
| `EX` on text ignores case | Live API, 2026-11-02 |
|
|
21
|
+
| `AND` and `OR` must be uppercase; whitespace around them is accepted | Live API, 2026-11-02 |
|
|
22
|
+
| Unquoted values such as `{3.EX.5}` work the same as quoted ones | Live API, 2026-11-02 |
|
|
23
|
+
| `EX ''` matches empty fields and `XEX ''` matches non-empty ones | Live API, 2026-11-02 |
|
|
24
|
+
| On a date field, `LT`, `LTE`, `GT`, and `GTE` match the same records as `BF`, `OBF`, `AF`, and `OAF` | Live API, 2026-11-02 |
|
|
25
|
+
| Timestamps can be compared with `AF` and `BF` using milliseconds | Live API, 2026-11-02 |
|
|
26
|
+
| With no `select`, only fields in the table's default set return, and Record ID# may not be one | Live API, 2026-11-02 |
|
|
27
|
+
| Timestamps return as `2026-10-21T11:49:56Z`, dates as `2026-10-18` | Live API, 2026-11-02 |
|
|
28
|
+
| A nonexistent app or table returns 401 with a valid token, not 404 | Live API, 2026-11-02 |
|
|
29
|
+
|
|
30
|
+
A bad realm or app can return 400 rather than 401 or 404, even with an invalid token, so don't
|
|
31
|
+
assume a 400 means the request body was wrong.
|
|
32
|
+
|
|
33
|
+
## Query pitfalls
|
|
34
|
+
|
|
35
|
+
Tested on 2026-11-02 against a small sandbox app. These matter because many mistakes return a
|
|
36
|
+
plausible result instead of an error.
|
|
37
|
+
|
|
38
|
+
| `where` clause | Result |
|
|
39
|
+
|---|---|
|
|
40
|
+
| Lowercase or unknown operator: `{6.ex.'x'}`, `{6.NOPE.'x'}` | **Behaves like `CT`** and returns matches |
|
|
41
|
+
| Unquoted value: `{6.EX.foo}` | No records, no error |
|
|
42
|
+
| Unknown field: `{999999.EX.'x'}` | No records, no error |
|
|
43
|
+
| Field label instead of ID: `{'Title'.EX.'x'}` | No records, no error |
|
|
44
|
+
| Trailing join: `{6.EX.'x'}AND` | No error |
|
|
45
|
+
| Lowercase join, missing brace, or free text | 400, `Criteria is invalid.` |
|
|
46
|
+
|
|
47
|
+
**Quoting values.** Tested with a temporary record containing `O'Brien`, a backslash, and a brace:
|
|
48
|
+
|
|
49
|
+
| Text in a value | Result |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `\'` or an unescaped `'` | Matches a stored apostrophe, but the unescaped form is ambiguous, so `F` and the string validator use `\'` |
|
|
52
|
+
| `''` | Doesn't match a stored apostrophe |
|
|
53
|
+
| `\\` | Matches a stored backslash; a single `\` before another character doesn't |
|
|
54
|
+
| `{` or `}` | 400, `Criteria is invalid.`, so values containing braces can't be searched for |
|
|
55
|
+
|
|
56
|
+
**A value can break out of its clause.** With `zz'}OR{3.GT.'0` as the value, the query matched every
|
|
57
|
+
record whether the quotes were left alone, written as `\'`, or written as `''`. Every breakout we
|
|
58
|
+
found needs a brace, so `F` rejects braces in values. The string validator can't tell a breakout
|
|
59
|
+
from a genuine second condition, so use `F` for any text you don't control.
|
|
60
|
+
|
|
61
|
+
## Writes
|
|
62
|
+
|
|
63
|
+
Tested on 2026-11-02 against temporary tables in a sandbox app.
|
|
64
|
+
|
|
65
|
+
| Behavior | Result |
|
|
66
|
+
|---|---|
|
|
67
|
+
| A row fails | The request returns 207, other rows are written, and `metadata.lineErrors` maps the row number, starting from 1, to its messages |
|
|
68
|
+
| `{"value": null}` | **Silently ignored**, leaving the old value |
|
|
69
|
+
| `{"value": ""}` | Clears text, date, timestamp, numeric, choice, and checkbox fields |
|
|
70
|
+
| Row with an existing Record ID# | Updates it, or reports it in `unchangedRecordIds` if nothing changed |
|
|
71
|
+
| Row with a missing Record ID#, with or without `mergeFieldId` | Row error, and no record is created |
|
|
72
|
+
| `mergeFieldId` on a unique field, value doesn't match | **Creates a record** |
|
|
73
|
+
| `mergeFieldId` on a non-unique field, or a row without the merge field | 400 for the whole request |
|
|
74
|
+
| Unknown field, formula field, built-in field | Row error |
|
|
75
|
+
| A value outside a field's choices | Accepted when `allowNewChoices` is on, otherwise a row error |
|
|
76
|
+
| Empty `data` | 400 |
|
|
77
|
+
| `DELETE /records` without `where` or with a blank one | 400 |
|
|
78
|
+
| `DELETE /records` with a malformed `where` | **`numberDeleted: 0`, no error** |
|
|
79
|
+
| `DELETE /records` with a lowercase operator | **Acts like `CT`**, so it can delete more than intended |
|
|
80
|
+
| `DELETE /fields` with `{"fieldIds": [...]}` | Partial failures are listed in `errors` beside `deletedFieldIds` |
|
|
81
|
+
| `POST /fields` with `unique` or `required` | 400. Set them afterward with `POST /fields/{id}` |
|
|
82
|
+
| Reading a cleared date or timestamp | Returns `""`, and a cleared numeric returns null |
|
|
83
|
+
| Checkbox queries | `{id.EX.'true'}`, `{id.EX.'1'}`, and `{id.TV.'true'}` match the same records |
|
|
84
|
+
|
|
85
|
+
## Reports
|
|
86
|
+
|
|
87
|
+
Tested on 2026-11-02 against a temporary table in a sandbox app, with table, ask-the-user, summary,
|
|
88
|
+
and chart reports built in the UI. The API can't create reports.
|
|
89
|
+
|
|
90
|
+
| Behavior | Result |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `GET /reports?tableId=` | A list of the same objects as `GET /reports/{id}?tableId=`: `id`, `name`, `type`, `description`, `query`, `properties`, `incomplete`, `usedCount`, and `usedLast` once used |
|
|
93
|
+
| Report IDs | Numeric strings, **unique only within a table**. A new table gets `1` (List All) and `2` (List Changes) |
|
|
94
|
+
| Report names | **Can repeat within a table** |
|
|
95
|
+
| `POST /reports/{id}/run?tableId=` | Needs no body. Returns `fields`, `data`, and `metadata`, as a query does |
|
|
96
|
+
| `top` and `skip` | Must be query parameters. **In the body they're silently ignored, and every record returns** |
|
|
97
|
+
| `top` omitted | Returned every record in a 12-record table |
|
|
98
|
+
| `top` of 0 or less, or a negative `skip` | 400, although a query accepts `top: 0` |
|
|
99
|
+
| `skip` past the end | No records, no error |
|
|
100
|
+
| A report ID with leading zeros, such as `01` | Runs the same report |
|
|
101
|
+
| Missing `tableId` | 400 |
|
|
102
|
+
| Unknown table | 401 for an ID that never existed, 404 for a dropped table |
|
|
103
|
+
| Unknown or non-numeric report ID | 404 |
|
|
104
|
+
| The report's filter, sort, and columns | Applied. Rows hold only the report's columns |
|
|
105
|
+
| A report whose columns leave out Record ID#, including the default `List All` | Rows have no field 3 |
|
|
106
|
+
| Running a report | **Adds 1 to `usedCount`, even when the run fails.** A GET doesn't, and records aren't modified |
|
|
107
|
+
| Ask-the-user report | `incomplete: true`, and running it returns 400, `Incomplete reports not supported` |
|
|
108
|
+
| Summary and chart reports | **Same response shape as a table report, but `fields` IDs are column positions starting at 2.** The second column has ID 3, the Record ID# field in a table report, so nothing in the response tells them apart. Only `type` on the report does |
|
|
109
|
+
| Chart category values | Dates return as text, `01-01-2026`, not `yyyy-MM-dd` |
|
|
110
|
+
| Renaming a column in a report | The UI doesn't offer this |
|
|
111
|
+
|
|
112
|
+
## File attachments
|
|
113
|
+
|
|
114
|
+
Tested on 2026-11-02 against a temporary table in a sandbox app, using a new `file` field with its
|
|
115
|
+
default settings.
|
|
116
|
+
|
|
117
|
+
| Behavior | Result |
|
|
118
|
+
|---|---|
|
|
119
|
+
| Upload | `POST /records` with `{"value": {"fileName": ..., "data": <base64>}}` |
|
|
120
|
+
| Download | `GET /files/{table}/{record}/{field}/{version}`. **The body is base64**, whatever `Content-Type` says. Bytes round-trip exactly, including all 256 values |
|
|
121
|
+
| File name in a download | `Content-Disposition: attachment; filename*=utf-8''<percent-encoded>` |
|
|
122
|
+
| `Content-Type` of a download | Based on the file name's extension: an `.html` name gives `text/html`, with a base64 body |
|
|
123
|
+
| Version `0` | The latest, **for `DELETE` as well as `GET`** |
|
|
124
|
+
| Each upload | Adds a version, even with identical contents, and reports the record as updated |
|
|
125
|
+
| Version limit | A new field has `maxVersions: 3`. **The oldest is silently removed**, and its download returns 404 |
|
|
126
|
+
| Version numbers | Not reused or renumbered after a delete or removal |
|
|
127
|
+
| `DELETE` a version | Returns that version's details. Deleting the last one keeps the record and empties the field |
|
|
128
|
+
| `{"value": ""}` or `{"value": null}` on a file field | **No error and no change**, reported in `unchangedRecordIds`. On insert, creates a record with no file |
|
|
129
|
+
| `{"value": "text"}`, or a value without `data` | Row error |
|
|
130
|
+
| A value without `fileName` | **No error, and creates a record with no file** |
|
|
131
|
+
| Empty `fileName` | Row error |
|
|
132
|
+
| Invalid base64 | **Accepted, and the file holds garbage**, with no row error, also inside a batch |
|
|
133
|
+
| Empty `data` | Accepted, as an empty file |
|
|
134
|
+
| A file value sent to a text field | **Stores the file name and discards the contents** |
|
|
135
|
+
| File names | **Stored as sent, including `../../etc/passwd`, `C:\\x\\y.txt`, and line breaks.** A NUL byte cuts the name short. No extension was refused, including `.exe` and `.html` |
|
|
136
|
+
| Reading a file field | `{"url": "/files/...", "versions": [...]}`, with a relative `url`. Empty is `{"url": "", "versions": []}` |
|
|
137
|
+
| Each entry in `versions` | `versionNumber`, `fileName`, `uploaded`, and `creator`, **which includes the uploader's email address** |
|
|
138
|
+
| Missing record, field, or version | 404 |
|
|
139
|
+
| A non-file field | 400 |
|
|
140
|
+
| Unknown table | 400, `Invalid usertoken`, unlike other endpoints' 401 |
|
|
141
|
+
|
|
142
|
+
**Request size limit.** `POST /records` accepted a body of exactly 41,943,040 bytes (40 MiB) and
|
|
143
|
+
returned 413, `request entity too large`, at one byte more. It's a limit on the whole request: two 25
|
|
144
|
+
MB files in one request returned 413, and two 15 MB files succeeded. It applies without files too:
|
|
145
|
+
1,500 rows of 30,000 characters returned 413 and wrote nothing. The largest file that fits was
|
|
146
|
+
31,457,208 bytes. Downloads worked up to that size.
|
|
147
|
+
|
|
148
|
+
## Formulas
|
|
149
|
+
|
|
150
|
+
Tested on 2026-11-02 against temporary tables in a sandbox app.
|
|
151
|
+
|
|
152
|
+
| Behavior | Result |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `POST /formula/run` | Takes `formula` and `from`, and `rid` if the formula uses fields. Returns `result`, `resultType`, and `fieldTypeApplied` |
|
|
155
|
+
| `result` | **Always a string**, whatever the type |
|
|
156
|
+
| `resultType` values seen | `text`, `number`, `bool`, `date`, `date/time`, `timeofday`, `duration`, `textlist`, `user`, `null` |
|
|
157
|
+
| Booleans | **`"1"` and `"0"`**, so `"0"` is a truthy string in Python |
|
|
158
|
+
| Numbers | Rounded for display: `1/3` returns `"0.3333333"` |
|
|
159
|
+
| Dates and timestamps | **Display text in the app's format, with no time zone:** `03-04-2026` and `03-03-2026 09:06 PM`, where the stored time was `2026-03-04T05:06:07Z`. A query returns the same field as ISO 8601 |
|
|
160
|
+
| Durations | Display text: `Days(3)` returns `"72:00"` |
|
|
161
|
+
| Empty values | `""`, for any type |
|
|
162
|
+
| A blank formula | `""` returns 400. **Whitespace returns 200 with an empty `null` result** |
|
|
163
|
+
| A formula using a field, with no `rid` | 400 |
|
|
164
|
+
| Missing record | 404, even for a formula that uses no fields |
|
|
165
|
+
| Missing `from` | 400 |
|
|
166
|
+
| Unknown table | 401 |
|
|
167
|
+
| A syntax error, unknown field or function, type mismatch, or `1/0` | 400. **Some messages include HTML**, such as a `javascript:` link |
|
|
168
|
+
| Function and field name case | Ignored: `today()` and `[title]` work |
|
|
169
|
+
| `fieldTypeApplied` | `false` in every test. Sending `fieldType` or `resultType` changed nothing, and we don't know what sets it |
|
|
170
|
+
| **Other tables** | **A formula can read other tables** with `GetRecord()` and `GetRecords()`, whatever `from` says |
|
|
171
|
+
| Other information | `AppID()`, `URLRoot()`, and `UserToEmail(User())` return the app ID, realm, and the token owner's email |
|
|
172
|
+
| Side effects | No record changed, including `Date Modified`, after about 60 runs |
|
|
173
|
+
| Formula length | A 1,000,000-character formula worked |
|
|
174
|
+
|
|
175
|
+
**Formula fields.** `POST /fields` with `properties.formula` creates one, with `mode: "formula"`. Unlike
|
|
176
|
+
many other mistakes, these are checked:
|
|
177
|
+
|
|
178
|
+
| Behavior | Result |
|
|
179
|
+
|---|---|
|
|
180
|
+
| A syntax error, unknown field, or result type that doesn't match the field | 400. A failed update keeps the old formula |
|
|
181
|
+
| Updating the formula | Values change right away, and other properties are kept |
|
|
182
|
+
| **Updating to an empty formula** | **200, and the field's values become empty** |
|
|
183
|
+
| Creating with an empty formula, or adding a formula to a plain field | 400, `Field must be of formula mode.` |
|
|
184
|
+
| Renaming a field a formula uses | Quickbase updates the formula's text |
|
|
185
|
+
| Writing to a formula field | Row error |
|
|
186
|
+
| Field types that took a formula | `text`, `numeric`, `date`, `checkbox`, `duration`, `timeofday`, `currency`, `percent`, `rating`, `url`, `email`, and `datetime` |
|
|
187
|
+
| **A timestamp formula** | **`timestamp` returns 400, `[formula] property is not supported`. `datetime` works, and the field then reports its type as `timestamp`** |
|
|
188
|
+
| `text-multi-line` | 400, `[formula] property is not supported` |
|
|
189
|
+
| Reading formula fields in a query | Full precision, unlike a formula run: `1/3` returns `0.3333333333333333`. A duration returns milliseconds, and a time of day `11:34:49.896` |
|
|
190
|
+
|
|
191
|
+
## Rate limits
|
|
192
|
+
|
|
193
|
+
Tested on 2026-11-02 with short bursts from 12 threads against temporary tables.
|
|
194
|
+
|
|
195
|
+
| Behavior | Result |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `retry-after` on a real 429 | **Whole seconds**, such as `9` or `10`. After waiting that long, the next request succeeded |
|
|
198
|
+
| 429 body | `{"error": "Quota has been exceeded"}`, **not the usual `message` and `description`** |
|
|
199
|
+
| When it started | After roughly 170 to 185 responses in 4 to 6 seconds, for both `/formula/run` and `/records/query`. That's more than the documented 100 per 10 seconds, so don't rely on the number. These bursts don't show a difference between the two endpoints |
|
|
200
|
+
|
|
201
|
+
## Temporary tokens
|
|
202
|
+
|
|
203
|
+
**The SDK doesn't support temporary tokens.** These findings are kept for when that's revisited.
|
|
204
|
+
|
|
205
|
+
Tested on 2026-11-02 against temporary tables in a sandbox app, with tokens fetched from a logged-in
|
|
206
|
+
browser's console, because a script can't get one.
|
|
207
|
+
|
|
208
|
+
| Behavior | Result |
|
|
209
|
+
|---|---|
|
|
210
|
+
| `GET /auth/temporary/{table or app ID}` from a script | **400, `Domain not allowed`, with or without credentials.** The check runs before authentication |
|
|
211
|
+
| The same request from a page on the realm | Works, using the browser's session (`credentials: "include"`). Returns `temporaryAuthorization` |
|
|
212
|
+
| Format | An opaque string of 137 characters, with letters, digits, and `.` or `=` |
|
|
213
|
+
| Header | `Authorization: QB-TEMP-TOKEN <token>` |
|
|
214
|
+
| Lifetime | The token stopped working 266 seconds after being pasted into the test. It was fetched shortly before that, so this fits the documented 5 minutes without confirming it |
|
|
215
|
+
| Expired, or out of scope | Both return 401, `Invalid Authorization.`, so they can't be told apart |
|
|
216
|
+
| A malformed token, or a user token with this header | 400, `Invalid Authorization` |
|
|
217
|
+
| A temporary token with `QB-USER-TOKEN` | 401, `User token is invalid` |
|
|
218
|
+
| Requesting another token with one | 400, `Domain not allowed` |
|
|
219
|
+
|
|
220
|
+
| Request | Table-scoped token | App-scoped token |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| Records in its table: query, write | **200, including writes** | 401 |
|
|
223
|
+
| Fields and reports of its table, running a report or formula | 200 | 401 |
|
|
224
|
+
| Another table: query, write, fields | 401 | 401 |
|
|
225
|
+
| `GET /tables` and `GET /apps/{id}` | **401** | 200 |
|
|
226
|
+
| **A formula that reads another table** | **200, and returns that table's data** | 401 |
|
|
227
|
+
|
|
228
|
+
Tokens are scoped narrowly, and an app-scoped token doesn't reach its tables' contents. **Table scope
|
|
229
|
+
doesn't contain a formula**: `GetRecord()` read a table the token couldn't otherwise reach. Uploads,
|
|
230
|
+
downloads, and file deletes weren't tested with a temporary token, and neither were tokens from users
|
|
231
|
+
with fewer permissions.
|
|
232
|
+
|
|
233
|
+
## Field types
|
|
234
|
+
|
|
235
|
+
Tested on 2026-11-02 against temporary tables in a sandbox app. These decide the types in generated
|
|
236
|
+
classes, and an unlisted type becomes `Any` and read-only.
|
|
237
|
+
|
|
238
|
+
`POST /fields` accepts these types: `text`, `text-multiple-choice`, `text-multi-line`, `rich-text`,
|
|
239
|
+
`numeric`, `currency`, `rating`, `percent`, `multitext`, `email`, `url`, `duration`, `date`,
|
|
240
|
+
`datetime`, `timestamp`, `timeofday`, `checkbox`, `user`, `multiuser`, `address`, `phone`, and `file`.
|
|
241
|
+
A `datetime` field reports its type as `timestamp`.
|
|
242
|
+
|
|
243
|
+
| Field type | A query returns | When empty |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| `recordid` | Integer | Never empty |
|
|
246
|
+
| `text`, `text-multiple-choice`, `text-multi-line`, `rich-text`, `email`, `url`, `phone` | Text | **`""`, not null** |
|
|
247
|
+
| `numeric`, `currency`, `rating`, `percent` | Number | null |
|
|
248
|
+
| `duration` | Milliseconds | null |
|
|
249
|
+
| `date` | `2026-03-04` | `""` |
|
|
250
|
+
| `timestamp` | `2026-03-04T05:06:07Z` | `""` |
|
|
251
|
+
| `timeofday` | `13:30:00` | `""` |
|
|
252
|
+
| `checkbox` | Boolean | `false` |
|
|
253
|
+
| `multitext` | A list of text | `[]` |
|
|
254
|
+
| `user` | `{"email", "id", "name"}` | null |
|
|
255
|
+
| `multiuser` | A list of users | `[]` |
|
|
256
|
+
| `file` | `{"url", "versions"}` | `{"url": "", "versions": []}` |
|
|
257
|
+
| `address` | Display text, such as `1 Main St, Springfield 12345` | `""` |
|
|
258
|
+
| `dblink` | `""` in the one case seen | |
|
|
259
|
+
|
|
260
|
+
| Behavior | Result |
|
|
261
|
+
|---|---|
|
|
262
|
+
| Lookup and formula fields | Return the same shapes as their type. Lookups were tested holding text, numeric, and date values, and empty for user, multitext, and checkbox. A formula of `Null` matched its type on text, numeric, and checkbox fields |
|
|
263
|
+
| Writable fields | Built-in fields 1 to 5 aren't, nor are formula, lookup, and summary fields, or `address` |
|
|
264
|
+
| An `address` field | Is read-only, with text subfields for its parts, which can be written. Creating one also creates its subfields, and sets `Country` to a default |
|
|
265
|
+
| `dblink` | Created by a relationship, on the parent table |
|
|
266
|
+
| Labels | **Can contain almost anything, including `"""`, line breaks, and code**, as can table names |
|
|
267
|
+
| Label uniqueness | Exact duplicates are refused, ignoring case, but `Due Date` and `Due-Date` can both exist |
|
|
268
|
+
| Field IDs | Assigned in order, and address subfields use up IDs |
|
|
269
|
+
| `GET /tables/{id}` | Returned an error without its `appId`, so use `GET /tables?appId=` |
|
|
270
|
+
|
|
271
|
+
## Unverified
|
|
272
|
+
|
|
273
|
+
Check these against a sandbox app before the feature that needs them ships.
|
|
274
|
+
|
|
275
|
+
| Question | Needed for |
|
|
276
|
+
|---|---|
|
|
277
|
+
| Can a query return fewer than `top` records before the end? The test table was too small to show it, so advance by `numRecords` | Paging |
|
|
278
|
+
| Which endpoints have different rate limits? | Transport |
|
|
279
|
+
| Whether a formula can read a table in another app | Formulas |
|
|
280
|
+
| Lookups that hold a value, for types other than text, numeric, and date, and field types only the UI can create | Generated classes |
|
|
281
|
+
| Correct API host for EU realms (pass `base_url` if it differs) | Realm handling |
|
|
282
|
+
| Table reports with a formula column, or with Record ID# as a column | Reports |
|
|
283
|
+
| Whether pages of a report are stable when its sort has ties | Reports |
|
|
284
|
+
| Whether a report's `top`-less run returns every record in a large table | Reports |
|
|
285
|
+
| Whether other endpoints or realms have a different request size limit | Writes |
|
|
286
|
+
| File fields with other version settings, and limits on total storage | File attachments |
|
|
287
|
+
| How reports and files behave for tokens with fewer permissions, since all testing used one token | Reports, file attachments |
|
|
288
|
+
|
|
289
|
+
## Known limits to revisit
|
|
290
|
+
|
|
291
|
+
Choices made in the SDK that are worth another look before 1.0.
|
|
292
|
+
|
|
293
|
+
| Limit | Why it's there | Possible change |
|
|
294
|
+
|---|---|---|
|
|
295
|
+
| Report details are cached for `schema_ttl`, so a report changed in the UI can be checked against old details | Avoids an extra request for every page | Refresh before each run, or compare the run response |
|
|
296
|
+
| Summary and chart reports can only be read through `qb.request()` | Their column IDs are positions, so they can't be records | A separate method returning `{label: value}` rows |
|
|
297
|
+
| `timeout` is set per client, so a large upload on a slow connection may need a larger one for every request | The transport has one timeout | A per-call timeout for uploads and downloads |
|
|
298
|
+
| Files are held in memory as bytes and base64 while being sent | The API takes base64 inside JSON, so there's nothing to stream | Probably none, unless Quickbase adds another upload route |
|
|
299
|
+
| `record["Attachment"]` returns Quickbase's raw dictionary | Kept simple for the first version | A typed value with versions, which could leave out uploader emails |
|
|
300
|
+
| Quickbase's formula error messages can contain HTML, which errors pass on unchanged | Errors keep Quickbase's message intact | Strip HTML from error messages, in case they're displayed |
|
|
301
|
+
| `FormulaResult.value` leaves dates, timestamps, and durations as display text | They depend on app settings, and timestamps have no time zone | Parse them using the app's settings, if a safe way appears |
|
|
302
|
+
| Temporary tokens aren't supported | Their main use is browser code pages, a script can't get one, and the SDK's table lookup needs `GET /tables`, which a table-scoped token can't call | After 1.0, a `temporary_token` client option that treats `app.table()` as an ID and skips that lookup, for a backend acting for a logged-in user |
|
|
303
|
+
| `codegen` writes one class per table, with no way to rename one | Names come from labels, so renaming a label renames the attribute | Keep names in a small config file, if this proves a problem |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--8<-- "CHANGELOG.md"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Before you start
|
|
4
|
+
|
|
5
|
+
You need three things from Quickbase:
|
|
6
|
+
|
|
7
|
+
- **Your realm**, the `acme` in `acme.quickbase.com`.
|
|
8
|
+
- **A user token** with access to your app. Quickbase explains how to create one in its
|
|
9
|
+
[authentication docs](https://developer.quickbase.com/auth). Treat it like a password, and give it
|
|
10
|
+
access to as few apps as possible.
|
|
11
|
+
- **Your app's ID**, which appears in the app's URL and usually starts with `b`.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install pyquickbase==0.2.0a1
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Read some records
|
|
20
|
+
|
|
21
|
+
Both clients have the same API. Use the sync client for scripts and notebooks, and the async client
|
|
22
|
+
in applications that already use asyncio or Trio.
|
|
23
|
+
|
|
24
|
+
=== "Sync"
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from pyquickbase import F, Quickbase
|
|
28
|
+
|
|
29
|
+
with Quickbase(realm="acme", token="your_user_token") as qb:
|
|
30
|
+
tasks = qb.app("bq1234567").table("Tasks")
|
|
31
|
+
|
|
32
|
+
for task in tasks.query(where=F("Status") == "Done", select=["Title"]):
|
|
33
|
+
print(task.id, task["Title"])
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
=== "Async"
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from pyquickbase import AsyncQuickbase, F
|
|
40
|
+
|
|
41
|
+
async with AsyncQuickbase(realm="acme", token="your_user_token") as qb:
|
|
42
|
+
tasks = qb.app("bq1234567").table("Tasks")
|
|
43
|
+
|
|
44
|
+
async for task in tasks.query(where=F("Status") == "Done", select=["Title"]):
|
|
45
|
+
print(task.id, task["Title"])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
To keep the token out of your code, set `QB_REALM` and `QB_USER_TOKEN` in the environment and use
|
|
49
|
+
`Quickbase.from_env()`.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from pyquickbase import Quickbase
|
|
53
|
+
|
|
54
|
+
with Quickbase.from_env() as qb:
|
|
55
|
+
print(qb.app("bq1234567").info().name)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Write a record
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
tasks.insert([{"Title": "Pour slab", "Status": "Not Started"}])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Next steps
|
|
65
|
+
|
|
66
|
+
- [Reading records](guides/reading.md) covers queries, paging, and record values.
|
|
67
|
+
- [Writing records](guides/writing.md) covers inserts, updates, upserts, and deletes.
|
|
68
|
+
- [Handling errors](guides/errors.md) lists what can go wrong.
|