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.
- pyaccesskit-0.1.0/.gitignore +27 -0
- pyaccesskit-0.1.0/CHANGELOG.md +60 -0
- pyaccesskit-0.1.0/LICENSE +21 -0
- pyaccesskit-0.1.0/PKG-INFO +201 -0
- pyaccesskit-0.1.0/README.md +170 -0
- pyaccesskit-0.1.0/docs/adr/0001-spike-findings.md +126 -0
- pyaccesskit-0.1.0/docs/adr/0002-integration-findings.md +24 -0
- pyaccesskit-0.1.0/docs/agents/guide.md +3 -0
- pyaccesskit-0.1.0/docs/agents/index.md +67 -0
- pyaccesskit-0.1.0/docs/concepts/engines.md +70 -0
- pyaccesskit-0.1.0/docs/concepts/lifecycle.md +99 -0
- pyaccesskit-0.1.0/docs/concepts/specs.md +77 -0
- pyaccesskit-0.1.0/docs/contributing.md +97 -0
- pyaccesskit-0.1.0/docs/environment.md +71 -0
- pyaccesskit-0.1.0/docs/faq.md +44 -0
- pyaccesskit-0.1.0/docs/getting-started.md +129 -0
- pyaccesskit-0.1.0/docs/guides/forms.md +127 -0
- pyaccesskit-0.1.0/docs/guides/modules.md +36 -0
- pyaccesskit-0.1.0/docs/guides/queries.md +65 -0
- pyaccesskit-0.1.0/docs/guides/recipes.md +176 -0
- pyaccesskit-0.1.0/docs/guides/relationships.md +62 -0
- pyaccesskit-0.1.0/docs/guides/tables.md +116 -0
- pyaccesskit-0.1.0/docs/guides/text-io.md +39 -0
- pyaccesskit-0.1.0/docs/index.md +70 -0
- pyaccesskit-0.1.0/docs/llms-full.txt +1929 -0
- pyaccesskit-0.1.0/docs/llms.txt +41 -0
- pyaccesskit-0.1.0/docs/reference/cli.md +68 -0
- pyaccesskit-0.1.0/docs/reference/data-types.md +58 -0
- pyaccesskit-0.1.0/docs/reference/errors.md +93 -0
- pyaccesskit-0.1.0/docs/releasing.md +73 -0
- pyaccesskit-0.1.0/examples/01_create_crm.py +70 -0
- pyaccesskit-0.1.0/examples/02_inspect_database.py +47 -0
- pyaccesskit-0.1.0/examples/03_build_small_app.py +143 -0
- pyaccesskit-0.1.0/examples/04_inventory_app.py +187 -0
- pyaccesskit-0.1.0/mkdocs.yml +97 -0
- pyaccesskit-0.1.0/pyproject.toml +176 -0
- pyaccesskit-0.1.0/scripts/gen_constants.py +240 -0
- pyaccesskit-0.1.0/scripts/gen_ref_pages.py +46 -0
- pyaccesskit-0.1.0/scripts/sync_docs.py +89 -0
- pyaccesskit-0.1.0/src/pyaccesskit/AGENT_GUIDE.md +455 -0
- pyaccesskit-0.1.0/src/pyaccesskit/__init__.py +167 -0
- pyaccesskit-0.1.0/src/pyaccesskit/__main__.py +6 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_backends/__init__.py +0 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_backends/access/__init__.py +1 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_backends/access/design.py +415 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_backends/dao/__init__.py +1 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_backends/dao/profile.py +40 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_backends/dao/schema.py +805 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_backends/dao/typemap.py +390 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_backends/fake/__init__.py +3 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_backends/fake/backend.py +680 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_backends/protocols.py +339 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_com/__init__.py +1 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_com/constants.py +394 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_com/dispatch.py +50 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_com/errors.py +184 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_com/gateway.py +199 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_com/raw.py +164 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_com/runtime.py +39 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_com/variants.py +72 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_engines/__init__.py +48 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_engines/access.py +300 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_engines/inproc.py +148 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_engines/probe.py +231 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_ledger.py +158 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_ops/__init__.py +0 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_ops/design.py +127 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_ops/schema.py +471 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_session/__init__.py +1 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_session/protocols.py +78 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_session/session.py +354 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_text/__init__.py +0 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_text/codec.py +114 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_version.py +3 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_win/__init__.py +1 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_win/access_process.py +348 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_win/console.py +56 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_win/inspector.py +53 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_win/job.py +65 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_win/processes.py +159 -0
- pyaccesskit-0.1.0/src/pyaccesskit/_win/watchdog.py +253 -0
- pyaccesskit-0.1.0/src/pyaccesskit/cli/__init__.py +10 -0
- pyaccesskit-0.1.0/src/pyaccesskit/cli/_output.py +101 -0
- pyaccesskit-0.1.0/src/pyaccesskit/cli/agent.py +99 -0
- pyaccesskit-0.1.0/src/pyaccesskit/cli/app.py +54 -0
- pyaccesskit-0.1.0/src/pyaccesskit/cli/cleanup.py +56 -0
- pyaccesskit-0.1.0/src/pyaccesskit/cli/doctor.py +101 -0
- pyaccesskit-0.1.0/src/pyaccesskit/cli/inspection.py +223 -0
- pyaccesskit-0.1.0/src/pyaccesskit/database.py +296 -0
- pyaccesskit-0.1.0/src/pyaccesskit/diagnostics.py +319 -0
- pyaccesskit-0.1.0/src/pyaccesskit/enums.py +258 -0
- pyaccesskit-0.1.0/src/pyaccesskit/errors.py +407 -0
- pyaccesskit-0.1.0/src/pyaccesskit/forms/__init__.py +45 -0
- pyaccesskit-0.1.0/src/pyaccesskit/forms/builder.py +295 -0
- pyaccesskit-0.1.0/src/pyaccesskit/forms/collection.py +117 -0
- pyaccesskit-0.1.0/src/pyaccesskit/forms/controls.py +157 -0
- pyaccesskit-0.1.0/src/pyaccesskit/forms/layout.py +300 -0
- pyaccesskit-0.1.0/src/pyaccesskit/forms/spec.py +169 -0
- pyaccesskit-0.1.0/src/pyaccesskit/forms/vba.py +138 -0
- pyaccesskit-0.1.0/src/pyaccesskit/maintenance.py +32 -0
- pyaccesskit-0.1.0/src/pyaccesskit/modules.py +101 -0
- pyaccesskit-0.1.0/src/pyaccesskit/objects.py +81 -0
- pyaccesskit-0.1.0/src/pyaccesskit/options.py +40 -0
- pyaccesskit-0.1.0/src/pyaccesskit/properties.py +74 -0
- pyaccesskit-0.1.0/src/pyaccesskit/py.typed +0 -0
- pyaccesskit-0.1.0/src/pyaccesskit/queries.py +190 -0
- pyaccesskit-0.1.0/src/pyaccesskit/relationships.py +143 -0
- pyaccesskit-0.1.0/src/pyaccesskit/schema/__init__.py +73 -0
- pyaccesskit-0.1.0/src/pyaccesskit/schema/_base.py +55 -0
- pyaccesskit-0.1.0/src/pyaccesskit/schema/_reserved_words.py +55 -0
- pyaccesskit-0.1.0/src/pyaccesskit/schema/columns.py +609 -0
- pyaccesskit-0.1.0/src/pyaccesskit/schema/compat.py +57 -0
- pyaccesskit-0.1.0/src/pyaccesskit/schema/expressions.py +162 -0
- pyaccesskit-0.1.0/src/pyaccesskit/schema/indexes.py +114 -0
- pyaccesskit-0.1.0/src/pyaccesskit/schema/names.py +122 -0
- pyaccesskit-0.1.0/src/pyaccesskit/schema/queries.py +192 -0
- pyaccesskit-0.1.0/src/pyaccesskit/schema/relationships.py +132 -0
- pyaccesskit-0.1.0/src/pyaccesskit/schema/tables.py +178 -0
- pyaccesskit-0.1.0/src/pyaccesskit/tables.py +333 -0
- pyaccesskit-0.1.0/src/pyaccesskit/units.py +301 -0
- pyaccesskit-0.1.0/tests/__init__.py +0 -0
- pyaccesskit-0.1.0/tests/conftest.py +76 -0
- pyaccesskit-0.1.0/tests/contract/__init__.py +0 -0
- pyaccesskit-0.1.0/tests/contract/conftest.py +55 -0
- pyaccesskit-0.1.0/tests/contract/test_design_contract.py +123 -0
- pyaccesskit-0.1.0/tests/contract/test_schema_contract.py +298 -0
- pyaccesskit-0.1.0/tests/fakes.py +153 -0
- pyaccesskit-0.1.0/tests/integration/__init__.py +0 -0
- pyaccesskit-0.1.0/tests/integration/backends.py +58 -0
- pyaccesskit-0.1.0/tests/integration/test_design.py +230 -0
- pyaccesskit-0.1.0/tests/integration/test_examples.py +42 -0
- pyaccesskit-0.1.0/tests/integration/test_lifecycle.py +325 -0
- pyaccesskit-0.1.0/tests/integration/test_roundtrip.py +282 -0
- pyaccesskit-0.1.0/tests/unit/__init__.py +0 -0
- pyaccesskit-0.1.0/tests/unit/test_api_fake.py +133 -0
- pyaccesskit-0.1.0/tests/unit/test_cli.py +187 -0
- pyaccesskit-0.1.0/tests/unit/test_codec.py +88 -0
- pyaccesskit-0.1.0/tests/unit/test_columns.py +148 -0
- pyaccesskit-0.1.0/tests/unit/test_com_errors.py +135 -0
- pyaccesskit-0.1.0/tests/unit/test_dao_sql.py +25 -0
- pyaccesskit-0.1.0/tests/unit/test_docs.py +107 -0
- pyaccesskit-0.1.0/tests/unit/test_expressions.py +111 -0
- pyaccesskit-0.1.0/tests/unit/test_forms.py +249 -0
- pyaccesskit-0.1.0/tests/unit/test_import_isolation.py +48 -0
- pyaccesskit-0.1.0/tests/unit/test_ledger.py +101 -0
- pyaccesskit-0.1.0/tests/unit/test_names.py +71 -0
- pyaccesskit-0.1.0/tests/unit/test_relationships_queries.py +145 -0
- pyaccesskit-0.1.0/tests/unit/test_review_regressions.py +262 -0
- pyaccesskit-0.1.0/tests/unit/test_session.py +160 -0
- pyaccesskit-0.1.0/tests/unit/test_tables.py +124 -0
- pyaccesskit-0.1.0/tests/unit/test_typemap.py +165 -0
- 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. |
|