pyquickbase 0.2.0a1__tar.gz → 1.0.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/CHANGELOG.md +24 -0
  2. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/CONTRIBUTING.md +13 -1
  3. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/PKG-INFO +5 -5
  4. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/README.md +3 -3
  5. pyquickbase-1.0.0rc1/ROADMAP.md +69 -0
  6. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/api-notes.md +37 -37
  7. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/getting-started.md +1 -1
  8. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/guides/clients.md +6 -0
  9. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/guides/codegen.md +19 -3
  10. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/guides/errors.md +8 -2
  11. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/guides/files.md +2 -2
  12. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/guides/reading.md +8 -3
  13. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/index.md +4 -4
  14. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/reference.md +2 -1
  15. pyquickbase-1.0.0rc1/docs/stability.md +49 -0
  16. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/mkdocs.yml +1 -0
  17. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/pyproject.toml +1 -1
  18. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_async/_resources.py +46 -27
  19. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_codegen.py +35 -3
  20. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_models.py +95 -91
  21. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_query.py +3 -1
  22. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_sync/_resources.py +46 -27
  23. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_typed.py +15 -6
  24. pyquickbase-1.0.0rc1/src/pyquickbase/_version.py +1 -0
  25. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_writes.py +28 -25
  26. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/errors.py +1 -1
  27. pyquickbase-1.0.0rc1/tests/_async/test_generated_contract.py +237 -0
  28. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_async/test_typed.py +35 -7
  29. pyquickbase-1.0.0rc1/tests/_sync/test_generated_contract.py +236 -0
  30. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_sync/test_typed.py +35 -7
  31. pyquickbase-1.0.0rc1/tests/fixtures/generated_tables.py +114 -0
  32. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/test_codegen.py +23 -8
  33. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/test_docs.py +12 -0
  34. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/test_files.py +15 -9
  35. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/test_formulas.py +6 -5
  36. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/test_query.py +25 -16
  37. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/test_writes.py +36 -31
  38. pyquickbase-0.2.0a1/src/pyquickbase/_version.py +0 -1
  39. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/.env.example +0 -0
  40. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/.github/workflows/ci.yml +0 -0
  41. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/.github/workflows/docs.yml +0 -0
  42. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/.github/workflows/release.yml +0 -0
  43. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/.gitignore +0 -0
  44. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/.python-version +0 -0
  45. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/LICENSE +0 -0
  46. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/SECURITY.md +0 -0
  47. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/changelog.md +0 -0
  48. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/guides/formulas.md +0 -0
  49. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/guides/schema.md +0 -0
  50. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/docs/guides/writing.md +0 -0
  51. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/scripts/generate_sync.py +0 -0
  52. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/__init__.py +0 -0
  53. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_async/__init__.py +0 -0
  54. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_async/_client.py +0 -0
  55. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_async/_transport.py +0 -0
  56. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_auth.py +0 -0
  57. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_cli.py +0 -0
  58. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_config.py +0 -0
  59. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_response.py +0 -0
  60. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_retry.py +0 -0
  61. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_schema.py +0 -0
  62. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_sync/__init__.py +0 -0
  63. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_sync/_client.py +0 -0
  64. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/_sync/_transport.py +0 -0
  65. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/src/pyquickbase/py.typed +0 -0
  66. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/__init__.py +0 -0
  67. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_async/test_client.py +0 -0
  68. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_async/test_files.py +0 -0
  69. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_async/test_formulas.py +0 -0
  70. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_async/test_reports.py +0 -0
  71. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_async/test_resources.py +0 -0
  72. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_async/test_writes.py +0 -0
  73. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_sync/test_client.py +0 -0
  74. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_sync/test_files.py +0 -0
  75. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_sync/test_formulas.py +0 -0
  76. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_sync/test_reports.py +0 -0
  77. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_sync/test_resources.py +0 -0
  78. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/_sync/test_writes.py +0 -0
  79. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/conftest.py +0 -0
  80. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/tests/test_core.py +0 -0
  81. {pyquickbase-0.2.0a1 → pyquickbase-1.0.0rc1}/uv.lock +0 -0
