pyaccesskit 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.
Files changed (86) hide show
  1. pyaccesskit/AGENT_GUIDE.md +455 -0
  2. pyaccesskit/__init__.py +167 -0
  3. pyaccesskit/__main__.py +6 -0
  4. pyaccesskit/_backends/__init__.py +0 -0
  5. pyaccesskit/_backends/access/__init__.py +1 -0
  6. pyaccesskit/_backends/access/design.py +415 -0
  7. pyaccesskit/_backends/dao/__init__.py +1 -0
  8. pyaccesskit/_backends/dao/profile.py +40 -0
  9. pyaccesskit/_backends/dao/schema.py +805 -0
  10. pyaccesskit/_backends/dao/typemap.py +390 -0
  11. pyaccesskit/_backends/fake/__init__.py +3 -0
  12. pyaccesskit/_backends/fake/backend.py +680 -0
  13. pyaccesskit/_backends/protocols.py +339 -0
  14. pyaccesskit/_com/__init__.py +1 -0
  15. pyaccesskit/_com/constants.py +394 -0
  16. pyaccesskit/_com/dispatch.py +50 -0
  17. pyaccesskit/_com/errors.py +184 -0
  18. pyaccesskit/_com/gateway.py +199 -0
  19. pyaccesskit/_com/raw.py +164 -0
  20. pyaccesskit/_com/runtime.py +39 -0
  21. pyaccesskit/_com/variants.py +72 -0
  22. pyaccesskit/_engines/__init__.py +48 -0
  23. pyaccesskit/_engines/access.py +300 -0
  24. pyaccesskit/_engines/inproc.py +148 -0
  25. pyaccesskit/_engines/probe.py +231 -0
  26. pyaccesskit/_ledger.py +158 -0
  27. pyaccesskit/_ops/__init__.py +0 -0
  28. pyaccesskit/_ops/design.py +127 -0
  29. pyaccesskit/_ops/schema.py +471 -0
  30. pyaccesskit/_session/__init__.py +1 -0
  31. pyaccesskit/_session/protocols.py +78 -0
  32. pyaccesskit/_session/session.py +354 -0
  33. pyaccesskit/_text/__init__.py +0 -0
  34. pyaccesskit/_text/codec.py +114 -0
  35. pyaccesskit/_version.py +3 -0
  36. pyaccesskit/_win/__init__.py +1 -0
  37. pyaccesskit/_win/access_process.py +348 -0
  38. pyaccesskit/_win/console.py +56 -0
  39. pyaccesskit/_win/inspector.py +53 -0
  40. pyaccesskit/_win/job.py +65 -0
  41. pyaccesskit/_win/processes.py +159 -0
  42. pyaccesskit/_win/watchdog.py +253 -0
  43. pyaccesskit/cli/__init__.py +10 -0
  44. pyaccesskit/cli/_output.py +101 -0
  45. pyaccesskit/cli/agent.py +99 -0
  46. pyaccesskit/cli/app.py +54 -0
  47. pyaccesskit/cli/cleanup.py +56 -0
  48. pyaccesskit/cli/doctor.py +101 -0
  49. pyaccesskit/cli/inspection.py +223 -0
  50. pyaccesskit/database.py +296 -0
  51. pyaccesskit/diagnostics.py +319 -0
  52. pyaccesskit/enums.py +258 -0
  53. pyaccesskit/errors.py +407 -0
  54. pyaccesskit/forms/__init__.py +45 -0
  55. pyaccesskit/forms/builder.py +295 -0
  56. pyaccesskit/forms/collection.py +117 -0
  57. pyaccesskit/forms/controls.py +157 -0
  58. pyaccesskit/forms/layout.py +300 -0
  59. pyaccesskit/forms/spec.py +169 -0
  60. pyaccesskit/forms/vba.py +138 -0
  61. pyaccesskit/maintenance.py +32 -0
  62. pyaccesskit/modules.py +101 -0
  63. pyaccesskit/objects.py +81 -0
  64. pyaccesskit/options.py +40 -0
  65. pyaccesskit/properties.py +74 -0
  66. pyaccesskit/py.typed +0 -0
  67. pyaccesskit/queries.py +190 -0
  68. pyaccesskit/relationships.py +143 -0
  69. pyaccesskit/schema/__init__.py +73 -0
  70. pyaccesskit/schema/_base.py +55 -0
  71. pyaccesskit/schema/_reserved_words.py +55 -0
  72. pyaccesskit/schema/columns.py +609 -0
  73. pyaccesskit/schema/compat.py +57 -0
  74. pyaccesskit/schema/expressions.py +162 -0
  75. pyaccesskit/schema/indexes.py +114 -0
  76. pyaccesskit/schema/names.py +122 -0
  77. pyaccesskit/schema/queries.py +192 -0
  78. pyaccesskit/schema/relationships.py +132 -0
  79. pyaccesskit/schema/tables.py +178 -0
  80. pyaccesskit/tables.py +333 -0
  81. pyaccesskit/units.py +301 -0
  82. pyaccesskit-0.1.0.dist-info/METADATA +201 -0
  83. pyaccesskit-0.1.0.dist-info/RECORD +86 -0
  84. pyaccesskit-0.1.0.dist-info/WHEEL +4 -0
  85. pyaccesskit-0.1.0.dist-info/entry_points.txt +2 -0
  86. pyaccesskit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,455 @@
