pyaccesskit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. pyaccesskit-0.1.0/.gitignore +27 -0
  2. pyaccesskit-0.1.0/CHANGELOG.md +60 -0
  3. pyaccesskit-0.1.0/LICENSE +21 -0
  4. pyaccesskit-0.1.0/PKG-INFO +201 -0
  5. pyaccesskit-0.1.0/README.md +170 -0
  6. pyaccesskit-0.1.0/docs/adr/0001-spike-findings.md +126 -0
  7. pyaccesskit-0.1.0/docs/adr/0002-integration-findings.md +24 -0
  8. pyaccesskit-0.1.0/docs/agents/guide.md +3 -0
  9. pyaccesskit-0.1.0/docs/agents/index.md +67 -0
  10. pyaccesskit-0.1.0/docs/concepts/engines.md +70 -0
  11. pyaccesskit-0.1.0/docs/concepts/lifecycle.md +99 -0
  12. pyaccesskit-0.1.0/docs/concepts/specs.md +77 -0
  13. pyaccesskit-0.1.0/docs/contributing.md +97 -0
  14. pyaccesskit-0.1.0/docs/environment.md +71 -0
  15. pyaccesskit-0.1.0/docs/faq.md +44 -0
  16. pyaccesskit-0.1.0/docs/getting-started.md +129 -0
  17. pyaccesskit-0.1.0/docs/guides/forms.md +127 -0
  18. pyaccesskit-0.1.0/docs/guides/modules.md +36 -0
  19. pyaccesskit-0.1.0/docs/guides/queries.md +65 -0
  20. pyaccesskit-0.1.0/docs/guides/recipes.md +176 -0
  21. pyaccesskit-0.1.0/docs/guides/relationships.md +62 -0
  22. pyaccesskit-0.1.0/docs/guides/tables.md +116 -0
  23. pyaccesskit-0.1.0/docs/guides/text-io.md +39 -0
  24. pyaccesskit-0.1.0/docs/index.md +70 -0
  25. pyaccesskit-0.1.0/docs/llms-full.txt +1929 -0
  26. pyaccesskit-0.1.0/docs/llms.txt +41 -0
  27. pyaccesskit-0.1.0/docs/reference/cli.md +68 -0
  28. pyaccesskit-0.1.0/docs/reference/data-types.md +58 -0
  29. pyaccesskit-0.1.0/docs/reference/errors.md +93 -0
  30. pyaccesskit-0.1.0/docs/releasing.md +73 -0
  31. pyaccesskit-0.1.0/examples/01_create_crm.py +70 -0
  32. pyaccesskit-0.1.0/examples/02_inspect_database.py +47 -0
  33. pyaccesskit-0.1.0/examples/03_build_small_app.py +143 -0
  34. pyaccesskit-0.1.0/examples/04_inventory_app.py +187 -0
  35. pyaccesskit-0.1.0/mkdocs.yml +97 -0
  36. pyaccesskit-0.1.0/pyproject.toml +176 -0
  37. pyaccesskit-0.1.0/scripts/gen_constants.py +240 -0
  38. pyaccesskit-0.1.0/scripts/gen_ref_pages.py +46 -0
  39. pyaccesskit-0.1.0/scripts/sync_docs.py +89 -0
  40. pyaccesskit-0.1.0/src/pyaccesskit/AGENT_GUIDE.md +455 -0
  41. pyaccesskit-0.1.0/src/pyaccesskit/__init__.py +167 -0
  42. pyaccesskit-0.1.0/src/pyaccesskit/__main__.py +6 -0
  43. pyaccesskit-0.1.0/src/pyaccesskit/_backends/__init__.py +0 -0
  44. pyaccesskit-0.1.0/src/pyaccesskit/_backends/access/__init__.py +1 -0
  45. pyaccesskit-0.1.0/src/pyaccesskit/_backends/access/design.py +415 -0
  46. pyaccesskit-0.1.0/src/pyaccesskit/_backends/dao/__init__.py +1 -0
  47. pyaccesskit-0.1.0/src/pyaccesskit/_backends/dao/profile.py +40 -0
  48. pyaccesskit-0.1.0/src/pyaccesskit/_backends/dao/schema.py +805 -0
  49. pyaccesskit-0.1.0/src/pyaccesskit/_backends/dao/typemap.py +390 -0
  50. pyaccesskit-0.1.0/src/pyaccesskit/_backends/fake/__init__.py +3 -0
  51. pyaccesskit-0.1.0/src/pyaccesskit/_backends/fake/backend.py +680 -0
  52. pyaccesskit-0.1.0/src/pyaccesskit/_backends/protocols.py +339 -0
  53. pyaccesskit-0.1.0/src/pyaccesskit/_com/__init__.py +1 -0
  54. pyaccesskit-0.1.0/src/pyaccesskit/_com/constants.py +394 -0
  55. pyaccesskit-0.1.0/src/pyaccesskit/_com/dispatch.py +50 -0
  56. pyaccesskit-0.1.0/src/pyaccesskit/_com/errors.py +184 -0
  57. pyaccesskit-0.1.0/src/pyaccesskit/_com/gateway.py +199 -0
  58. pyaccesskit-0.1.0/src/pyaccesskit/_com/raw.py +164 -0
  59. pyaccesskit-0.1.0/src/pyaccesskit/_com/runtime.py +39 -0
  60. pyaccesskit-0.1.0/src/pyaccesskit/_com/variants.py +72 -0
  61. pyaccesskit-0.1.0/src/pyaccesskit/_engines/__init__.py +48 -0
  62. pyaccesskit-0.1.0/src/pyaccesskit/_engines/access.py +300 -0
  63. pyaccesskit-0.1.0/src/pyaccesskit/_engines/inproc.py +148 -0
  64. pyaccesskit-0.1.0/src/pyaccesskit/_engines/probe.py +231 -0
  65. pyaccesskit-0.1.0/src/pyaccesskit/_ledger.py +158 -0
  66. pyaccesskit-0.1.0/src/pyaccesskit/_ops/__init__.py +0 -0
  67. pyaccesskit-0.1.0/src/pyaccesskit/_ops/design.py +127 -0
  68. pyaccesskit-0.1.0/src/pyaccesskit/_ops/schema.py +471 -0
  69. pyaccesskit-0.1.0/src/pyaccesskit/_session/__init__.py +1 -0
  70. pyaccesskit-0.1.0/src/pyaccesskit/_session/protocols.py +78 -0
  71. pyaccesskit-0.1.0/src/pyaccesskit/_session/session.py +354 -0
  72. pyaccesskit-0.1.0/src/pyaccesskit/_text/__init__.py +0 -0
  73. pyaccesskit-0.1.0/src/pyaccesskit/_text/codec.py +114 -0
  74. pyaccesskit-0.1.0/src/pyaccesskit/_version.py +3 -0
  75. pyaccesskit-0.1.0/src/pyaccesskit/_win/__init__.py +1 -0
  76. pyaccesskit-0.1.0/src/pyaccesskit/_win/access_process.py +348 -0
  77. pyaccesskit-0.1.0/src/pyaccesskit/_win/console.py +56 -0
  78. pyaccesskit-0.1.0/src/pyaccesskit/_win/inspector.py +53 -0
  79. pyaccesskit-0.1.0/src/pyaccesskit/_win/job.py +65 -0
  80. pyaccesskit-0.1.0/src/pyaccesskit/_win/processes.py +159 -0
  81. pyaccesskit-0.1.0/src/pyaccesskit/_win/watchdog.py +253 -0
  82. pyaccesskit-0.1.0/src/pyaccesskit/cli/__init__.py +10 -0
  83. pyaccesskit-0.1.0/src/pyaccesskit/cli/_output.py +101 -0
  84. pyaccesskit-0.1.0/src/pyaccesskit/cli/agent.py +99 -0
  85. pyaccesskit-0.1.0/src/pyaccesskit/cli/app.py +54 -0
  86. pyaccesskit-0.1.0/src/pyaccesskit/cli/cleanup.py +56 -0
  87. pyaccesskit-0.1.0/src/pyaccesskit/cli/doctor.py +101 -0
  88. pyaccesskit-0.1.0/src/pyaccesskit/cli/inspection.py +223 -0
  89. pyaccesskit-0.1.0/src/pyaccesskit/database.py +296 -0
  90. pyaccesskit-0.1.0/src/pyaccesskit/diagnostics.py +319 -0
  91. pyaccesskit-0.1.0/src/pyaccesskit/enums.py +258 -0
  92. pyaccesskit-0.1.0/src/pyaccesskit/errors.py +407 -0
  93. pyaccesskit-0.1.0/src/pyaccesskit/forms/__init__.py +45 -0
  94. pyaccesskit-0.1.0/src/pyaccesskit/forms/builder.py +295 -0
  95. pyaccesskit-0.1.0/src/pyaccesskit/forms/collection.py +117 -0
  96. pyaccesskit-0.1.0/src/pyaccesskit/forms/controls.py +157 -0
  97. pyaccesskit-0.1.0/src/pyaccesskit/forms/layout.py +300 -0
  98. pyaccesskit-0.1.0/src/pyaccesskit/forms/spec.py +169 -0
  99. pyaccesskit-0.1.0/src/pyaccesskit/forms/vba.py +138 -0
  100. pyaccesskit-0.1.0/src/pyaccesskit/maintenance.py +32 -0
  101. pyaccesskit-0.1.0/src/pyaccesskit/modules.py +101 -0
  102. pyaccesskit-0.1.0/src/pyaccesskit/objects.py +81 -0
  103. pyaccesskit-0.1.0/src/pyaccesskit/options.py +40 -0
  104. pyaccesskit-0.1.0/src/pyaccesskit/properties.py +74 -0
  105. pyaccesskit-0.1.0/src/pyaccesskit/py.typed +0 -0
  106. pyaccesskit-0.1.0/src/pyaccesskit/queries.py +190 -0
  107. pyaccesskit-0.1.0/src/pyaccesskit/relationships.py +143 -0
  108. pyaccesskit-0.1.0/src/pyaccesskit/schema/__init__.py +73 -0
  109. pyaccesskit-0.1.0/src/pyaccesskit/schema/_base.py +55 -0
  110. pyaccesskit-0.1.0/src/pyaccesskit/schema/_reserved_words.py +55 -0
  111. pyaccesskit-0.1.0/src/pyaccesskit/schema/columns.py +609 -0
  112. pyaccesskit-0.1.0/src/pyaccesskit/schema/compat.py +57 -0
  113. pyaccesskit-0.1.0/src/pyaccesskit/schema/expressions.py +162 -0
  114. pyaccesskit-0.1.0/src/pyaccesskit/schema/indexes.py +114 -0
  115. pyaccesskit-0.1.0/src/pyaccesskit/schema/names.py +122 -0
  116. pyaccesskit-0.1.0/src/pyaccesskit/schema/queries.py +192 -0
  117. pyaccesskit-0.1.0/src/pyaccesskit/schema/relationships.py +132 -0
  118. pyaccesskit-0.1.0/src/pyaccesskit/schema/tables.py +178 -0
  119. pyaccesskit-0.1.0/src/pyaccesskit/tables.py +333 -0
  120. pyaccesskit-0.1.0/src/pyaccesskit/units.py +301 -0
  121. pyaccesskit-0.1.0/tests/__init__.py +0 -0
  122. pyaccesskit-0.1.0/tests/conftest.py +76 -0
  123. pyaccesskit-0.1.0/tests/contract/__init__.py +0 -0
  124. pyaccesskit-0.1.0/tests/contract/conftest.py +55 -0
  125. pyaccesskit-0.1.0/tests/contract/test_design_contract.py +123 -0
  126. pyaccesskit-0.1.0/tests/contract/test_schema_contract.py +298 -0
  127. pyaccesskit-0.1.0/tests/fakes.py +153 -0
  128. pyaccesskit-0.1.0/tests/integration/__init__.py +0 -0
  129. pyaccesskit-0.1.0/tests/integration/backends.py +58 -0
  130. pyaccesskit-0.1.0/tests/integration/test_design.py +230 -0
  131. pyaccesskit-0.1.0/tests/integration/test_examples.py +42 -0
  132. pyaccesskit-0.1.0/tests/integration/test_lifecycle.py +325 -0
  133. pyaccesskit-0.1.0/tests/integration/test_roundtrip.py +282 -0
  134. pyaccesskit-0.1.0/tests/unit/__init__.py +0 -0
  135. pyaccesskit-0.1.0/tests/unit/test_api_fake.py +133 -0
  136. pyaccesskit-0.1.0/tests/unit/test_cli.py +187 -0
  137. pyaccesskit-0.1.0/tests/unit/test_codec.py +88 -0
  138. pyaccesskit-0.1.0/tests/unit/test_columns.py +148 -0
  139. pyaccesskit-0.1.0/tests/unit/test_com_errors.py +135 -0
  140. pyaccesskit-0.1.0/tests/unit/test_dao_sql.py +25 -0
  141. pyaccesskit-0.1.0/tests/unit/test_docs.py +107 -0
  142. pyaccesskit-0.1.0/tests/unit/test_expressions.py +111 -0
  143. pyaccesskit-0.1.0/tests/unit/test_forms.py +249 -0
  144. pyaccesskit-0.1.0/tests/unit/test_import_isolation.py +48 -0
  145. pyaccesskit-0.1.0/tests/unit/test_ledger.py +101 -0
  146. pyaccesskit-0.1.0/tests/unit/test_names.py +71 -0
  147. pyaccesskit-0.1.0/tests/unit/test_relationships_queries.py +145 -0
  148. pyaccesskit-0.1.0/tests/unit/test_review_regressions.py +262 -0
  149. pyaccesskit-0.1.0/tests/unit/test_session.py +160 -0
  150. pyaccesskit-0.1.0/tests/unit/test_tables.py +124 -0
  151. pyaccesskit-0.1.0/tests/unit/test_typemap.py +165 -0
  152. pyaccesskit-0.1.0/tests/unit/test_units.py +109 -0