@@ -6,6 +6,30 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.0.0rc1] - 2026-11-02
10
+
11
+ ### Added
12
+
13
+ - A stability and versioning policy, which applies from 1.0.
14
+
15
+ ### Changed
16
+
17
+ - `TypedTable.check_schema()` now always fetches the table's fields again, instead of doing nothing
18
+ after the first check. Other methods still check once, on first use, against cached metadata.
19
+ - A generated class no longer fails when a field becomes writable. Otherwise, supporting a new field
20
+ type in a later version would break classes generated earlier.
21
+ - Generated names no longer depend on the Python version running `pyquickbase codegen`, so a label
22
+ can't get a different name on a newer Python.
23
+ - The error from `TypedRecord.from_record()` now mentions that a report may have left out a field.
24
+ - `SchemaMismatchError` and `UnknownFieldError` now say a field may be hidden from the token, because
25
+ Quickbase leaves out fields a role can't see, as if they didn't exist.
26
+
27
+ ### Removed
28
+
29
+ - `from_api()` on `AppInfo`, `TableInfo`, `Field`, `Relationship`, `ReportInfo`, `FormulaResult`, and
30
+ `Page`, `FileDownload.from_response()`, `FileUpload.to_api()`, and `WriteResult.merged()` and
31
+ `summary()`. These were internal helpers, and they're now private so they can change freely.
32
+
9
33
  ## [0.2.0a1] - 2026-11-02
10
34
 
11
35
  ### Added
@@ -34,10 +34,22 @@ Anything exported from `pyquickbase/__init__.py` or defined in `errors.py` is pu
34
34
  starting with `_` are private and may change at any time. Update `CHANGELOG.md` for changes that
35
35
  users would notice.
36
36
 
37
+ `docs/stability.md` says what 1.x promises, so check it before changing anything public. From 1.0, a
38
+ breaking change needs a major version, and removing something needs a `DeprecationWarning` in a minor
39
+ release first. Generated code must keep working on later 1.x versions, so treat what it relies on as
40
+ public, even when the name is private. `tests/fixtures/generated_tables.py` is an old generated file
41
+ that the tests run against the current code. From 1.0, don't regenerate it to make a test pass, because
42
+ a failure there means you've broken users' files.
43
+
37
44
  ## Secrets
38
45
 
39
46
  Never commit real tokens, realm names, or customer data, including in test fixtures. Tokens must
40
47
  never appear in exceptions, `repr`s, or logs.
41
48
 
42
- `docs/api-notes.md` tracks which Quickbase API behavior has been confirmed. Please update it if you
49
+ ## Notes and plans
50
+
51
+ `docs/api-notes.md` holds what's been confirmed about the Quickbase API. Please update it if you
43
52
  confirm or disprove something.
53
+
54
+ `ROADMAP.md` holds what's left to do, including anything untested and any limit we may want to change.
55
+ Delete an item once it's done.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyquickbase
3
- Version: 0.2.0a1
3
+ Version: 1.0.0rc1
4
4
  Summary: An unofficial, typed Python SDK for the Quickbase API, with sync and async clients.
5
5
  Project-URL: Documentation, https://matthew-williams-solutions.github.io/pyquickbase/
6
6
  Project-URL: Repository, https://github.com/matthew-williams-solutions/pyquickbase
@@ -10,7 +10,7 @@ Author: Matthew Williams
10
10
  License-Expression: MIT
11
11
  License-File: LICENSE
12
12
  Keywords: api,async,client,quickbase,sdk
13
- Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Framework :: AnyIO
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -31,7 +31,7 @@ sync and async clients.
31
31
 