1
+ # PyAccessKit — guide for AI coding agents
2
+
3
+ You are writing Python that builds or changes a Microsoft Access application with PyAccessKit. This guide
4
+ is the authoritative, compact reference for that job. Print it any time with `pyaccesskit guide`. Follow the
5
+ rules below exactly; they encode Access behaviour that is not obvious and that you cannot observe directly.
6
+
7
+ ## 1. Workflow
8
+
9
+ 1. **Check the machine** once: `pyaccesskit doctor --json`. Continue only if `"usable": true`.
10
+ `"auto_engine"` says what `engine="auto"` uses (`"dao"` or `"access"`). Forms, modules and text
11
+ import/export need full Microsoft Access (`engines[].engine == "access"` available).
12
+ 2. **Understand an existing database** before changing it: `pyaccesskit inspect DB --json` (read-only, runs
13
+ no startup code). Its `tables[].spec` entries are valid `TableSpec` JSON.
14
+ 3. **Write the program** in three parts (see the complete example in section 9):
15
+ *specs as data* → *one build function inside one `with` block* → *verification after reopening*.
16
+ 4. **Run it**, read errors (section 7), fix, rerun. Building with `AccessDatabase.create(...)` is atomic,
17
+ so a failed run leaves nothing behind and can simply be repeated.
18
+ 5. **Verify**: `to_spec() == spec.normalized()` for tables, `fetch()` for queries, `check_opens()` for every
19
+ form (this also compiles VBA).
20
+
21
+ Validate specs without Access: `pyaccesskit schema table` (JSON Schema), or construct the spec in Python —
22
+ invalid specs raise `SpecError` listing every problem.
23
+
24
+ ## 2. Rules (MUST / NEVER)
25
+
26
+ Sessions and processes
27
+ - MUST open databases only with `with AccessDatabase.create(path, ...) as db:` or
28
+ `with AccessDatabase.open(path, ...) as db:`. Everything is cleaned up on exit, also on exceptions.
29
+ - NEVER use `win32com.client.Dispatch`, `GetObject`, `DispatchEx` or `pythoncom` for Access. NEVER kill
30
+ `MSACCESS.EXE` processes. PyAccessKit owns and closes its own Access processes; if a crash left one
31
+ behind, run `pyaccesskit cleanup`.
32
+ - NEVER call `Quit`, `Close`, `CloseCurrentDatabase` on `db.raw.*` objects. Avoid `db.raw` unless the API
33
+ truly lacks a feature.
34
+ - Use a session only on the thread that opened it. Do not open the same file in two sessions at once.
35
+ - `create()` refuses to replace an existing file unless `overwrite=True`.
36
+
37
+ Schema
38
+ - `Column.number(...)` is a Long Integer. Foreign keys that point to an `autonumber` use `Column.number`.
39
+ - Defaults: Python values (`default=0`, `default=True`, `default="pcs"`, `default=date(2026, 1, 1)`).
40
+ Access expressions MUST be wrapped: `default=Expr("Now()")`, `default=Expr("Date()")`.
41
+ - Text length is 1–255 (`Column.text`); use `Column.long_text` for more. Decimal needs
42
+ `precision` 1–28 and `scale <= precision`.
43
+ - At most one AutoNumber per table; name it `<Table>ID` by convention. Primary key index is named
44
+ `PrimaryKey`; `unique=True`/`indexed=True` create an index named after the column.
45
+ - Names: ≤ 64 chars; no `.`, `!`, `` ` ``, `[`, `]`; no leading space. Names are case-insensitive:
46
+ `Name` and `name` clash. AVOID reserved words and names like `Name`, `Date`, `Time`, `Value`, `Order`,
47
+ `Level`, `Section`, `Note`, `Description`, `Type`, `Year`, `Month`, `User`, `Text` — use
48
+ `CustomerName`, `OrderDate`, … PyAccessKit warns (`AccessNameWarning`); treat that warning as an error.
49
+ - Create tables before relationships, relationships before data, data before forms that need it.
50
+
51
+ SQL
52
+ - Access SQL (Jet/ACE), not ANSI/T-SQL: `TOP n` (no `LIMIT`), `IIf()` (no `CASE`), `&` concatenates strings,
53
+ dates are `#2026-01-31#`, booleans `True`/`False`, `LIKE` wildcards are `*` and `?`, several joins need
54
+ parentheses: `FROM (A INNER JOIN B ON ...) INNER JOIN C ON ...`.
55
+ - NEVER build SQL with f-strings or `+`. Pass values as parameters: SQL `[name]`, Python
56
+ `db.execute(sql, {"name": value})`. Saved queries may declare `PARAMETERS [pX] Long;`.
57
+ - SQL that Python runs (`db.execute`, `db.fetch_all`, `Query.fetch`) goes through DAO outside the Access UI:
58
+ use only engine functions (`IIf`, `IsNull`, `Sum`, `Format`, `Left`, `DateAdd`, `Year`…). `Nz()` and your
59
+ own VBA functions are NOT available there (DAO error 3085). They do work in form control sources.
60
+
61
+ Forms and VBA
62
+ - Forms need full Access. Build them with `with db.forms.create(name, record_source=..., ...) as form:`;
63
+ the form is saved when the block ends. Reuse a name only with `replace=True`.
64
+ - Buttons MUST have a name (`form.button("cmdSave", caption=..., on_click=Vba(...))`). Controls with events
65
+ need plain VBA identifier names.
66
+ - `Vba("...")` holds a procedure **body** only — never `Sub`/`End Sub`. Put helpers and module-level
67
+ variables in `form.module_code("...")` or in a standard module (`db.modules.create`).
68
+ - VBA is case-insensitive: a constant `PLATFORM` and a function `Platform` clash and the module will not
69
+ compile (reported later as Access error 7960 or an `AccessDialogError`). Keep all names in a module
70
+ unique ignoring case. `Option Compare Database` / `Option Explicit` are added for you.
71
+ - VBA text must be representable in the Windows ANSI code page (no emoji); otherwise `SpecError`.
72
+ - `default_view=FormView.CONTINUOUS` uses a tabular layout: labels go in the form header, so do not combine
73
+ it with `header=False` unless every control has `label=False`.
74
+
75
+ ## 3. Program skeleton
76
+
77
+ ```python
78
+ from pathlib import Path
79
+
80
+ from pyaccesskit import AccessDatabase, Column, TableSpec
81
+
82
+ CUSTOMERS = TableSpec(
83
+ name="Customers",
84
+ columns=[
85
+ Column.autonumber("CustomerID", primary_key=True),
86
+ Column.text("CustomerName", length=120, required=True),
87
+ ],
88
+ )
89
+
90
+
91
+ def build(db: AccessDatabase) -> None:
92
+ db.tables.create(CUSTOMERS)
93
+
94
+
95
+ def verify(path: Path) -> None:
96
+ with AccessDatabase.open(path, readonly=True) as db:
97
+ assert db.tables["Customers"].to_spec() == CUSTOMERS.normalized()
98
+
99
+
100
+ def main(path: Path) -> None:
101
+ with AccessDatabase.create(path, overwrite=True) as db:
102
+ build(db)
103
+ verify(path)
104
+ ```
105
+
106
+ To change an existing database, open it writable and apply the change, then verify:
107
+
108
+ ```python
109
+ from pyaccesskit import AccessDatabase, Column
110
+
111
+ with AccessDatabase.open("app.accdb") as db:
112
+ if "Phone" not in db.tables["Customers"].fields:
113
+ db.tables["Customers"].add_column(Column.text("Phone", length=30))
114
+ ```
115
+
116
+ Open changes are not atomic across steps: back up the file first (copy it) when a change is risky.
117
+
118
+ ## 4. API cheat sheet
119
+
120
+ ```text
121
+ AccessDatabase.create(path, *, overwrite=False, atomic=True, engine="auto", options=None)
122
+ AccessDatabase.open(path, *, readonly=False, exclusive=None, password=None, engine="auto", options=None)
123
+ db.path db.transport db.format_version db.readonly db.is_open db.access_pid
124
+ db.execute(sql, params=None) -> int # action SQL, returns affected rows
125
+ db.fetch_all(sql, params=None, *, limit=None) -> list[dict]
126
+ db.properties["AppTitle"] = "..." ; db.properties["StartUpForm"] = "frmMain" ; .get(name, default)
127
+
128
+ db.tables: names() | [name] | get(name) | in | len | iter | specs()
129
+ create(TableSpec) | create(name, *, columns, indexes=(), primary_key=None, description=None,
130
+ validation_rule=None, validation_text=None, properties=None)
131
+ drop(name, *, drop_relationships=False)
132
+ Table: name, fields, indexes, primary_key, description (settable), is_linked, properties, record_count(),
133
+ to_spec(), add_column(col), drop_column(name), rename_column(old, new), create_index(IndexSpec),
134
+ drop_index(name), rename(new), drop()
135
+ Field: name, data_type, size, required, spec, properties, rename(new), drop()
136
+
137
+ Column.text(name, *, length=255, allow_zero_length=False, unicode_compression=True, input_mask=None, **common)
138
+ Column.long_text(name, *, rich_text=False, append_only=False, **common)
139
+ Column.number(name, *, size=NumberSize.LONG_INTEGER, decimal_places=None, input_mask=None, **common)
140
+ Column.decimal(name, *, precision=18, scale=0, **common) Column.currency(name, **common)
141
+ Column.autonumber(name, *, replication_id=False, primary_key=..., description=..., caption=...)
142
+ Column.date_time(name, **common) Column.yes_no(name, **common) Column.hyperlink(name, **common)
143
+ Column.ole_object(name, *, required=False, description=..., caption=...)
144
+ common = required, default, validation_rule, validation_text, description, caption, format,
145
+ primary_key, unique, indexed, properties={...}
146
+ IndexSpec.on(name, *columns, unique=False, ignore_nulls=False, required=False) # ("Col", "desc") pairs
147
+ IndexSpec.primary_key(*columns)
148
+ TableSpec(name, columns, indexes=(), primary_key=None|"Col"|["A","B"], description, validation_rule, ...)
149
+
150
+ db.relationships.create("Primary.Col", "Foreign.Col", *, name=None, enforce_integrity=True,
151
+ cascade_update=False, cascade_delete=False, one_to_one=False, join=JoinType.INNER)
152
+ db.relationships.create(RelationshipSpec.between("A.ID", "B.AID", cascade_delete=True))
153
+ composite: create(("A", ["K1", "K2"]), ("B", ["K1", "K2"]))
154
+
155
+ db.queries.create(name, sql, *, description=None, replace=False) | create(QuerySpec, replace=False)
156
+ db.queries.create_pass_through(name, sql, *, connect="ODBC;...", returns_records=True, timeout=60)
157
+ Query: name, sql (settable), kind, parameters, description, execute(params) -> int,
158
+ fetch(params=None, *, limit=None) -> list[dict], rename(new), drop(), to_spec()
159
+
160
+ db.forms.create(name, *, replace=False, **FormSpec options) -> FormBuilder (use as context manager)
161
+ FormSpec options: record_source, caption, default_view (FormView.SINGLE|CONTINUOUS|DATASHEET|SPLIT),
162
+ layout (LayoutKind.AUTO|STACKED|TABULAR|NONE), header, width, allow_additions, allow_edits,
163
+ allow_deletions, data_entry, navigation_buttons, record_selectors, dividing_lines, scroll_bars,
164
+ auto_center, pop_up, modal, option_explicit, properties={...}
165
+ FormBuilder: textbox(field=None, *, label=None|False|"text", control_source=None, format=None, enabled=True,
166
+ locked=False, after_update=None, name=None, section=Section.DETAIL, at=(left, top), width, height,
167
+ visible=True, properties={})
168
+ checkbox(field, *, label, enabled, locked, after_update, ...)
169
+ combobox(field, *, row_source, row_source_type=RowSourceType.TABLE_QUERY|VALUE_LIST, bound_column=1,
170
+ column_count=1, column_widths=[cm(0), cm(4)], limit_to_list=True, label, ...)
171
+ label(caption, ...) button(name, *, caption, on_click=None, section, at, width, height)
172
+ on_load(Vba) on_current(Vba) module_code(str) to_spec() save() discard()
173
+ db.forms.build(FormSpec, replace=False) ; db.forms[name]: controls(), check_opens(), export_text(),
174
+ rename(new), drop()
175
+
176
+ db.modules.create(name, code, *, kind=ModuleKind.STANDARD|CLASS, replace=False) ; Module: code (settable),
177
+ kind, rename(new), drop()
178
+ db.objects: names(kind) export_text(kind, name) import_text(kind, name, text, *, replace=False)
179
+ save_text(kind, name, path) load_text(kind, name, path, *, replace=False) delete(kind, name)
180
+ rename(kind, old, new) kind = "form" | "report" | "macro" | "module" | "query"
181
+
182
+ Units: cm(2), mm(5), inch(1), pt(12), twips(1440); Length supports + - * / and comparisons.
183
+ Enums: FormView single|continuous|datasheet|split; LayoutKind auto|stacked|tabular|none;
184
+ NumberSize byte|integer|long_integer|single|double|replication_id; Section detail|header|footer;
185
+ RowSourceType table_query|value_list; ModuleKind standard|class; JoinType inner|left|right;
186
+ QueryKind select|crosstab|delete|update|append|make_table|ddl|pass_through|union|...
187
+ SessionOptions(visible=False, macro_security=MacroSecurity.DISABLE, dialog_policy=DialogPolicy.FAIL,
188
+ call_timeout=600.0, quit_timeout=30.0, access_progid="Access.Application")
189
+ ```
190
+
191
+ All names above are importable from `pyaccesskit` (form control specs from `pyaccesskit.forms`).
192
+
193
+ ## 5. Values in and out
194
+
195
+ | Access type | Python value written | Python value read |
196
+ |---|---|---|
197
+ | Short/Long Text, Hyperlink | `str` | `str` |
198
+ | Number (Byte/Integer/Long) | `int` | `int` |
199
+ | Number (Single/Double) | `float` | `float` |
200
+ | Decimal, Currency | `Decimal` (or `int`) | `Decimal` |
201
+ | Date/Time | naive `datetime` or `date` (wall-clock) | naive `datetime` |
202
+ | Yes/No | `bool` | `bool` |
203
+ | OLE Object | `bytes` | `bytes` |
204
+ | Replication ID (GUID) | `str` | `str` in DAO's form `"{guid {…}}"` |
205
+ | Null | `None` | `None` |
206
+
207
+ A `time` reads back as a `datetime` on 1899-12-30 (Access's day zero). Aggregates can come back as `float`
208
+ (e.g. `Sum` over integers through `IIf`). `bytes` parameters work in `db.execute`/`db.fetch_all`; a saved
209
+ query that receives bytes must declare the parameter: `PARAMETERS [payload] LongBinary;`.
210
+
211
+ ## 6. Access limits
212
+
213
+ | Limit | Value |
214
+ |---|---|
215
+ | Object and column names | 64 characters |
216
+ | Columns per table | 255 |
217
+ | Indexes per table (including relationship indexes) | 32 |
218
+ | Columns per index | 10 |
219
+ | Short Text length | 255 |
220
+ | Form width / section height | 22 inches (`inch(22)`) |
221
+ | Database size | 2 GB |
222
+ | SQL statement | ~64,000 characters |
223
+
224
+ ## 7. Errors and what to do
225
+
226
+ All exceptions derive from `pyaccesskit.PyAccessKitError`; `str(exc)` says what failed and why;
227
+ `exc.details.number` holds the Access/DAO error number when there is one.
228
+
229
+ | Exception | Typical cause | Fix |
230
+ |---|---|---|
231
+ | `SpecError` | invalid spec, name, option or default | read `exc.problems`, correct the spec |
232
+ | `ObjectExistsError` | name already used (tables and queries share one namespace) | pick another name, or `replace=True` where offered |
233
+ | `ObjectNotFoundError` | wrong table/column/query/form name | check `names()`; lookups are case-insensitive |
234
+ | `RelationshipError` | incompatible key types, no unique index on the primary side | primary side needs a PK/unique index; FK type `Column.number` for AutoNumber keys |
235
+ | `IntegrityViolationError` | duplicate key (3022), missing related row (3201), rows still related (3200) | fix the data or insert parents first |
236
+ | `SqlSyntaxError` | invalid Access SQL (`exc.sql` holds it) | apply the SQL rules in section 2 |
237
+ | `MissingParameterError` | a `[name]` in SQL without a value (often a misspelled column) | pass the parameter or fix the column name |
238
+ | `ComError` 3085 "Undefined function" | `Nz`/VBA function in SQL run from Python | use `IIf(IsNull(x), 0, x)` |
239
+ | `ComError` 7960 / `AccessDialogError` naming VBA | VBA does not compile | fix the module (duplicate names, missing `End Sub`, typos) |
240
+ | `AccessDialogError` | Access showed a modal dialog; `exc.dialogs` has title/text | usually VBA or a broken expression; read the text |
241
+ | `CapabilityError` | design feature with `engine="dao"` or `readonly=True` | use `engine="auto"`/`"access"`, open writable |
242
+ | `AccessRuntimeOnlyError` | only the Access Runtime is installed | forms/modules impossible; schema and data still work |
243
+ | `DatabaseLockedError` | file open elsewhere (exclusive) | close it in Access, or open `readonly=True` |
244
+ | `DatabaseExistsError` | target exists | `overwrite=True`, or another path |
245
+ | `AccessTimeoutError` | one call exceeded `call_timeout`; the owned Access was ended | split the work, raise `SessionOptions(call_timeout=...)` |
246
+ | `EngineUnavailableError` | no usable engine | run `pyaccesskit doctor` |
247
+
248
+ Warnings: `AccessNameWarning` (troublesome name) — rename; `AccessDialogWarning` (with
249
+ `dialog_policy="warn"`).
250
+
251
+ ## 8. Not supported yet (do not attempt through PyAccessKit)
252
+
253
+ Reports, subforms, tab controls, list boxes, option groups, attachment / calculated / multi-value / lookup
254
+ columns, creating linked tables, macros (other than raw text import), ribbons, VBA references, compiling on
255
+ demand, encrypted database creation, changing a column's type or order (create a new table and copy rows
256
+ with `INSERT INTO ... SELECT` instead). For anything essential, `db.raw.access` / `db.raw.dao` expose the
257
+ underlying objects; save your own raw design changes.
258
+
259
+ ## 9. Complete example
260
+
261
+ A small inventory application: three related tables, two queries (one with a parameter), a VBA module using
262
+ conditional compilation, a single form with lookups and events, a continuous form, startup settings, and
263
+ verification. It is `examples/04_inventory_app.py` in the source tree and is run by the test-suite.
264
+
265
+ <!-- example:start -->
266
+ ```python
267
+ """A complete small Access application, written the way the agent guide recommends.
268
+
269
+ Run: python examples/04_inventory_app.py [path/to/inventory.accdb]
270
+
271
+ Structure: (1) the schema as immutable specs, (2) one build function, (3) verification that reopens the
272
+ file and checks what was built. Everything happens in one atomic ``create()``: if any step fails, no file
273
+ is left behind and the Access process PyAccessKit started is closed.
274
+ """
275
+
276
+ from __future__ import annotations
277
+
278
+ import sys
279
+ from decimal import Decimal
280
+ from pathlib import Path
281
+
282
+ from pyaccesskit import (
283
+ AccessDatabase,
284
+ Column,
285
+ Expr,
286
+ FormView,
287
+ NumberSize,
288
+ RelationshipSpec,
289
+ RowSourceType,
290
+ TableSpec,
291
+ Vba,
292
+ cm,
293
+ )
294
+
295
+ # --- 1. Schema as data ---------------------------------------------------------------------------
296
+ TABLES = [
297
+ TableSpec(
298
+ name="Categories",
299
+ columns=[
300
+ Column.autonumber("CategoryID", primary_key=True),
301
+ Column.text("CategoryName", length=60, required=True, unique=True),
302
+ ],
303
+ ),
304
+ TableSpec(
305
+ name="Products",
306
+ description="Everything we stock",
307
+ columns=[
308
+ Column.autonumber("ProductID", primary_key=True),
309
+ Column.text("ProductName", length=100, required=True, indexed=True),
310
+ Column.number("CategoryID", required=True),
311
+ Column.text("Unit", length=10, required=True, default="pcs"),
312
+ Column.currency("UnitCost", default=0, validation_rule=">=0"),
313
+ Column.number("ReorderLevel", size=NumberSize.INTEGER, default=5),
314
+ Column.yes_no("Discontinued", default=False),
315
+ ],
316
+ ),
317
+ TableSpec(
318
+ name="StockMoves",
319
+ columns=[
320
+ Column.autonumber("MoveID", primary_key=True),
321
+ Column.number("ProductID", required=True),
322
+ Column.date_time("MovedAt", default=Expr("Now()"), format="General Date"),
323
+ Column.number(
324
+ "Quantity",
325
+ required=True,
326
+ validation_rule="<>0",
327
+ validation_text="Use a positive number for receipts, negative for issues",
328
+ ),
329
+ Column.text("Reference", length=50),
330
+ ],
331
+ ),
332
+ ]
333
+ RELATIONSHIPS = [
334
+ RelationshipSpec.between("Categories.CategoryID", "Products.CategoryID"),
335
+ RelationshipSpec.between("Products.ProductID", "StockMoves.ProductID", cascade_delete=True),
336
+ ]
337
+ QUERIES = {
338
+ "qryStockLevels": (
339
+ # Only database-engine functions here: Nz() and VBA functions exist inside Access, not in DAO.
340
+ "SELECT p.ProductID, p.ProductName, p.ReorderLevel,\n"
341
+ "IIf(IsNull(Sum(m.Quantity)), 0, Sum(m.Quantity)) AS OnHand\n"
342
+ "FROM Products AS p LEFT JOIN StockMoves AS m ON p.ProductID = m.ProductID\n"
343
+ "GROUP BY p.ProductID, p.ProductName, p.ReorderLevel;"
344
+ ),
345
+ "qryLowStock": (
346
+ "PARAMETERS [pMargin] Long;\n"
347
+ "SELECT ProductName, OnHand, ReorderLevel FROM qryStockLevels\n"
348
+ "WHERE OnHand <= ReorderLevel + [pMargin] ORDER BY ProductName;"
349
+ ),
350
+ }
351
+ INVENTORY_MODULE = """\
352
+ Private lastRefresh As Date
353
+
354
+ #If Win64 Then
355
+ Private Const PLATFORM_NAME As String = "64-bit Office"
356
+ #Else
357
+ Private Const PLATFORM_NAME As String = "32-bit Office"
358
+ #End If
359
+
360
+ Public Function OnHand(ByVal productID As Long) As Long
361
+ OnHand = Nz(DSum("Quantity", "StockMoves", "ProductID=" & productID), 0)
362
+ lastRefresh = Now()
363
+ End Function
364
+
365
+ Public Function Platform() As String
366
+ Platform = PLATFORM_NAME
367
+ End Function
368
+ """
369
+
370
+
371
+ # --- 2. Build ------------------------------------------------------------------------------------
372
+ def build(db: AccessDatabase) -> None:
373
+ for table in TABLES:
374
+ db.tables.create(table)
375
+ for relationship in RELATIONSHIPS:
376
+ db.relationships.create(relationship)
377
+ for name, sql in QUERIES.items():
378
+ db.queries.create(name, sql)
379
+
380
+ for category in ("Stationery", "Hardware"):
381
+ db.execute("INSERT INTO Categories (CategoryName) VALUES ([name])", {"name": category})
382
+ products = [("Notebook", 1, Decimal("1.20")), ("Stapler", 2, Decimal("6.50"))]
383
+ for name, category, cost in products:
384
+ db.execute(
385
+ "INSERT INTO Products (ProductName, CategoryID, UnitCost) VALUES ([n], [c], [cost])",
386
+ {"n": name, "c": category, "cost": cost},
387
+ )
388
+ db.execute("INSERT INTO StockMoves (ProductID, Quantity, Reference) VALUES (1, 40, 'PO-1')")
389
+ db.execute("INSERT INTO StockMoves (ProductID, Quantity, Reference) VALUES (2, 3, 'PO-2')")
390
+
391
+ db.modules.create("modInventory", INVENTORY_MODULE)
392
+
393
+ with db.forms.create(
394
+ "frmProducts", record_source="Products", caption="Products", width=cm(16)
395
+ ) as form:
396
+ form.textbox("ProductName", label="Product", width=cm(8))
397
+ form.combobox(
398
+ "CategoryID",
399
+ label="Category",
400
+ row_source="SELECT CategoryID, CategoryName FROM Categories ORDER BY CategoryName",
401
+ column_count=2,
402
+ column_widths=[cm(0), cm(5)],
403
+ )
404
+ form.combobox(
405
+ "Unit", row_source='"pcs";"box";"kg"', row_source_type=RowSourceType.VALUE_LIST
406
+ )
407
+ form.textbox("UnitCost", label="Unit cost", format="Currency")
408
+ form.textbox(
409
+ name="txtOnHand",
410
+ label="On hand",
411
+ control_source="=OnHand([ProductID])",
412
+ locked=True,
413
+ enabled=False,
414
+ )
415
+ form.checkbox("Discontinued")
416
+ form.button("cmdClose", caption="Close", on_click=Vba("DoCmd.Close acForm, Me.Name"))
417
+ form.on_current(Vba("Me.txtOnHand.Requery"))
418
+
419
+ with db.forms.create(
420
+ "frmStockMoves",
421
+ record_source="SELECT * FROM StockMoves ORDER BY MovedAt DESC",
422
+ caption="Stock moves",
423
+ default_view=FormView.CONTINUOUS,
424
+ ) as form:
425
+ form.textbox("MovedAt", width=cm(4))
426
+ form.textbox("ProductID", width=cm(2))
427
+ form.textbox("Quantity", width=cm(2))
428
+ form.textbox("Reference", width=cm(4))
429
+
430
+ db.properties["AppTitle"] = "Inventory"
431
+ db.properties["StartUpForm"] = "frmProducts"
432
+
433
+
434
+ # --- 3. Verify -----------------------------------------------------------------------------------
435
+ def verify(path: Path) -> None:
436
+ with AccessDatabase.open(path) as db:
437
+ for table in TABLES: # what Access stored is exactly what was specified
438
+ assert db.tables[table.name].to_spec() == table.normalized(), table.name
439
+ low = db.queries["qryLowStock"].fetch({"pMargin": 0})
440
+ assert [row["ProductName"] for row in low] == ["Stapler"], low
441
+ for name in db.forms.names():
442
+ db.forms[name].check_opens() # compiles the form's module and opens it hidden
443
+ print(f"verified {path}: {db.tables.names()}, forms {db.forms.names()}, low stock {low}")
444
+
445
+
446
+ def main(target: Path) -> None:
447
+ with AccessDatabase.create(target, overwrite=True) as db:
448
+ build(db)
449
+ verify(target)
450
+
451
+
452
+ if __name__ == "__main__":
453
+ main(Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd() / "inventory.accdb")
454
+ ```
455
+ <!-- example:end -->
@@ -0,0 +1,167 @@
1
+ """PyAccessKit: a modern, typed, Pythonic toolkit for Microsoft Access databases and applications.
2
+
3
+ Quick start::
4
+
5
+ from pyaccesskit import AccessDatabase, Column
6
+
7
+ with AccessDatabase.create("crm.accdb") as db:
8
+ db.tables.create(
9
+ "Customers",
10
+ columns=[
11
+ Column.autonumber("CustomerID", primary_key=True),
12
+ Column.text("CustomerName", length=200, required=True),
13
+ ],
14
+ )
15
+ """
16
+
17
+ from pyaccesskit._version import __version__
18
+ from pyaccesskit.database import AccessDatabase
19
+ from pyaccesskit.diagnostics import Diagnosis, diagnose
20
+ from pyaccesskit.enums import (
21
+ ControlKind,
22
+ DataType,
23
+ DialogPolicy,
24
+ Engine,
25
+ FormView,
26
+ JoinType,
27
+ LayoutKind,
28
+ MacroSecurity,
29
+ ModuleKind,
30
+ NumberSize,
31
+ ObjectKind,
32
+ PropertyType,
33
+ QueryKind,
34
+ RowSourceType,
35
+ ScrollBars,
36
+ Section,
37
+ Transport,
38
+ )
39
+ from pyaccesskit.errors import (
40
+ AccessApplicationError,
41
+ AccessDialogError,
42
+ AccessDialogWarning,
43
+ AccessNameWarning,
44
+ AccessNotInstalledError,
45
+ AccessProcessDiedError,
46
+ AccessRuntimeOnlyError,
47
+ AccessTimeoutError,
48
+ CapabilityError,
49
+ CleanupError,
50
+ ComError,
51
+ DaoNotAvailableError,
52
+ DatabaseError,
53
+ DatabaseExistsError,
54
+ DatabaseLockedError,
55
+ DatabaseNotFoundError,
56
+ EngineUnavailableError,
57
+ EnvironmentProblem,
58
+ IntegrityViolationError,
59
+ InvalidPasswordError,
60
+ MissingParameterError,
61
+ ObjectError,
62
+ ObjectExistsError,
63
+ ObjectInUseError,
64
+ ObjectNotFoundError,
65
+ PyAccessKitError,
66
+ QueryError,
67
+ ReadOnlyError,
68
+ RelationshipError,
69
+ SchemaError,
70
+ SessionClosedError,
71
+ SessionError,
72
+ SpecError,
73
+ SqlSyntaxError,
74
+ UnrecognizedFormatError,
75
+ WrongThreadError,
76
+ )
77
+ from pyaccesskit.forms import FormBuilder, FormSpec, Vba
78
+ from pyaccesskit.maintenance import reap_orphans
79
+ from pyaccesskit.options import SessionOptions
80
+ from pyaccesskit.schema import (
81
+ Column,
82
+ Expr,
83
+ IndexSpec,
84
+ PassThroughOptions,
85
+ QuerySpec,
86
+ RelationshipSpec,
87
+ TableSpec,
88
+ )
89
+ from pyaccesskit.units import Length, cm, inch, mm, pt, twips
90
+
91
+ __all__ = [
92
+ "AccessApplicationError",
93
+ "AccessDatabase",
94
+ "AccessDialogError",
95
+ "AccessDialogWarning",
96
+ "AccessNameWarning",
97
+ "AccessNotInstalledError",
98
+ "AccessProcessDiedError",
99
+ "AccessRuntimeOnlyError",
100
+ "AccessTimeoutError",
101
+ "CapabilityError",
102
+ "CleanupError",
103
+ "Column",
104
+ "ComError",
105
+ "ControlKind",
106
+ "DaoNotAvailableError",
107
+ "DataType",
108
+ "DatabaseError",
109
+ "DatabaseExistsError",
110
+ "DatabaseLockedError",
111
+ "DatabaseNotFoundError",
112
+ "Diagnosis",
113
+ "DialogPolicy",
114
+ "Engine",
115
+ "EngineUnavailableError",
116
+ "EnvironmentProblem",
117
+ "Expr",
118
+ "FormBuilder",
119
+ "FormSpec",
120
+ "FormView",
121
+ "IndexSpec",
122
+ "IntegrityViolationError",
123
+ "InvalidPasswordError",
124
+ "JoinType",
125
+ "LayoutKind",
126
+ "Length",
127
+ "MacroSecurity",
128
+ "MissingParameterError",
129
+ "ModuleKind",
130
+ "NumberSize",
131
+ "ObjectError",
132
+ "ObjectExistsError",
133
+ "ObjectInUseError",
134
+ "ObjectKind",
135
+ "ObjectNotFoundError",
136
+ "PassThroughOptions",
137
+ "PropertyType",
138
+ "PyAccessKitError",
139
+ "QueryError",
140
+ "QueryKind",
141
+ "QuerySpec",
142
+ "ReadOnlyError",
143
+ "RelationshipError",
144
+ "RelationshipSpec",
145
+ "RowSourceType",
146
+ "SchemaError",
147
+ "ScrollBars",
148
+ "Section",
149
+ "SessionClosedError",
150
+ "SessionError",
151
+ "SessionOptions",
152
+ "SpecError",
153
+ "SqlSyntaxError",
154
+ "TableSpec",
155
+ "Transport",
156
+ "UnrecognizedFormatError",
157
+ "Vba",
158
+ "WrongThreadError",
159
+ "__version__",
160
+ "cm",
161
+ "diagnose",
162
+ "inch",
163
+ "mm",
164
+ "pt",
165
+ "reap_orphans",
166
+ "twips",
167
+ ]
@@ -0,0 +1,6 @@
1
+ """``python -m pyaccesskit`` runs the command-line interface."""
2
+
3
+ from pyaccesskit.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
File without changes
@@ -0,0 +1 @@
1
+ """Access.Application adapter (COM). Imported lazily by the engines."""