@@ -0,0 +1,27 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ site/
9
+
10
+ # Virtual environments (incl. the optional x86 env used for in-process DAO tests)
11
+ .venv/
12
+ .venv-*/
13
+
14
+ # Tooling caches
15
+ .pytest_cache/
16
+ .ruff_cache/
17
+ .hypothesis/
18
+ .coverage
19
+ .coverage.*
20
+ htmlcov/
21
+ coverage.xml
22
+
23
+ # Microsoft Access lock files and scratch databases
24
+ *.laccdb
25
+ *.ldb
26
+ examples/out/
27
+ scratch/
@@ -0,0 +1,60 @@
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/) (0.x releases may contain breaking changes, which are listed
6
+ under **Changed**).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-09-26
11
+
12
+ First release.
13
+
14
+ ### Added
15
+
16
+ - **Lifecycle.** `AccessDatabase.create()` (atomic by default) and `AccessDatabase.open()` (read-only and
17
+ shared, exclusive, password), used as context managers. Guaranteed cleanup, with cleanup failures
18
+ attached as notes to the original exception. `db.raw` escape hatch with revocable proxies.
19
+ - **Engines.** In-process DAO, Access-hosted DAO, and Access design sessions. `engine="auto"` selects one
20
+ and switches to a design session on the first design feature. Bitness-aware diagnostics.
21
+ - **Process safety.** Access is always started as a new instance and never attached to. Process identity is
22
+ PID, creation time and image. Kill-on-close job objects, an ownership ledger with `reap_orphans()` /
23
+ `pyaccesskit cleanup`, a dialog watchdog (`AccessDialogError`), per-call timeouts, and Ctrl+C handling.
24
+ - **Tables.** Short Text, Long Text, Number (all field sizes), Decimal, Currency, AutoNumber (incrementing
25
+ or Replication ID), Date/Time, Yes/No, Hyperlink and OLE Object columns. Literal and `Expr` defaults,
26
+ validation rules, captions, formats and input masks. Indexes (composite, descending, unique, primary).
27
+ Add, drop and rename columns and indexes, rename and drop tables, property bags. `Table.to_spec()`
28
+ round-trips to the normalized spec.
29
+ - **Relationships.** Single and composite keys, referential integrity, cascades and join types, with
30
+ pre-validation of tables, columns, types and unique indexes.
31
+ - **Queries and data.** Saved and pass-through queries, query kinds and parameters, and
32
+ `execute()`/`fetch_all()` with parameters bound by name.
33
+ - **Forms** (provisional). Labels, text boxes, check boxes, combo boxes and buttons. Stacked and tabular
34
+ layouts, VBA event procedures, form properties, atomic build-then-swap replacement, `check_opens()` and
35
+ `controls()`.
36
+ - **VBA modules** (provisional). Standard and class modules; read and replace code.
37
+ - **Text import/export** (provisional). `SaveAsText`/`LoadFromText` for forms, reports, macros, queries and
38
+ modules, with the correct encoding per object type; files are UTF-8.
39
+ - **Specs.** Immutable, serializable Pydantic models (`TableSpec`, `Column.*`, `IndexSpec`,
40
+ `RelationshipSpec`, `QuerySpec`, `FormSpec`) with JSON Schema. `Length` units (`cm`, `mm`, `inch`, `pt`,
41
+ `twips`).
42
+ - **Errors.** A typed exception hierarchy with Access/DAO error numbers and `DBEngine.Errors` details.
43
+ - **CLI.** `pyaccesskit doctor [--json] [--probe]`, `pyaccesskit inspect DB [--json] [--counts]` and
44
+ `pyaccesskit cleanup [--dry-run] [--json]`. `python -m pyaccesskit`.
45
+ - **Diagnostics.** `pyaccesskit.diagnose()`.
46
+ - **For AI agents.** `pyaccesskit guide` prints a version-matched guide (rules, API cheat sheet, SQL dialect
47
+ notes, error table, limits, and a complete tested example); `pyaccesskit schema` prints JSON Schemas of the
48
+ specs; the documentation site publishes `llms.txt` and `llms-full.txt`.
49
+ - **Documentation.** Recipes, error / data-type / CLI references, FAQ, and an API reference generated from
50
+ the docstrings.
51
+
52
+ ### Fixed (found while documenting)
53
+
54
+ - Opening a database for design no longer runs its `StartUpForm` (which could raise dialogs with code
55
+ disabled); the setting is preserved.
56
+ - `bytes` query parameters are declared as `LongBinary`, so OLE Object data is no longer corrupted; binary
57
+ values read back as `bytes`.
58
+
59
+ [Unreleased]: https://github.com/ariedotcodotnz/PyAccessKit/compare/v0.1.0...HEAD
60
+ [0.1.0]: https://github.com/ariedotcodotnz/PyAccessKit/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arie Joe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyaccesskit
3
+ Version: 0.1.0
4
+ Summary: A modern, typed, Pythonic toolkit for building and modifying Microsoft Access databases and applications.
5
+ Project-URL: Homepage, https://github.com/ariedotcodotnz/PyAccessKit
6
+ Project-URL: Documentation, https://ariedotcodotnz.github.io/PyAccessKit/
7
+ Project-URL: Changelog, https://github.com/ariedotcodotnz/PyAccessKit/blob/HEAD/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/ariedotcodotnz/PyAccessKit/issues
9
+ Project-URL: Source, https://github.com/ariedotcodotnz/PyAccessKit
10
+ Author: Arie Joe
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: accdb,access,automation,com,dao,database,microsoft-access,pywin32
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Database
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: pydantic<3,>=2.7
28
+ Requires-Dist: pywin32>=306; sys_platform == 'win32'
29
+ Requires-Dist: typer>=0.12
30
+ Description-Content-Type: text/markdown
31
+
32
+ # PyAccessKit
33
+
34
+ A modern, typed, Pythonic toolkit for building and modifying Microsoft Access databases (`.accdb`) and,
35
+ progressively, whole Access applications without raw `pywin32` calls, DAO magic numbers or orphaned
36
+ `MSACCESS.EXE` processes.
37
+
38
+ ```python
39
+ from pyaccesskit import AccessDatabase, Column, Expr, Vba, cm
40
+
41
+ with AccessDatabase.create("crm.accdb") as db:
42
+ db.tables.create(
43
+ "Customers",
44
+ columns=[
45
+ Column.autonumber("CustomerID", primary_key=True),
46
+ Column.text("CustomerName", length=200, required=True),
47
+ Column.text("Email", length=255, unique=True),
48
+ Column.date_time("CreatedAt", default=Expr("Now()")),
49
+ ],
50
+ )
51
+ db.tables.create(
52
+ "Orders",
53
+ columns=[
54
+ Column.autonumber("OrderID", primary_key=True),
55
+ Column.number("CustomerID", required=True), # Long Integer, as in Access
56
+ Column.currency("Total", default=0),
57
+ ],
58
+ )
59
+ db.relationships.create("Customers.CustomerID", "Orders.CustomerID", cascade_delete=True)
60
+ db.queries.create("qryCustomers", "SELECT * FROM Customers ORDER BY CustomerName;")
61
+
62
+ with db.forms.create("frmCustomers", record_source="Customers", caption="Customers") as form:
63
+ form.textbox("CustomerName", label="Customer name", width=cm(8))
64
+ form.textbox("Email")
65
+ form.button("cmdClose", caption="Close", on_click=Vba("DoCmd.Close acForm, Me.Name"))
66
+ ```
67
+
68
+ When the `with` block ends, the database file appears at `crm.accdb`, and any Access process PyAccessKit
69
+ started is closed. If the block raises, no file is created and nothing is left running.
70
+
71
+ > **Status:** alpha (`0.1.0`). The API may still change between 0.x releases.
72
+
73
+ ## Installation
74
+
75
+ ```console
76
+ pip install pyaccesskit
77
+ ```
78
+
79
+ Requirements:
80
+
81
+ - Windows 10 or 11 and CPython 3.11–3.14 (64- or 32-bit; free-threaded builds are not supported).
82
+ - Microsoft Access 2016 or later, including Microsoft 365. Alternatively, the Microsoft 365 Access Runtime
83
+ with a Python of the same bitness, for schema and data work only.
84
+
85
+ Check your machine with:
86
+
87
+ ```console
88
+ pyaccesskit doctor # which engines work here, and why not
89
+ pyaccesskit doctor --probe # also builds a scratch database end to end
90
+ ```
91
+
92
+ The spec models (`TableSpec`, `Column`, `FormSpec`, units, layout…) are pure Python and also import on
93
+ Linux and macOS, so you can validate specs in CI without Access.
94
+
95
+ ## What you can do in 0.1
96
+
97
+ | Area | Highlights | Stability |
98
+ |---|---|---|
99
+ | Lifecycle | Atomic `create()`, `open()` (read-only/shared, exclusive, password), guaranteed cleanup, `db.raw` escape hatch | stable-track |
100
+ | Tables | Every common column type, defaults as Python values or `Expr`, validation rules, captions, formats, input masks; indexes (composite, descending, unique, primary); add, drop and rename columns and indexes; `to_spec()` round-trips | stable-track |
101
+ | Relationships | Single and composite keys, referential integrity, cascades, join types, all checked before Access sees them | stable-track |
102
+ | Queries & data | Saved queries (select, action, union, crosstab, pass-through), parameters bound by name, `execute()` / `fetch_all()` | stable-track |
103
+ | Forms | Labels, text boxes, check boxes, combo boxes and buttons; stacked or tabular layout; VBA event procedures; atomic build-then-swap replacement | provisional |
104
+ | Modules & text I/O | Standard and class modules; `SaveAsText`/`LoadFromText` for forms, reports, macros, queries and modules, stored as UTF-8 | provisional |
105
+ | Decimal columns | `Column.decimal(precision=…, scale=…)` via ADO DDL | provisional |
106
+
107
+ Reports, linked tables, VBA references, and the declarative `build`/`plan`/`apply` workflow are planned
108
+ (see the [roadmap](https://ariedotcodotnz.github.io/PyAccessKit/#roadmap)).
109
+
110
+ ## Engines
111
+
112
+ PyAccessKit reaches the database in one of three ways and picks one for you (`engine="auto"`):
113
+
114
+ | Engine | When | Notes |
115
+ |---|---|---|
116
+ | In-process DAO | Python and Office have the **same bitness** | Fastest; never starts `MSACCESS.EXE`; enough for tables, relationships, queries and data |
117
+ | Access-hosted DAO | Bitness differs, or `engine="access"` | A hidden Access instance that PyAccessKit owns hosts DAO; the database is **not** opened in the Access UI, so no startup code runs |
118
+ | Design session | First use of forms, modules or text I/O | The database becomes Access's current database; handles such as `db.tables["X"]` keep working across the switch |
119
+
120
+ | Python | Office | In-process DAO | Access transports |
121
+ |---|---|---|---|
122
+ | 64-bit | 64-bit | ✅ | ✅ |
123
+ | 32-bit | 32-bit | ✅ | ✅ |
124
+ | 64-bit | 32-bit (common with Microsoft 365) | ❌ | ✅ |
125
+ | 32-bit | 64-bit | ❌ | ✅ |
126
+
127
+ `pyaccesskit doctor` explains what applies to your machine.
128
+
129
+ ## Safety guarantees
130
+
131
+ - **Never touches your own Access windows.** PyAccessKit always starts a *new* Access instance
132
+ (`CoCreateInstanceEx`, local server). It never attaches to a running Access (which `Dispatch()` does),
133
+ so it can never close your work.
134
+ - **No orphaned `MSACCESS.EXE`.** Every Access process it starts is identified by PID, creation time and
135
+ image, and placed in a kill-on-close job object. If Python crashes, Windows ends the process. The process
136
+ is also recorded in an ownership ledger: `pyaccesskit cleanup` ends leftovers from crashed sessions, and
137
+ only when their Python owner is gone. Processes it did not start are never touched.
138
+ - **Exceptions clean up too.** Closing runs every cleanup step even if one fails. Cleanup failures are
139
+ attached as notes to the exception that caused the close, never masking it.
140
+ - **Atomic creation.** `AccessDatabase.create()` builds in a hidden sibling file and moves it into place
141
+ only on success. Tables are created all-or-nothing, and forms are built under a temporary name and
142
+ swapped in.
143
+ - **No hangs on hidden dialogs.** A watchdog watches the windows of *our* Access process. Unexpected
144
+ dialogs are dismissed and reported as `AccessDialogError` with their text. Calls that exceed
145
+ `call_timeout` end the owned process, and Ctrl+C works during long calls.
146
+ - **No startup code by default.** Databases are opened with macros disabled. Read-only sessions never open
147
+ the database in the Access UI, so `AutoExec` and startup forms do not run.
148
+
149
+ ## Errors you can act on
150
+
151
+ COM errors are translated into specific exceptions. Each one carries what PyAccessKit was doing, the
152
+ object involved, and the original Access or DAO error:
153
+
154
+ ```python
155
+ from pyaccesskit import AccessDatabase, IntegrityViolationError
156
+
157
+ with AccessDatabase.open("crm.accdb") as db:
158
+ try:
159
+ db.execute("INSERT INTO Orders (CustomerID, Total) VALUES (999, 10)")
160
+ except IntegrityViolationError as exc:
161
+ print(exc) # what failed, with Access's own explanation
162
+ print(exc.details.number) # 3201: there is no related record in Customers
163
+ ```
164
+
165
+ Most mistakes are caught before Access is involved at all. A duplicate table name, an unknown column in a
166
+ relationship, or incompatible key types each raise a precise `ObjectExistsError`, `SpecError` or
167
+ `RelationshipError`.
168
+
169
+ ## Command line
170
+
171
+ ```console
172
+ pyaccesskit doctor [--json] [--probe] # environment report (exit code 3 if nothing works)
173
+ pyaccesskit inspect DB [--json] [--counts] # tables, relationships, queries, objects; read-only
174
+ pyaccesskit cleanup [--dry-run] [--json] # end orphaned Access processes started by PyAccessKit
175
+ pyaccesskit guide [--path] # the guide for AI coding agents
176
+ pyaccesskit schema [KIND] # JSON Schema of the specs
177
+ ```
178
+
179
+ Exit codes: `0` success, `1` error, `2` usage error, `3` environment unusable.
180
+
181
+ ## Documentation
182
+
183
+ - [Getting started](https://ariedotcodotnz.github.io/PyAccessKit/getting-started/) and [recipes](https://ariedotcodotnz.github.io/PyAccessKit/guides/recipes/)
184
+ - **[Building with AI agents](https://ariedotcodotnz.github.io/PyAccessKit/agents/)**: `pyaccesskit guide` prints a version-matched guide
185
+ that lets coding agents write Access applications with PyAccessKit; `pyaccesskit schema` prints the
186
+ JSON Schemas of the specs; the site publishes `llms.txt` and `llms-full.txt`.
187
+ - Concepts: [engines](https://ariedotcodotnz.github.io/PyAccessKit/concepts/engines/), [lifecycle](https://ariedotcodotnz.github.io/PyAccessKit/concepts/lifecycle/),
188
+ [specs](https://ariedotcodotnz.github.io/PyAccessKit/concepts/specs/)
189
+ - Guides: [tables](https://ariedotcodotnz.github.io/PyAccessKit/guides/tables/), [relationships](https://ariedotcodotnz.github.io/PyAccessKit/guides/relationships/),
190
+ [queries](https://ariedotcodotnz.github.io/PyAccessKit/guides/queries/), [forms](https://ariedotcodotnz.github.io/PyAccessKit/guides/forms/), [modules](https://ariedotcodotnz.github.io/PyAccessKit/guides/modules/),
191
+ [text I/O](https://ariedotcodotnz.github.io/PyAccessKit/guides/text-io/)
192
+ - Reference: [errors](https://ariedotcodotnz.github.io/PyAccessKit/reference/errors/), [data types](https://ariedotcodotnz.github.io/PyAccessKit/reference/data-types/),
193
+ [command line](https://ariedotcodotnz.github.io/PyAccessKit/reference/cli/), and an API reference generated from the docstrings
194
+ - [Environment & troubleshooting](https://ariedotcodotnz.github.io/PyAccessKit/environment/), [FAQ](https://ariedotcodotnz.github.io/PyAccessKit/faq/),
195
+ [contributing](https://ariedotcodotnz.github.io/PyAccessKit/contributing/), [architecture decisions](https://github.com/ariedotcodotnz/PyAccessKit/tree/HEAD/docs/adr)
196
+ - Examples: [`examples/`](https://github.com/ariedotcodotnz/PyAccessKit/tree/HEAD/examples) (`04_inventory_app.py` is the complete, tested application the agent
197
+ guide walks through)
198
+
199
+ ## License
200
+
201
+ MIT. See [LICENSE](https://github.com/ariedotcodotnz/PyAccessKit/blob/HEAD/LICENSE).
@@ -0,0 +1,170 @@
1
+ # PyAccessKit
2
+
3
+ A modern, typed, Pythonic toolkit for building and modifying Microsoft Access databases (`.accdb`) and,
4
+ progressively, whole Access applications without raw `pywin32` calls, DAO magic numbers or orphaned
5
+ `MSACCESS.EXE` processes.
6
+
7
+ ```python
8
+ from pyaccesskit import AccessDatabase, Column, Expr, Vba, cm
9
+
10
+ with AccessDatabase.create("crm.accdb") as db:
11
+ db.tables.create(
12
+ "Customers",
13
+ columns=[
14
+ Column.autonumber("CustomerID", primary_key=True),
15
+ Column.text("CustomerName", length=200, required=True),
16
+ Column.text("Email", length=255, unique=True),
17
+ Column.date_time("CreatedAt", default=Expr("Now()")),
18
+ ],
19
+ )
20
+ db.tables.create(
21
+ "Orders",
22
+ columns=[
23
+ Column.autonumber("OrderID", primary_key=True),
24
+ Column.number("CustomerID", required=True), # Long Integer, as in Access
25
+ Column.currency("Total", default=0),
26
+ ],
27
+ )
28
+ db.relationships.create("Customers.CustomerID", "Orders.CustomerID", cascade_delete=True)
29
+ db.queries.create("qryCustomers", "SELECT * FROM Customers ORDER BY CustomerName;")
30
+
31
+ with db.forms.create("frmCustomers", record_source="Customers", caption="Customers") as form:
32
+ form.textbox("CustomerName", label="Customer name", width=cm(8))
33
+ form.textbox("Email")
34
+ form.button("cmdClose", caption="Close", on_click=Vba("DoCmd.Close acForm, Me.Name"))
35
+ ```
36
+
37
+ When the `with` block ends, the database file appears at `crm.accdb`, and any Access process PyAccessKit
38
+ started is closed. If the block raises, no file is created and nothing is left running.
39
+
40
+ > **Status:** alpha (`0.1.0`). The API may still change between 0.x releases.
41
+
42
+ ## Installation
43
+
44
+ ```console
45
+ pip install pyaccesskit
46
+ ```
47
+
48
+ Requirements:
49
+
50
+ - Windows 10 or 11 and CPython 3.11–3.14 (64- or 32-bit; free-threaded builds are not supported).
51
+ - Microsoft Access 2016 or later, including Microsoft 365. Alternatively, the Microsoft 365 Access Runtime
52
+ with a Python of the same bitness, for schema and data work only.
53
+
54
+ Check your machine with:
55
+
56
+ ```console
57
+ pyaccesskit doctor # which engines work here, and why not
58
+ pyaccesskit doctor --probe # also builds a scratch database end to end
59
+ ```
60
+
61
+ The spec models (`TableSpec`, `Column`, `FormSpec`, units, layout…) are pure Python and also import on
62
+ Linux and macOS, so you can validate specs in CI without Access.
63
+
64
+ ## What you can do in 0.1
65
+
66
+ | Area | Highlights | Stability |
67
+ |---|---|---|
68
+ | Lifecycle | Atomic `create()`, `open()` (read-only/shared, exclusive, password), guaranteed cleanup, `db.raw` escape hatch | stable-track |
69
+ | Tables | Every common column type, defaults as Python values or `Expr`, validation rules, captions, formats, input masks; indexes (composite, descending, unique, primary); add, drop and rename columns and indexes; `to_spec()` round-trips | stable-track |
70
+ | Relationships | Single and composite keys, referential integrity, cascades, join types, all checked before Access sees them | stable-track |
71
+ | Queries & data | Saved queries (select, action, union, crosstab, pass-through), parameters bound by name, `execute()` / `fetch_all()` | stable-track |
72
+ | Forms | Labels, text boxes, check boxes, combo boxes and buttons; stacked or tabular layout; VBA event procedures; atomic build-then-swap replacement | provisional |
73
+ | Modules & text I/O | Standard and class modules; `SaveAsText`/`LoadFromText` for forms, reports, macros, queries and modules, stored as UTF-8 | provisional |
74
+ | Decimal columns | `Column.decimal(precision=…, scale=…)` via ADO DDL | provisional |
75
+
76
+ Reports, linked tables, VBA references, and the declarative `build`/`plan`/`apply` workflow are planned
77
+ (see the [roadmap](https://ariedotcodotnz.github.io/PyAccessKit/#roadmap)).
78
+
79
+ ## Engines
80
+
81
+ PyAccessKit reaches the database in one of three ways and picks one for you (`engine="auto"`):
82
+
83
+ | Engine | When | Notes |
84
+ |---|---|---|
85
+ | In-process DAO | Python and Office have the **same bitness** | Fastest; never starts `MSACCESS.EXE`; enough for tables, relationships, queries and data |
86
+ | Access-hosted DAO | Bitness differs, or `engine="access"` | A hidden Access instance that PyAccessKit owns hosts DAO; the database is **not** opened in the Access UI, so no startup code runs |
87
+ | Design session | First use of forms, modules or text I/O | The database becomes Access's current database; handles such as `db.tables["X"]` keep working across the switch |
88
+
89
+ | Python | Office | In-process DAO | Access transports |
90
+ |---|---|---|---|
91
+ | 64-bit | 64-bit | ✅ | ✅ |
92
+ | 32-bit | 32-bit | ✅ | ✅ |
93
+ | 64-bit | 32-bit (common with Microsoft 365) | ❌ | ✅ |
94
+ | 32-bit | 64-bit | ❌ | ✅ |
95
+
96
+ `pyaccesskit doctor` explains what applies to your machine.
97
+
98
+ ## Safety guarantees
99
+
100
+ - **Never touches your own Access windows.** PyAccessKit always starts a *new* Access instance
101
+ (`CoCreateInstanceEx`, local server). It never attaches to a running Access (which `Dispatch()` does),
102
+ so it can never close your work.
103
+ - **No orphaned `MSACCESS.EXE`.** Every Access process it starts is identified by PID, creation time and
104
+ image, and placed in a kill-on-close job object. If Python crashes, Windows ends the process. The process
105
+ is also recorded in an ownership ledger: `pyaccesskit cleanup` ends leftovers from crashed sessions, and
106
+ only when their Python owner is gone. Processes it did not start are never touched.
107
+ - **Exceptions clean up too.** Closing runs every cleanup step even if one fails. Cleanup failures are
108
+ attached as notes to the exception that caused the close, never masking it.
109
+ - **Atomic creation.** `AccessDatabase.create()` builds in a hidden sibling file and moves it into place
110
+ only on success. Tables are created all-or-nothing, and forms are built under a temporary name and
111
+ swapped in.
112
+ - **No hangs on hidden dialogs.** A watchdog watches the windows of *our* Access process. Unexpected
113
+ dialogs are dismissed and reported as `AccessDialogError` with their text. Calls that exceed
114
+ `call_timeout` end the owned process, and Ctrl+C works during long calls.
115
+ - **No startup code by default.** Databases are opened with macros disabled. Read-only sessions never open
116
+ the database in the Access UI, so `AutoExec` and startup forms do not run.
117
+
118
+ ## Errors you can act on
119
+
120
+ COM errors are translated into specific exceptions. Each one carries what PyAccessKit was doing, the
121
+ object involved, and the original Access or DAO error:
122
+
123
+ ```python
124
+ from pyaccesskit import AccessDatabase, IntegrityViolationError
125
+
126
+ with AccessDatabase.open("crm.accdb") as db:
127
+ try:
128
+ db.execute("INSERT INTO Orders (CustomerID, Total) VALUES (999, 10)")
129
+ except IntegrityViolationError as exc:
130
+ print(exc) # what failed, with Access's own explanation
131
+ print(exc.details.number) # 3201: there is no related record in Customers
132
+ ```
133
+
134
+ Most mistakes are caught before Access is involved at all. A duplicate table name, an unknown column in a
135
+ relationship, or incompatible key types each raise a precise `ObjectExistsError`, `SpecError` or
136
+ `RelationshipError`.
137
+
138
+ ## Command line
139
+
140
+ ```console
141
+ pyaccesskit doctor [--json] [--probe] # environment report (exit code 3 if nothing works)
142
+ pyaccesskit inspect DB [--json] [--counts] # tables, relationships, queries, objects; read-only
143
+ pyaccesskit cleanup [--dry-run] [--json] # end orphaned Access processes started by PyAccessKit
144
+ pyaccesskit guide [--path] # the guide for AI coding agents
145
+ pyaccesskit schema [KIND] # JSON Schema of the specs
146
+ ```
147
+
148
+ Exit codes: `0` success, `1` error, `2` usage error, `3` environment unusable.
149
+
150
+ ## Documentation
151
+
152
+ - [Getting started](https://ariedotcodotnz.github.io/PyAccessKit/getting-started/) and [recipes](https://ariedotcodotnz.github.io/PyAccessKit/guides/recipes/)
153
+ - **[Building with AI agents](https://ariedotcodotnz.github.io/PyAccessKit/agents/)**: `pyaccesskit guide` prints a version-matched guide
154
+ that lets coding agents write Access applications with PyAccessKit; `pyaccesskit schema` prints the
155
+ JSON Schemas of the specs; the site publishes `llms.txt` and `llms-full.txt`.
156
+ - Concepts: [engines](https://ariedotcodotnz.github.io/PyAccessKit/concepts/engines/), [lifecycle](https://ariedotcodotnz.github.io/PyAccessKit/concepts/lifecycle/),
157
+ [specs](https://ariedotcodotnz.github.io/PyAccessKit/concepts/specs/)
158
+ - Guides: [tables](https://ariedotcodotnz.github.io/PyAccessKit/guides/tables/), [relationships](https://ariedotcodotnz.github.io/PyAccessKit/guides/relationships/),
159
+ [queries](https://ariedotcodotnz.github.io/PyAccessKit/guides/queries/), [forms](https://ariedotcodotnz.github.io/PyAccessKit/guides/forms/), [modules](https://ariedotcodotnz.github.io/PyAccessKit/guides/modules/),
160
+ [text I/O](https://ariedotcodotnz.github.io/PyAccessKit/guides/text-io/)
161
+ - Reference: [errors](https://ariedotcodotnz.github.io/PyAccessKit/reference/errors/), [data types](https://ariedotcodotnz.github.io/PyAccessKit/reference/data-types/),
162
+ [command line](https://ariedotcodotnz.github.io/PyAccessKit/reference/cli/), and an API reference generated from the docstrings
163
+ - [Environment & troubleshooting](https://ariedotcodotnz.github.io/PyAccessKit/environment/), [FAQ](https://ariedotcodotnz.github.io/PyAccessKit/faq/),
164
+ [contributing](https://ariedotcodotnz.github.io/PyAccessKit/contributing/), [architecture decisions](https://github.com/ariedotcodotnz/PyAccessKit/tree/HEAD/docs/adr)
165
+ - Examples: [`examples/`](https://github.com/ariedotcodotnz/PyAccessKit/tree/HEAD/examples) (`04_inventory_app.py` is the complete, tested application the agent
166
+ guide walks through)
167
+
168
+ ## License
169
+
170
+ MIT. See [LICENSE](https://github.com/ariedotcodotnz/PyAccessKit/blob/HEAD/LICENSE).
@@ -0,0 +1,126 @@
1
+ # ADR 0001 — Phase-0 spike findings
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-09-26
5
+ - **Environment:** Microsoft 365 Apps (Current Channel) Access 16.0.20326.20158 **32-bit** Click-to-Run,
6
+ Windows 11, pywin32 312; CPython 3.14 x64 (Access transports) and CPython 3.12 x86 (in-process DAO).
7
+ - **Scripts:** `scripts/spikes/s01…s10*.py` (throwaway; reproducible).
8
+
9
+ Each finding lists the decision it drives. "✅" = assumption from the plan confirmed; "⚠️" = reality
10
+ differed and the design was adjusted.
11
+
12
+ ## S1 — process ownership
13
+
14
+ - ✅ Every `CoCreateInstanceEx(CLSCTX_LOCAL_SERVER)` starts a **new** `MSACCESS.EXE` (~1.1 s).
15
+ - ✅ `app.hWndAccessApp()` works on a hidden instance with no database and resolves (via
16
+ `GetWindowThreadProcessId`) to the right process; `QueryFullProcessImageNameW` confirms `MSACCESS.EXE`.
17
+ - ✅ A DCOM-launched Access **can** be assigned to our job object; closing the job handle kills it.
18
+ - ✅ Defaults: `Visible=False`, `UserControl=False`, `AutomationSecurity=2` (ByUI).
19
+ - ✅ `Quit(acQuitSaveNone)` → process exit in ~2.5 s; holding a child COM reference did not block exit.
20
+ - `SysCmd(724)` → `'32-bit'`, `SysCmd(720)` → full version string, `SysCmd(6)` → runtime flag. Used by
21
+ `doctor --probe`.
22
+
23
+ ## S2 — `AutomationSecurity` and startup code
24
+
25
+ | Open path | Opens | AutoExec runs | Dialogs | Design ops |
26
+ |---|---|---|---|---|
27
+ | Access-hosted DAO (`app.DBEngine.OpenDatabase`) | ✅ | never | 0 | n/a |
28
+ | `ByUI` (default) in an untrusted folder | ✅ | no (Trust Center dependent) | 0 | ✅ |
29
+ | `ForceDisable` | ✅ | **no** | 0 | ✅ |
30
+ | `Low` | ✅ | **yes** | 0 | ✅ |
31
+
32
+ ⚠️ The documentation's warning that ForceDisable "will not open any database" does not apply to modern
33
+ Access. **Decision:** default `macro_security = "disable"` (ForceDisable): deterministic, independent of the
34
+ user's Trust Center. Code execution (`Application.Run`, 0.2) requires an explicit opt-in.
35
+
36
+ ## S3 — DAO- vs Access-created databases
37
+
38
+ `NewCurrentDatabase` writes 15 database properties that DAO `CreateDatabase` does not; opening a
39
+ DAO-created file in Access only adds nav-pane properties. The user-visible ones include `UseMDIMode=0`
40
+ (tabbed documents), `ShowDocumentTabs=True`, `Themed Form Controls=1`.
41
+ **Decision:** the in-process DAO transport applies this **native defaults profile** (see
42
+ `_backends/dao/profile.py`) so output is identical whichever engine created the file. `dbVersion120` is the
43
+ creation format (`dbVersion150/167` still report `Version=12.0`).
44
+
45
+ ## S4 — Decimal(p, s)
46
+
47
+ - ✅ DAO DDL rejects `DECIMAL` (3292); ADO DDL (`CurrentProject.Connection` or in-process ADODB + ACE
48
+ OLEDB) supports ADD/ALTER/CREATE with precision and scale.
49
+ - ⚠️ DAO `CreateField(..., dbDecimal)` silently produces a **BigInt (Type 16)** field, even after setting
50
+ `Precision`/`Scale`. **Never create decimals through DAO.**
51
+ - ✅ DAO *reads* precision/scale through the hidden `Field.Properties("Precision"/"Scale")` (only meaningful
52
+ when `Type == 20`) — introspection needs no ADO.
53
+ - In-process ADO cannot open a file DAO holds exclusively → the in-process transport closes DAO, runs the
54
+ ADO DDL, and reopens (S10).
55
+
56
+ ## S5 — QueryDefs
57
+
58
+ - ⚠️ Jet **rewrites** SQL: uppercases keywords, one clause per line, appends `;\r\n` (DDL/UNION are kept
59
+ closer to verbatim). **Decision:** store what Access stores; compare with a whitespace/case-insensitive
60
+ normalizer.
61
+ - ✅ No reference validation at save time (missing tables and not-yet-created queries are accepted) →
62
+ `build` needs no topological ordering of queries.
63
+ - ✅ Tables and queries share one namespace (3012 / 3010).
64
+ - ⚠️ `QueryDef.Type` reads `0` until `QueryDefs.Refresh()`; afterwards append=64, crosstab=16, DDL=96,
65
+ delete=32, make-table=80, pass-through=112, union=128, update=48. **Always refresh before enumerating.**
66
+ - Parameters come back **bracketed** (`[pMin]`); a bracketed identifier matching a column is a column, not
67
+ a parameter. Pass-through: `CreateQueryDef(name)` then `Connect` → `SQL` works with no reachable DSN.
68
+ - `Recordset.GetRows(n)` returns column-major tuples.
69
+
70
+ ## S6 — forms in a hidden instance
71
+
72
+ - ⚠️ pywin32 dynamic dispatch cannot call **indexed properties** (`Form.Section(0)` → "Member not found")
73
+ and raises `AttributeError` (not `com_error`) for members missing from a specific control's typeinfo
74
+ (e.g. `Label.ControlSource`). **Decision:** the COM gateway performs its own `IDispatch.Invoke` by DISPID
75
+ with the exact arguments for property reads, indexed properties, and method calls.
76
+ - ✅ `CreateForm` → properties → `CreateControl` (+ attached labels via `Parent`) → `HasModule` +
77
+ `Module.AddFromString` → `DoCmd.Close(acSaveYes)` → `DoCmd.Rename` works; geometry round-trips exactly;
78
+ the form opens in Form view (hidden) and binds data.
79
+ - ✅ Unsaved forms closed with `acSaveNo` leave nothing behind (free rollback).
80
+ - ✅ Header/footer: `DoCmd.RunCommand(acCmdFormHdrFtr)` right after `CreateForm` works hidden;
81
+ `CreateControl` into a missing section raises 2148.
82
+ - ✅ Build-then-swap with a hidden `~pak_bak_…` backup name works.
83
+ - ⚠️ A modal **"Save As"** dialog appears in the *hidden* instance when `CloseCurrentDatabase` finds unsaved
84
+ new objects. **Decision:** close every open form/report with `acSaveNo` before closing the database, and
85
+ keep the dialog watchdog alive until the process has exited.
86
+
87
+ ## S7 — SaveAsText / LoadFromText
88
+
89
+ - Forms, queries and macros export as **UTF-16LE with BOM**; they must be re-imported as UTF-16LE (UTF-8
90
+ without BOM loses non-ASCII text).
91
+ - ⚠️ Modules are read **and** written in the **ANSI code page** (cp1252 here); UTF-8 is mangled and a UTF-8
92
+ BOM becomes literal `` code. **Decision:** module codec = ANSI; reject unrepresentable characters.
93
+ - ⚠️ Class modules: Access exports them with four leading `Attribute VB_GlobalNameSpace/Creatable/
94
+ PredeclaredId/Exposed` lines (no `VERSION 1.0 CLASS` header); `LoadFromText` uses those lines to create a
95
+ class module. A VB6-style header yields an empty *standard* module. **Decision:** the codec adds/strips
96
+ exactly Access's header.
97
+ - `LoadFromText` silently overwrites existing objects; imported objects persist.
98
+ - VBE (`app.VBE.ActiveVBProject.VBComponents`) is accessible without Trust Center changes.
99
+
100
+ ## S8 — dialogs
101
+
102
+ - ✅ pywin32 releases the GIL during `Invoke`; a watcher thread detects our PID's visible windows while the
103
+ call blocks. `WM_CLOSE` dismisses OK boxes; **Yes/No boxes ignore `WM_CLOSE`** and need `BM_CLICK` on a
104
+ button (preference Cancel → No → OK). Button captions carry mnemonics (`&No`).
105
+ - ⚠️ `app.Run("Proc")` through pywin32's typed wrapper fails (`DISP_E_BADPARAMCOUNT`); an exact-argument
106
+ `Invoke` works (`'ok'`, `Add2(2,3) → 5`). Same root cause as S6.
107
+
108
+ ## S9 — errors
109
+
110
+ - All Access/DAO errors arrive as `DISP_E_EXCEPTION` with `scode = 0x800A0000 | number`; DAO errors carry
111
+ `source='DAO.<Collection>'`, Access errors `source=None` with a filled description.
112
+ - Verified numbers: 3265, 3010, 3012, 3191, 3270, 3609 (no unique index for relationship), 3201 (existing
113
+ data violates integrity), 3368, 3125, 3141, 3129, 3075, 3061, 3292, 2102, 2148, 2149, 2220, 2462, 2467,
114
+ 2493, 3045, 3024, 3343, 3204, 7865, 7866, 7874.
115
+ - ⚠️ `DBEngine.Errors` keeps **stale** entries after Access-level errors → attach it only when its last
116
+ entry matches the current error number.
117
+ - ⚠️ **`OpenCurrentDatabase` fails silently** (no exception, no dialog) for locked or non-database files:
118
+ `CurrentProject.FullName == ''`, `CurrentDb() is None`. **Decision:** verify every open and, on failure,
119
+ probe with DAO `OpenDatabase` to raise the precise error (locked / format / missing).
120
+
121
+ ## S10 — in-process DAO (x86 Python, C2R Office)
122
+
123
+ - ✅ `DAO.DBEngine.120` and ADODB + `Microsoft.ACE.OLEDB.16.0` work in-process from 32-bit Python under
124
+ Click-to-Run (engine ready in ~0.1 s).
125
+ - ADO cannot open a file DAO holds exclusively; with DAO closed, ADO decimal DDL works and DAO then reads
126
+ `Precision`/`Scale` correctly.
@@ -0,0 +1,24 @@
1
+ # ADR 0002 — Findings from the first integration runs
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-09-26
5
+ - **Context:** the first end-to-end runs of the full stack (smoke script, contract suite on real Access,
6
+ lifecycle and round-trip suites) surfaced behaviours the Phase-0 spikes had not exercised. Each one is now
7
+ covered by a test.
8
+
9
+ | # | Finding | Decision |
10
+ |---|---|---|
11
+ | 1 | ``DoCmd.SetWarnings`` raises Access error 2046 ("isn't available now") when no database is open. | Called (best effort) only after a design session has a current database. |
12
+ | 2 | Late-bound ``Index.Fields`` of an *appended* index returns a **string** (``"+Col1;-Col2"``), not a collection. | ``_index_fields`` parses the string form (``+`` ascending, ``-`` descending). |
13
+ | 3 | pywin32's dynamic dispatch resolves ``Item`` as a *property* on some DAO collections and returns the wrong object. | All DAO collection access goes through the gateway's exact ``IDispatch.Invoke`` (``_item``). |
14
+ | 4 | pywin32 treats a naive ``datetime`` as local time and converts it to UTC when building a ``VT_DATE`` (a 13-hour shift was observed). | ``to_variant`` passes wall-clock values as UTC-tagged datetimes; ``normalize`` reads the wall-clock back. |
15
+ | 5 | DAO silently ignores ``DefaultValue`` on an auto-increment field, so *random* AutoNumbers (``GenUniqueID()``) cannot be created. | ``AutoNumberColumn(new_values=...)`` was removed rather than shipped half-working. |
16
+ | 6 | Releasing a COM proxy after Access has exited makes COM raise-and-handle ``RPC_E_DISCONNECTED``; ``faulthandler`` prints it as a "Windows fatal exception". | ``gc.collect()`` before quitting releases unreachable proxies while Access is alive; the test-suite disables pytest's faulthandler plugin. |
17
+ | 7 | ``WaitForSingleObject`` needs ``SYNCHRONIZE``; a query-only handle made live orphans look "already exited". | Identity checks open processes with ``SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION``. |
18
+ | 8 | Race: dismissing a dialog unblocks the COM call before the watchdog had recorded the dialog. | Dialogs are recorded *before* dismissal; ``end()`` waits for in-progress handling. |
19
+ | 9 | After the watchdog terminated Access (timeout), shutdown's COM steps could only fail. | Shutdown skips COM steps when the owned process is already gone; closing stays quiet. |
20
+ | 10 | ``AutomationSecurity`` (``macro_security``) disables VBA and ``AutoExec``, but ``OpenCurrentDatabase`` still opens the database's ``StartUpForm``; with code disabled the form can raise a dialog, so design sessions failed on apps that set one. | Before a design open the engine removes ``StartUpForm`` through DAO (which runs nothing) and restores it as soon as the database is open (or through DAO again if the open fails). |
21
+ | 11 | Access-application functions (``Nz``, VBA functions from modules) are unavailable to SQL run through DAO outside the Access UI (DAO error 3085 "Undefined function"). They do work in form control sources. | Documented for users and agents: SQL that Python runs uses engine functions only (``IIf``, ``IsNull``…). |
22
+ | 12 | A VBA module that does not compile is only reported when Access next compiles code, e.g. when a form expression uses it (Access error 7960 during the form build). | Documented; ``check_opens()`` on every form is the recommended verification step. A dedicated ``VbaCompileError`` is planned for 0.2. |
23
+ | 13 | Implicit QueryDef parameters (a bare ``[name]`` in SQL) are typed as text; binding ``bytes`` to one stores a corrupted, doubled value. ``Parameter.Type`` cannot be changed for implicit parameters. | Ad-hoc SQL declares bytes-valued parameters (``PARAMETERS [x] LongBinary;``) automatically; saved queries with a non-binary parameter reject bytes with ``SpecError`` explaining the declaration. OLE/binary values read back as ``bytes``. |
24
+ | 14 | A database created by in-process DAO has no ``Forms``/``Reports``/``Scripts``/``Modules`` containers until Access opens it; reading ``Containers("Forms")`` fails with error 3265. | Listing objects of a kind whose container is missing returns an empty list. |
@@ -0,0 +1,3 @@
1
+ <!-- The agent guide ships inside the package (pyaccesskit guide); this page includes that file. -->
2
+
3
+ --8<-- "src/pyaccesskit/AGENT_GUIDE.md"