32
32
  **[Read the documentation](https://matthew-williams-solutions.github.io/pyquickbase/)**
33
33
 
34
- > **Status: early alpha.** Anything may change before 1.0. This project is not affiliated with or endorsed by Quickbase, Inc.
34
+ > **Status: release candidate.** Small changes are still possible before 1.0. This project is not affiliated with or endorsed by Quickbase, Inc.
35
35
 
36
36
  ## What works today
37
37
 
@@ -54,10 +54,10 @@ sync and async clients.
54
54
  ## Usage
55
55
 
56
56
  ```bash
57
- pip install pyquickbase==0.2.0a1
57
+ pip install pyquickbase==1.0.0rc1
58
58
  ```
59
59
 
60
- This is an alpha release, so pin the exact version.
60
+ This is a release candidate, so pin the exact version.
61
61
 
62
62
  ```python
63
63
  from datetime import date
@@ -5,7 +5,7 @@ sync and async clients.
5
5
 
6
6
  **[Read the documentation](https://matthew-williams-solutions.github.io/pyquickbase/)**
7
7
 
8
- > **Status: early alpha.** Anything may change before 1.0. This project is not affiliated with or endorsed by Quickbase, Inc.
8
+ > **Status: release candidate.** Small changes are still possible before 1.0. This project is not affiliated with or endorsed by Quickbase, Inc.
9
9
 
10
10
  ## What works today
11
11
 
@@ -28,10 +28,10 @@ sync and async clients.
28
28
  ## Usage
29
29
 
30
30
  ```bash
31
- pip install pyquickbase==0.2.0a1
31
+ pip install pyquickbase==1.0.0rc1
32
32
  ```
33
33
 
34
- This is an alpha release, so pin the exact version.
34
+ This is a release candidate, so pin the exact version.
35
35
 
36
36
  ```python
37
37
  from datetime import date
@@ -0,0 +1,69 @@
1
+ # Roadmap
2
+
3
+ What's left to do. Delete an item once it's done: `CHANGELOG.md` and git history record what's
4
+ finished, and `docs/api-notes.md` records what's been confirmed about Quickbase. Limits we've decided
5
+ to keep are explained in the guides, not here.
6
+
7
+ ## Before 1.0
8
+
9
+ - Install `1.0.0rc1` from PyPI and try it live on a scratch table.
10
+ - In the 1.0 release commit:
11
+ - Change the classifier in `pyproject.toml` from Beta to Production/Stable.
12
+ - Remove the release candidate warnings from `README.md` and `docs/index.md`, and the advice to pin
13
+ the exact version. If the pins go too, update `test_install_pins_match_the_version`.
14
+ - If codegen's output has changed, refresh `tests/fixtures/generated_tables.py` one last time.
15
+
16
+ ## After 1.0
17
+
18
+ Nothing here should break anyone.
19
+
20
+ - Switch the docs from Material to Zensical, **by 2027-05-05**, when Material's maintenance ends. A
21
+ trial build of 0.0.74 worked, and its strict mode also catches broken anchors, which MkDocs doesn't.
22
+ Wait for 0.1.0, announced for 2026-11-05.
23
+ - A method for summary and chart reports, returning `{label: value}` rows.
24
+ - A per-call timeout for uploads and downloads.
25
+ - A small config file to choose generated names.
26
+ - `query_page()`, `select`, and `batch_size` on typed tables. `select` needs a design, because a
27
+ generated class requires every column.
28
+ - A setting that repeats a typed table's schema check after a set time.
29
+ - Typed access to file and user values, as a dictionary subclass, which could leave out uploader emails.
30
+ - Export `FieldRef`, `OrderBy`, and `Expression`, which appear in public signatures.
31
+ - Tie a `Row` to its class when the code runs. Only a type checker refuses a row from another class.
32
+
33
+ ## Open questions
34
+
35
+ Nothing is waiting on these. Test one against a sandbox app before depending on the answer, then move
36
+ the result to `docs/api-notes.md`.
37
+
38
+ **Queries and limits**
39
+
40
+ - Can a query return fewer than `top` records before the end? The SDK already advances by the number
41
+ returned.
42
+ - Which endpoints have different rate limits?
43
+ - Do other endpoints or realms have a different request size limit?
44
+
45
+ **Reports**
46
+
47
+ - Can a deleted report's ID be reused? A new report got ID 8, the highest so far, but the deleted
48
+ report's own ID wasn't recorded. If it can, cached report details could misread a total as a Record
49
+ ID#.
50
+ - Do pages stay stable with ties above 1,200 records, or while records are added or removed?
51
+ - Does a run without `top` return every record above 1,200 records? The SDK always sends `top`, so this
52
+ only affects `qb.request()`.
53
+
54
+ **Fields and files**
55
+
56
+ - What do lookups return for types other than text, numeric, and date, and what about field types only
57
+ the UI can create? These are `Any` and read-only in generated classes until confirmed.
58
+ - How do file fields behave with other version settings, and are there limits on total storage?
59
+
60
+ **Realms and apps**
61
+
62
+ - What's the API host for EU realms?
63
+ - Can a formula read a table in another app?
64
+ - How do roles with write access or record-level rules behave, and a report that sorts or filters on a
65
+ hidden field? Only a view-only role with hidden fields has been tested.
66
+ - Can a restricted user's formula read a table their role has no access to? A role's hidden fields held
67
+ up, but this wasn't tested, because the role's other tables were the seeded ones.
68
+ - Can renaming a table, or using the UI, give two tables the same name? If so, `app.table()` takes the
69
+ first match, unlike fields and reports, which raise an error.
@@ -1,8 +1,9 @@
1
1
  # Quickbase API notes
2
2
 
3
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.
4
+ renders in the browser and has no published spec that we've found, so check anything not listed here
5
+ against a real app before depending on it. Questions we haven't answered yet are in the
6
+ [roadmap](https://github.com/matthew-williams-solutions/pyquickbase/blob/main/ROADMAP.md#open-questions).
6
7
 
7
8
  ## Confirmed
8
9
 
@@ -26,6 +27,7 @@ renders in the browser and has no published spec that we've found, so check anyt
26
27
  | 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
28
  | Timestamps return as `2026-10-21T11:49:56Z`, dates as `2026-10-18` | Live API, 2026-11-02 |
28
29
  | A nonexistent app or table returns 401 with a valid token, not 404 | Live API, 2026-11-02 |
30
+ | Creating a table with a name already in the app, ignoring case, returns 400, `Invalid input` | Live API, 2026-11-02 |
29
31
 
30
32
  A bad realm or app can return 400 rather than 401 or 404, even with an invalid token, so don't
31
33
  assume a 400 means the request body was wrong.
@@ -103,6 +105,11 @@ and chart reports built in the UI. The API can't create reports.
103
105
  | Unknown or non-numeric report ID | 404 |
104
106
  | The report's filter, sort, and columns | Applied. Rows hold only the report's columns |
105
107
  | A report whose columns leave out Record ID#, including the default `List All` | Rows have no field 3 |
108
+ | A table report with Record ID# as a column, even in second position | `fields` uses the real ID, 3, with type `recordid`, unlike a summary report's positions. All 1,200 rows matched a query's IDs |
109
+ | A table report with a formula column | `fields` uses the formula field's real ID and its type, such as `numeric`. All 1,200 values matched a query |
110
+ | A sort with ties, 400 records on each of 3 values | Ties came back in Record ID# order. Six full runs, with page sizes of 7, 37, 100, and 1,000, gave the same order, with nothing repeated or missing. Editing a tied record's other field didn't move it |
111
+ | `top` omitted, in a 1,200-record table | Returned every record, and `metadata` has no `top` |
112
+ | Changing a saved report's type | The UI doesn't offer this |
106
113
  | Running a report | **Adds 1 to `usedCount`, even when the run fails.** A GET doesn't, and records aren't modified |
107
114
  | Ask-the-user report | `incomplete: true`, and running it returns 400, `Incomplete reports not supported` |
108
115
  | 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 |
@@ -200,7 +207,11 @@ Tested on 2026-11-02 with short bursts from 12 threads against temporary tables.
200
207
 
201
208
  ## Temporary tokens
202
209
 
203
- **The SDK doesn't support temporary tokens.** These findings are kept for when that's revisited.
210
+ **The SDK doesn't support temporary tokens.** Their main use is browser code pages, a script can't get
211
+ one, and the SDK's table lookup needs `GET /tables`, which a table-scoped token can't call. These
212
+ findings are kept for when that's revisited. Supporting them would need a `temporary_token` client
213
+ option that treats `app.table()` as an ID and skips that lookup, for a backend acting for a logged-in
214
+ user.
204
215
 
205
216
  Tested on 2026-11-02 against temporary tables in a sandbox app, with tokens fetched from a logged-in
206
217
  browser's console, because a script can't get one.
@@ -230,6 +241,29 @@ doesn't contain a formula**: `GetRecord()` read a table the token couldn't other
230
241
  downloads, and file deletes weren't tested with a temporary token, and neither were tokens from users
231
242
  with fewer permissions.
232
243
 
244
+ ## Restricted users
245
+
246
+ Tested on 2026-11-02 against a temporary table in a sandbox app, with a second user's token. Its role
247
+ could only view that table, had no access to the app's other tables, and had access `None` on one text
248
+ field and one file field. A role with write access or record-level rules wasn't tested.
249
+
250
+ | Behavior | Result |
251
+ |---|---|
252
+ | A token's permissions | Its user's, through their role |
253
+ | `GET /fields` | **A hidden field is left out**, exactly as if it didn't exist, so a deleted field and a hidden one can't be told apart |
254
+ | `GET /fields/{id}` for a hidden field | 500, `QuickBase is experiencing technical difficulties` |
255
+ | `GET /tables` | Lists only tables the role can use |
256
+ | `GET /apps/{id}` | 403, for this role |
257
+ | `select` with a hidden field and a visible one | **200, and the hidden field is silently left out** |
258
+ | `select` with only hidden fields | 400, `Report contains a restricted field` |
259
+ | `where` or `sortBy` on a hidden field | 403, whether or not anything matches |
260
+ | Running a table report with a hidden column | **200, and the column is silently left out.** A report with a hidden field in its sort or filter wasn't tested |
261
+ | A file field's value, read with view access | Includes `creator`, with **the uploader's email address** |
262
+ | Downloading from a visible file field | 200 |
263
+ | Downloading from a hidden file field | 404, `No such field` |
264
+ | A formula using a hidden field, directly or through `GetRecords()` | 403, including when `GetRecords()` only filters on it. A visible field works |
265
+ | Insert, update, delete, upload, deleting a file version, creating a field | 403, `Insufficient permissions`, and nothing changed |
266
+
233
267
  ## Field types
234
268
 
235
269
  Tested on 2026-11-02 against temporary tables in a sandbox app. These decide the types in generated
@@ -267,37 +301,3 @@ A `datetime` field reports its type as `timestamp`.
267
301
  | Label uniqueness | Exact duplicates are refused, ignoring case, but `Due Date` and `Due-Date` can both exist |
268
302
  | Field IDs | Assigned in order, and address subfields use up IDs |
269
303
  | `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 |
@@ -13,7 +13,7 @@ You need three things from Quickbase:
13
13
  ## Install
14
14
 
15
15
  ```bash
16
- pip install pyquickbase==0.2.0a1
16
+ pip install pyquickbase==1.0.0rc1
17
17
  ```
18
18
 
19
19
  ## Read some records
@@ -44,6 +44,12 @@ qb = Quickbase(realm="acme", token="your_user_token", retry=policy)
44
44
  `max_retry_after` is the longest `Retry-After` the client will wait for. Past that, it raises
45
45
  `RateLimitError`. Use `max_retries=0` to disable retrying.
46
46
 
47
+ ## Using another API host
48
+
49
+ `base_url` sets the API root, which is `https://api.quickbase.com/v1` by default. The right host for EU
50
+ realms hasn't been confirmed, so pass `base_url` if yours differs. It must use HTTPS, except for
51
+ loopback addresses.
52
+
47
53
  ## Proxies, TLS, and logging
48
54
 
49
55
  Pass your own [httpx](https://www.python-httpx.org/) client for proxies, custom TLS, event hooks, or
@@ -53,6 +53,8 @@ selects all of its fields. A record's `repr` leaves out its values, because they
53
53
  | `file` | `dict[str, Any]`, as described in [File attachments](files.md) |
54
54
  | Anything else | `Any` |
55
55
 
56
+ Numbers are `float`, as in `Record`, so currency values aren't exact decimals.
57
+
56
58
  ## Writing
57
59
 
58
60
  `Tasks.new()` builds a row for a new record, and `Tasks.change()` builds one for an existing record.
@@ -75,9 +77,18 @@ take a `FileUpload`.
75
77
  ## Keeping it up to date
76
78
 
77
79
  A generated class is compared with its table the first time it's used, and `SchemaMismatchError` lists
78
- what's different if a field was deleted, changed type, or became writable or read-only. Reads and writes
79
- stop rather than risk returning wrong types. Run the command again to fix it. Adding a field or renaming
80
- one doesn't cause an error, because the class uses field IDs.
80
+ what's different if a field was deleted, changed type, or became read-only. Reads and writes stop rather
81
+ than risk returning wrong types. Run the command again to fix it. Adding a field, renaming one, or a
82
+ field becoming writable doesn't cause an error, because the class uses field IDs and is still safe.
83
+
84
+ That first check uses cached metadata, 5 minutes old at most by default (see `schema_ttl`), and later
85
+ changes aren't noticed. A long-running process can call `tasks.check_schema()` from time to time, which
86
+ always fetches the fields again.
87
+
88
+ **Restricted users.** Quickbase leaves out fields a user's role can't see, so a hidden field looks the
89
+ same as a deleted one, and the class raises `SchemaMismatchError` for that user. Generate a class with a
90
+ token that has the same access as the code using it. A class generated by the most restricted user also
91
+ works for anyone who can see more.
81
92
 
82
93
  To catch changes sooner, run this in CI. It changes nothing, and fails if the file is out of date.
83
94
 
@@ -100,3 +111,8 @@ turned into underscores, so a label can't inject code into the module.
100
111
  If two labels give the same name, such as `Due Date` and `Due-Date`, the field with the lowest ID keeps
101
112
  it and the other gets its ID added: `due_date_9`. A new field therefore never takes an existing field's
102
113
  name. Record ID# is always `id`. Renaming a label renames its attribute the next time you generate.
114
+
115
+ **Names can move.** If the field holding a plain name is deleted, the next field with that name takes it
116
+ when you generate again, so `due_date` can come to mean a different field with no error. Class names work
117
+ the same way, and a clash between them is settled only among the tables you generate, so `--table` can
118
+ change a class's name. Review the diff when you generate again, and use `--check` in CI.
@@ -1,6 +1,8 @@
1
1
  # Handling errors
2
2
 
3
- Everything raised by this library derives from `QuickbaseError`.
3
+ Every exception this library defines derives from `QuickbaseError`. Reading a missing key from a
4
+ `Record`, including `record.id` on a report row without Record ID#, raises Python's `KeyError`, as any
5
+ mapping does.
4
6
 
5
7
  ```python
6
8
  from pyquickbase import AuthenticationError, QuickbaseError, RateLimitError
@@ -26,7 +28,7 @@ These mean nothing was sent to Quickbase.
26
28
  | `InvalidValueError` | A row, value, or file request that can't be sent |
27
29
  | `SchemaMismatchError` | A generated class no longer matches its table |
28
30
  | `UnknownTableError` | No table with that name or ID in this app |
29
- | `UnknownFieldError` | No field with that label or ID in this table |
31
+ | `UnknownFieldError` | No field with that label or ID in this table, or the token's role can't see it |
30
32
  | `UnknownReportError` | No report with that name or ID on this table, or several with that name |
31
33
  | `UnsupportedReportError` | A report that can't be read as records, such as a summary or chart |
32
34
  | `ReadOnlyError` | The client isn't allowed to send this request |
@@ -50,6 +52,10 @@ and `request_id`. Include `request_id` when reporting a problem to Quickbase sup
50
52
  `AuthenticationError` can mean either a bad token or a wrong ID. A bad realm or app can also
51
53
  return a 400.
52
54
 
55
+ !!! warning "Messages can contain HTML"
56
+ Quickbase's formula error messages can include HTML, such as a `javascript:` link, and errors
57
+ pass them on unchanged. Escape them before showing them in a web page.
58
+
53
59
  ## Other failures
54
60
 
55
61
  | Exception | Cause |
@@ -73,8 +73,8 @@ are whatever was uploaded. Treat both as untrusted, especially before opening or
73
73
 
74
74
  ## Listing versions
75
75
 
76
- Reading a file field in a query returns Quickbase's value unchanged. `url` is an empty string, and
77
- `versions` an empty list, when the field has no file.
76
+ Reading a file field in a query returns Quickbase's value unchanged, as a plain dictionary. `url` is an
77
+ empty string, and `versions` an empty list, when the field has no file.
78
78
 
79
79
  ```python
80
80
  task = next(iter(tasks.query(where="{3.EX.7}", select=["Attachment"])))
@@ -144,9 +144,13 @@ print(len(page.records), page.total, page.has_more)
144
144
  The report decides the filter, sort, and columns:
145
145
 
146
146
  - **Record ID# may be missing.** `record.id` raises `KeyError` if the report doesn't include that
147
- column, and the default `List All` report doesn't.
147
+ column, and the default `List All` report doesn't. To check first, use `3 in record`, because
148
+ `hasattr(record, "id")` raises `KeyError` too.
148
149
  - **Pages may not be stable.** `query()` adds Record ID# as the last sort key to keep pages stable,
149
- but a report's sort can't be changed. If that matters, prefer `query()`.
150
+ but a report's sort can't be changed. Ties stayed in Record ID# order in a 1,200-record test, but
151
+ that isn't confirmed for larger tables or while records change. If it matters, prefer `query()`.
152
+ - **Columns can be missing for restricted users.** A column the user's role can't see is left out with
153
+ no error, so `record["Salary"]` raises `KeyError` for them.
150
154
  - **Report IDs are only unique within a table.** Different tables reuse the same IDs, so always get a
151
155
  report from its own table.
152
156
  - **Names can repeat.** A repeated name raises `UnknownReportError`, which lists the IDs to choose
@@ -160,4 +164,5 @@ ask-the-user reports through the API.
160
164
 
161
165
  Running a report adds to its usage count in Quickbase, even if the run fails, but changes no records.
162
166
  Report details come from the same cache as other metadata, so `usedCount` in `raw` can be out of
163
- date.
167
+ date, and so can the details used to check that a report is a table report. Quickbase's UI doesn't let
168
+ you change a saved report's type.
@@ -3,9 +3,9 @@
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
- !!! warning "Alpha"
7
- This project is in early alpha, so anything may change before 1.0. Pin the exact version you
8
- install. It isn't affiliated with or endorsed by Quickbase, Inc.
6
+ !!! warning "Release candidate"
7
+ This is a release candidate, so small changes are still possible before 1.0. Pin the exact version
8
+ you install. It isn't affiliated with or endorsed by Quickbase, Inc.
9
9
 
10
10
  ```python
11
11
  from datetime import date
@@ -39,7 +39,7 @@ writing `null` to a field does nothing. This SDK turns those into errors or corr
39
39
  ## Install
40
40
 
41
41
  ```bash
42
- pip install pyquickbase==0.2.0a1
42
+ pip install pyquickbase==1.0.0rc1
43
43
  ```
44
44
 
45
45
  It needs Python 3.11 or newer. Next, see [Getting started](getting-started.md).
@@ -1,6 +1,7 @@
1
1
  # API reference
2
2
 
3
- Everything listed here is public. Modules starting with `_` are private and may change at any time.
3
+ Everything listed here is public, as described in [Stability and versioning](stability.md). Modules
4
+ starting with `_` are private and may change at any time.
4
5
 
5
6
  ## Clients
6
7
 
@@ -0,0 +1,49 @@
1
+ # Stability and versioning
2
+
3
+ This policy applies from version 1.0. Until then, anything may change in any release.
4
+
5
+ From 1.0, pyquickbase follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Breaking
6
+ changes come only in a major version, and anything removed first gets a `DeprecationWarning` in a
7
+ minor release.
8
+
9
+ ## What's covered
10
+
11
+ - Every name in `pyquickbase.__all__` and `pyquickbase.errors`, with its documented methods,
12
+ signatures, return types, and defaults.
13
+ - The `pyquickbase` command and its options.
14
+ - The promise that a `read_only` client changes no records. It can still run reports, which adds to
15
+ their usage count in Quickbase, and formulas.
16
+ - File and user values staying plain dictionaries. Their contents come from Quickbase.
17
+
18
+ ## What isn't covered
19
+
20
+ - Modules and names starting with `_`.
21
+ - Constructors of objects the client creates for you, such as `App`, `Table`, and `Report`. Use
22
+ `qb.app()`, `app.table()`, and `table.report()` instead.
23
+ - The wording of error messages and `repr`s, so don't parse them.
24
+ - Quickbase's own behavior, including any change it makes to its responses, and the contents of `raw`
25
+ attributes.
26
+
27
+ ## Generated code
28
+
29
+ A file generated by any 1.x version works on every later 1.x version, so upgrading doesn't force you
30
+ to generate it again.
31
+
32
+ Generating it again can rename attributes, when a label changes or a field with the same name is
33
+ deleted. See [how names are chosen](guides/codegen.md#how-names-are-chosen). Run
34
+ `pyquickbase codegen --check` in CI to catch this.
35
+
36
+ ## Minor and patch releases
37
+
38
+ A minor release can add features, optional arguments, and support for more field types. A generated
39
+ attribute typed `Any` can become more specific, but only once you generate the file again.
40
+
41
+ A patch release only fixes bugs.
42
+
43
+ ## Dependencies and Python versions
44
+
45
+ A Python version is dropped only after it reaches end of life, and only in a minor release. pip
46
+ won't install a release on a Python version it doesn't support, so older Python versions keep the last
47
+ release that worked.
48
+
49
+ `httpx` stays below version 1 until a major version of pyquickbase, because `http_client` exposes it.
@@ -39,6 +39,7 @@ nav:
39
39
  - Handling errors: guides/errors.md
40
40
  - Configuring clients: guides/clients.md
41
41
  - API reference: reference.md
42
+ - Stability and versioning: stability.md
42
43
  - Quickbase API notes: api-notes.md
43
44
  - Changelog: changelog.md
44
45
 
@@ -9,7 +9,7 @@ authors = [{ name = "Matthew Williams" }]
9
9
  requires-python = ">=3.11"
10
10
  keywords = ["quickbase", "api", "sdk", "client", "async"]
11
11
  classifiers = [
12
- "Development Status :: 3 - Alpha",
12
+ "Development Status :: 4 - Beta",
13
13
  "Framework :: AnyIO",
14
14
  "Intended Audience :: Developers",
15
15
  "Programming Language :: Python :: 3 :: Only",