pyaccesskit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. pyaccesskit/AGENT_GUIDE.md +455 -0
  2. pyaccesskit/__init__.py +167 -0
  3. pyaccesskit/__main__.py +6 -0
  4. pyaccesskit/_backends/__init__.py +0 -0
  5. pyaccesskit/_backends/access/__init__.py +1 -0
  6. pyaccesskit/_backends/access/design.py +415 -0
  7. pyaccesskit/_backends/dao/__init__.py +1 -0
  8. pyaccesskit/_backends/dao/profile.py +40 -0
  9. pyaccesskit/_backends/dao/schema.py +805 -0
  10. pyaccesskit/_backends/dao/typemap.py +390 -0
  11. pyaccesskit/_backends/fake/__init__.py +3 -0
  12. pyaccesskit/_backends/fake/backend.py +680 -0
  13. pyaccesskit/_backends/protocols.py +339 -0
  14. pyaccesskit/_com/__init__.py +1 -0
  15. pyaccesskit/_com/constants.py +394 -0
  16. pyaccesskit/_com/dispatch.py +50 -0
  17. pyaccesskit/_com/errors.py +184 -0
  18. pyaccesskit/_com/gateway.py +199 -0
  19. pyaccesskit/_com/raw.py +164 -0
  20. pyaccesskit/_com/runtime.py +39 -0
  21. pyaccesskit/_com/variants.py +72 -0
  22. pyaccesskit/_engines/__init__.py +48 -0
  23. pyaccesskit/_engines/access.py +300 -0
  24. pyaccesskit/_engines/inproc.py +148 -0
  25. pyaccesskit/_engines/probe.py +231 -0
  26. pyaccesskit/_ledger.py +158 -0
  27. pyaccesskit/_ops/__init__.py +0 -0
  28. pyaccesskit/_ops/design.py +127 -0
  29. pyaccesskit/_ops/schema.py +471 -0
  30. pyaccesskit/_session/__init__.py +1 -0
  31. pyaccesskit/_session/protocols.py +78 -0
  32. pyaccesskit/_session/session.py +354 -0
  33. pyaccesskit/_text/__init__.py +0 -0
  34. pyaccesskit/_text/codec.py +114 -0
  35. pyaccesskit/_version.py +3 -0
  36. pyaccesskit/_win/__init__.py +1 -0
  37. pyaccesskit/_win/access_process.py +348 -0
  38. pyaccesskit/_win/console.py +56 -0
  39. pyaccesskit/_win/inspector.py +53 -0
  40. pyaccesskit/_win/job.py +65 -0
  41. pyaccesskit/_win/processes.py +159 -0
  42. pyaccesskit/_win/watchdog.py +253 -0
  43. pyaccesskit/cli/__init__.py +10 -0
  44. pyaccesskit/cli/_output.py +101 -0
  45. pyaccesskit/cli/agent.py +99 -0
  46. pyaccesskit/cli/app.py +54 -0
  47. pyaccesskit/cli/cleanup.py +56 -0
  48. pyaccesskit/cli/doctor.py +101 -0
  49. pyaccesskit/cli/inspection.py +223 -0
  50. pyaccesskit/database.py +296 -0
  51. pyaccesskit/diagnostics.py +319 -0
  52. pyaccesskit/enums.py +258 -0
  53. pyaccesskit/errors.py +407 -0
  54. pyaccesskit/forms/__init__.py +45 -0
  55. pyaccesskit/forms/builder.py +295 -0
  56. pyaccesskit/forms/collection.py +117 -0
  57. pyaccesskit/forms/controls.py +157 -0
  58. pyaccesskit/forms/layout.py +300 -0
  59. pyaccesskit/forms/spec.py +169 -0
  60. pyaccesskit/forms/vba.py +138 -0
  61. pyaccesskit/maintenance.py +32 -0
  62. pyaccesskit/modules.py +101 -0
  63. pyaccesskit/objects.py +81 -0
  64. pyaccesskit/options.py +40 -0
  65. pyaccesskit/properties.py +74 -0
  66. pyaccesskit/py.typed +0 -0
  67. pyaccesskit/queries.py +190 -0
  68. pyaccesskit/relationships.py +143 -0
  69. pyaccesskit/schema/__init__.py +73 -0
  70. pyaccesskit/schema/_base.py +55 -0
  71. pyaccesskit/schema/_reserved_words.py +55 -0
  72. pyaccesskit/schema/columns.py +609 -0
  73. pyaccesskit/schema/compat.py +57 -0
  74. pyaccesskit/schema/expressions.py +162 -0
  75. pyaccesskit/schema/indexes.py +114 -0
  76. pyaccesskit/schema/names.py +122 -0
  77. pyaccesskit/schema/queries.py +192 -0
  78. pyaccesskit/schema/relationships.py +132 -0
  79. pyaccesskit/schema/tables.py +178 -0
  80. pyaccesskit/tables.py +333 -0
  81. pyaccesskit/units.py +301 -0
  82. pyaccesskit-0.1.0.dist-info/METADATA +201 -0
  83. pyaccesskit-0.1.0.dist-info/RECORD +86 -0
  84. pyaccesskit-0.1.0.dist-info/WHEEL +4 -0
  85. pyaccesskit-0.1.0.dist-info/entry_points.txt +2 -0
  86. pyaccesskit-0.1.0.dist-info/licenses/LICENSE +21 -0
pyaccesskit/units.py ADDED
@@ -0,0 +1,301 @@
1
+ # pyright: reportUnnecessaryIsInstance=false
2
+ # (operators and parse() deliberately type-check arguments at runtime)
3
+ """Physical lengths for form and report layout.
4
+
5
+ Access measures positions and sizes in **twips** (1/1440 inch, 1/20 point). PyAccessKit lets you work in
6
+ familiar units instead and converts exactly once, when talking to Access::
7
+
8
+ from pyaccesskit.units import cm, mm, inch
9
+
10
+ width = cm(6) + mm(5) # Length(twips=3685)
11
+ width.cm # 6.5 (approximately; lengths are whole twips)
12
+
13
+ A :class:`Length` is an immutable whole number of twips. In specs it serializes to a readable string such
14
+ as ``"6.5cm"`` and parses from strings (``"2cm"``, ``"10mm"``, ``"1.5in"``, ``"12pt"``, ``"300tw"``) or plain
15
+ integers (twips).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+ import re
22
+ from typing import TYPE_CHECKING, Any, SupportsFloat
23
+
24
+ from pydantic_core import core_schema
25
+
26
+ if TYPE_CHECKING:
27
+ from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
28
+ from pydantic.json_schema import JsonSchemaValue
29
+
30
+ __all__ = [
31
+ "TWIPS_PER_CM",
32
+ "TWIPS_PER_INCH",
33
+ "TWIPS_PER_MM",
34
+ "TWIPS_PER_POINT",
35
+ "Length",
36
+ "cm",
37
+ "inch",
38
+ "mm",
39
+ "pt",
40
+ "twips",
41
+ ]
42
+
43
+ TWIPS_PER_INCH = 1440
44
+ TWIPS_PER_POINT = 20
45
+ TWIPS_PER_CM = TWIPS_PER_INCH / 2.54
46
+ TWIPS_PER_MM = TWIPS_PER_CM / 10
47
+
48
+ _UNIT_FACTORS: dict[str, float] = {
49
+ "tw": 1,
50
+ "twip": 1,
51
+ "twips": 1,
52
+ "pt": TWIPS_PER_POINT,
53
+ "point": TWIPS_PER_POINT,
54
+ "points": TWIPS_PER_POINT,
55
+ "mm": TWIPS_PER_MM,
56
+ "cm": TWIPS_PER_CM,
57
+ "in": TWIPS_PER_INCH,
58
+ "inch": TWIPS_PER_INCH,
59
+ "inches": TWIPS_PER_INCH,
60
+ '"': TWIPS_PER_INCH,
61
+ }
62
+ _PATTERN = re.compile(r"^\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s*([a-zA-Z\"]*)\s*$")
63
+
64
+
65
+ def _to_twips(value: SupportsFloat, factor: float) -> int:
66
+ number = float(value)
67
+ if not math.isfinite(number):
68
+ raise ValueError(f"length must be finite, got {value!r}")
69
+ return round(number * factor)
70
+
71
+
72
+ class Length:
73
+ """An immutable length, stored as a whole number of twips.
74
+
75
+ Construct lengths with the helper functions :func:`twips`, :func:`pt`, :func:`mm`, :func:`cm` and
76
+ :func:`inch`, or parse text with :meth:`parse`.
77
+ """
78
+
79
+ __slots__ = ("_twips",)
80
+
81
+ _twips: int
82
+
83
+ def __init__(self, twips: int) -> None:
84
+ if isinstance(twips, bool) or not isinstance(twips, int):
85
+ raise TypeError(
86
+ f"Length expects an integer number of twips, got {type(twips).__name__}"
87
+ )
88
+ object.__setattr__(self, "_twips", twips)
89
+
90
+ # --------------------------------------------------------------------------------------- access
91
+ @property
92
+ def twips(self) -> int:
93
+ """The length in twips (the unit Access uses)."""
94
+ return self._twips
95
+
96
+ @property
97
+ def points(self) -> float:
98
+ """The length in typographic points."""
99
+ return self._twips / TWIPS_PER_POINT
100
+
101
+ @property
102
+ def mm(self) -> float:
103
+ """The length in millimetres."""
104
+ return self._twips / TWIPS_PER_MM
105
+
106
+ @property
107
+ def cm(self) -> float:
108
+ """The length in centimetres."""
109
+ return self._twips / TWIPS_PER_CM
110
+
111
+ @property
112
+ def inches(self) -> float:
113
+ """The length in inches."""
114
+ return self._twips / TWIPS_PER_INCH
115
+
116
+ # ---------------------------------------------------------------------------------- conversion
117
+ @classmethod
118
+ def parse(cls, value: str | int | Length) -> Length:
119
+ """Parse ``"2cm"``, ``"10 mm"``, ``"1.5in"``, ``"12pt"``, ``"300tw"`` or an integer number of twips.
120
+
121
+ Raises:
122
+ ValueError: If the text is not a recognised length.
123
+ """
124
+ if isinstance(value, Length):
125
+ return value
126
+ if isinstance(value, bool):
127
+ raise TypeError("a boolean is not a length")
128
+ if isinstance(value, int):
129
+ return cls(value)
130
+ match = _PATTERN.match(value)
131
+ if match is None:
132
+ raise ValueError(
133
+ f"not a length: {value!r} (expected e.g. '2cm', '10mm', '1.5in', '12pt')"
134
+ )
135
+ number, unit = match.group(1), match.group(2).lower()
136
+ if not unit:
137
+ if "." in number:
138
+ raise ValueError(
139
+ f"a unitless length must be a whole number of twips, got {value!r}"
140
+ )
141
+ return cls(int(number))
142
+ factor = _UNIT_FACTORS.get(unit)
143
+ if factor is None:
144
+ raise ValueError(f"unknown length unit {unit!r} in {value!r}")
145
+ return cls(_to_twips(float(number), factor))
146
+
147
+ def format(self, unit: str = "cm") -> str:
148
+ """Format in ``unit`` (``"cm"``, ``"mm"``, ``"in"``, ``"pt"`` or ``"tw"``).
149
+
150
+ The result is the *shortest* decimal that parses back to exactly the same number of twips:
151
+ ``cm(1.5).format() == "1.5cm"`` and ``Length.parse(x.format(unit)) == x`` for every length and unit.
152
+ """
153
+ key = unit.lower()
154
+ factor = _UNIT_FACTORS.get(key)
155
+ if factor is None:
156
+ raise ValueError(f"unknown length unit {unit!r}")
157
+ if factor == 1:
158
+ return f"{self._twips}tw"
159
+ value = self._twips / factor
160
+ text = f"{value:.6f}"
161
+ for decimals in range(7):
162
+ text = f"{value:.{decimals}f}"
163
+ if _to_twips(float(text), factor) == self._twips:
164
+ break
165
+ text = text.rstrip("0").rstrip(".") if "." in text else text
166
+ return f"{'0' if text in ('', '-0') else text}{key}"
167
+
168
+ # ---------------------------------------------------------------------------------- arithmetic
169
+ def __add__(self, other: Length) -> Length:
170
+ if not isinstance(other, Length):
171
+ return NotImplemented
172
+ return Length(self._twips + other._twips)
173
+
174
+ def __sub__(self, other: Length) -> Length:
175
+ if not isinstance(other, Length):
176
+ return NotImplemented
177
+ return Length(self._twips - other._twips)
178
+
179
+ def __mul__(self, factor: float) -> Length:
180
+ if isinstance(factor, bool) or not isinstance(factor, (int, float)):
181
+ return NotImplemented
182
+ return Length(_to_twips(self._twips, factor))
183
+
184
+ __rmul__ = __mul__
185
+
186
+ def __truediv__(self, other: float | Length) -> Any:
187
+ if isinstance(other, Length):
188
+ return self._twips / other._twips
189
+ if isinstance(other, bool) or not isinstance(other, (int, float)):
190
+ return NotImplemented
191
+ return Length(_to_twips(self._twips, 1 / other))
192
+
193
+ def __neg__(self) -> Length:
194
+ return Length(-self._twips)
195
+
196
+ def __pos__(self) -> Length:
197
+ return self
198
+
199
+ def __abs__(self) -> Length:
200
+ return Length(abs(self._twips))
201
+
202
+ def __bool__(self) -> bool:
203
+ return self._twips != 0
204
+
205
+ # ---------------------------------------------------------------------------------- comparison
206
+ def __eq__(self, other: object) -> bool:
207
+ if not isinstance(other, Length):
208
+ return NotImplemented
209
+ return self._twips == other._twips
210
+
211
+ def __lt__(self, other: Length) -> bool:
212
+ if not isinstance(other, Length):
213
+ return NotImplemented
214
+ return self._twips < other._twips
215
+
216
+ def __le__(self, other: Length) -> bool:
217
+ if not isinstance(other, Length):
218
+ return NotImplemented
219
+ return self._twips <= other._twips
220
+
221
+ def __gt__(self, other: Length) -> bool:
222
+ if not isinstance(other, Length):
223
+ return NotImplemented
224
+ return self._twips > other._twips
225
+
226
+ def __ge__(self, other: Length) -> bool:
227
+ if not isinstance(other, Length):
228
+ return NotImplemented
229
+ return self._twips >= other._twips
230
+
231
+ def __hash__(self) -> int:
232
+ return hash(("Length", self._twips))
233
+
234
+ def __setattr__(self, name: str, value: object) -> None:
235
+ raise AttributeError("Length is immutable")
236
+
237
+ def __repr__(self) -> str:
238
+ return f"Length(twips={self._twips})"
239
+
240
+ def __str__(self) -> str:
241
+ return self.format("cm")
242
+
243
+ # ------------------------------------------------------------------------------------- pydantic
244
+ @classmethod
245
+ def __get_pydantic_core_schema__(
246
+ cls, _source: Any, _handler: GetCoreSchemaHandler
247
+ ) -> core_schema.CoreSchema:
248
+ def validate(value: Any) -> Length:
249
+ if isinstance(value, (Length, str, int)):
250
+ return cls.parse(value)
251
+ # ValueError (not TypeError) so Pydantic reports a normal ValidationError.
252
+ raise ValueError(
253
+ f"expected a Length, a string like '2cm', or an integer of twips; got {value!r}"
254
+ )
255
+
256
+ return core_schema.no_info_plain_validator_function(
257
+ validate,
258
+ serialization=core_schema.plain_serializer_function_ser_schema(
259
+ lambda length: length.format("cm"), when_used="json"
260
+ ),
261
+ )
262
+
263
+ @classmethod
264
+ def __get_pydantic_json_schema__(
265
+ cls, _schema: core_schema.CoreSchema, _handler: GetJsonSchemaHandler
266
+ ) -> JsonSchemaValue:
267
+ return {
268
+ "anyOf": [
269
+ {
270
+ "type": "string",
271
+ "pattern": r"^\s*[+-]?(\d+(\.\d*)?|\.\d+)\s*(tw|twips?|pt|points?|mm|cm|in|inch|inches)\s*$",
272
+ },
273
+ {"type": "integer", "description": "twips"},
274
+ ],
275
+ "description": "A length such as '2cm', '10mm', '1.5in', '12pt', or an integer number of twips.",
276
+ }
277
+
278
+
279
+ def twips(value: int) -> Length:
280
+ """A length of ``value`` twips (1/1440 inch)."""
281
+ return Length(value)
282
+
283
+
284
+ def pt(value: float) -> Length:
285
+ """A length of ``value`` typographic points (20 twips each)."""
286
+ return Length(_to_twips(value, TWIPS_PER_POINT))
287
+
288
+
289
+ def mm(value: float) -> Length:
290
+ """A length of ``value`` millimetres."""
291
+ return Length(_to_twips(value, TWIPS_PER_MM))
292
+
293
+
294
+ def cm(value: float) -> Length:
295
+ """A length of ``value`` centimetres."""
296
+ return Length(_to_twips(value, TWIPS_PER_CM))
297
+
298
+
299
+ def inch(value: float) -> Length:
300
+ """A length of ``value`` inches."""
301
+ return Length(_to_twips(value, TWIPS_PER_INCH))
@@ -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,86 @@
1
+ pyaccesskit/AGENT_GUIDE.md,sha256=aoLu67-zUau0vFG5o_98ZFnnVLeIOKSlmysuG_033m8,23262
2
+ pyaccesskit/__init__.py,sha256=mWCH8BYs6BCaIyOy0I52j6UAmtaJRLeB4YupitMVdoQ,3714
3
+ pyaccesskit/__main__.py,sha256=-wO3_oGdJ1BGc_y9RMNWRX1MOPhFqZcY-fCWgXu1cZU,138
4
+ pyaccesskit/_ledger.py,sha256=9q3UKnn_xC5qQ5TgyJw1CExByqieEGY22sIA4-ne0h4,5472
5
+ pyaccesskit/_version.py,sha256=Y_2bqYDoRNq0PnhUrLbcwDTa_O-tiNwjoBWEyVHCbyk,97
6
+ pyaccesskit/database.py,sha256=6iSzKwTWcfhYB6xtCkCl8Xguvvfv4apa-6F1aCfROIE,11135
7
+ pyaccesskit/diagnostics.py,sha256=uloHujGHFtKH8v-1F-pHu09JGy11cb54Z3pL1u87fQQ,11372
8
+ pyaccesskit/enums.py,sha256=VPmMAv1stpjOEXLMScQL7t4AOXk4EjSI1DLe7bw6NWw,7405
9
+ pyaccesskit/errors.py,sha256=rN3rTHE2svxomhX33WrCJ9sB30gxv51-RtwcGYXMsGU,13220
10
+ pyaccesskit/maintenance.py,sha256=I2s7OMT-ZgXIJAMnZC3_TwZxZlBPWj80LlbhCavtZiQ,1267
11
+ pyaccesskit/modules.py,sha256=nEvOq96BRZ1l-t5KS1I8zBJRl5dqd4_WUWNPj1IL2rE,3670
12
+ pyaccesskit/objects.py,sha256=5FnK5CZWDBx79zWYecNK4R8FXrTXMjlTU6bLB3erQGo,3242
13
+ pyaccesskit/options.py,sha256=zcm_yezWvdbVmVN-RfjdjQlHi6cab-XGdhp_qb2nzOY,1871
14
+ pyaccesskit/properties.py,sha256=EWEExLazI8SMuOCTJHea_7LAtJnRlFWCQSvmhOBrCqg,2820
15
+ pyaccesskit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ pyaccesskit/queries.py,sha256=YGBF5i_ykKZMWJfKfQqeZsUp-0KOtA5uf7-2assqi_s,6629
17
+ pyaccesskit/relationships.py,sha256=-7gW36qpvQCYgTkFevlu7b8ZzqVevXZRaDMaXvulQAg,4754
18
+ pyaccesskit/tables.py,sha256=eWNsJj6MJdvBBLt2T-Fy8UDtKHEU2tqRYF7I6eZ4w-A,11619
19
+ pyaccesskit/units.py,sha256=GNN6qcPspZEjGlrSK7A5-7A2wH1gojt2mxUWAfcxb7Y,10125
20
+ pyaccesskit/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ pyaccesskit/_backends/protocols.py,sha256=-dmQTGpvyZ2gD78-FK_3KN-7CoqDEulo-HGLkpgzNSw,10475
22
+ pyaccesskit/_backends/access/__init__.py,sha256=XoP3TE17S_WWjVhanyMrmZ1Wnj6nUuH2Uk-2oYst7BA,72
23
+ pyaccesskit/_backends/access/design.py,sha256=_MMXPz-IiaVdA-Kdr_M_uHdmo92qnLp-uBX5LlWVUOA,17368
24
+ pyaccesskit/_backends/dao/__init__.py,sha256=pdzTRkF-LVtPiuGfRJBl4zAs8eZiweby26Tteg9fvJo,57
25
+ pyaccesskit/_backends/dao/profile.py,sha256=hHxKK_lnN58a96_qZoA_9ZwEUylrBGciQNboEc7bwUc,1640
26
+ pyaccesskit/_backends/dao/schema.py,sha256=vWe892shAl5HSF7MrCnHoWlBrD3fNC3Qfpa_ybAY6Ck,33780
27
+ pyaccesskit/_backends/dao/typemap.py,sha256=KaGCKcD3ZlcBh7atIaPvkgcchIjSdibi4DrunDZfT8o,12918
28
+ pyaccesskit/_backends/fake/__init__.py,sha256=VDcYa1B2vZAWMnoJW9qG6di9rMYy_8j5MJZGkUx9zOU,86
29
+ pyaccesskit/_backends/fake/backend.py,sha256=rI39xSbgc_kC-XvGLO80cYati-aibi64gMOd3pGsIc0,27386
30
+ pyaccesskit/_com/__init__.py,sha256=2yeD-cfAEkAQtCJCFIuH-OkjfHV3ehT8U892SwS0LU4,89
31
+ pyaccesskit/_com/constants.py,sha256=PrTiwyb_Jd-LpL4qpkjzE05Z6B9ou3b6UlZiZqhjEwk,10145
32
+ pyaccesskit/_com/dispatch.py,sha256=lGblOMRuLM4KlZ0Qmyick8-jhviIRZHjo1gsEdvPwow,2054
33
+ pyaccesskit/_com/errors.py,sha256=uu4ihD7JbZubK0AW52hs3usp-9J2yC6MrZqL4rrikzw,6747
34
+ pyaccesskit/_com/gateway.py,sha256=N6KThEiPnge8OWOPQ_i_jQKNkBB961IdCCMycSZTnKg,7209
35
+ pyaccesskit/_com/raw.py,sha256=xj-Uxqnw9JyE1FWphJH3pP_3B0YAYPK7lz2lEKPzw8s,6472
36
+ pyaccesskit/_com/runtime.py,sha256=Oulp7DBjGOXBaWy2YtYAXIpBkR2FjKiRc10OxTspFEc,1365
37
+ pyaccesskit/_com/variants.py,sha256=CbgD9HSbc1Vr5CkwYyWWfIvEgmq8YlLBWLG16p5_fGw,2606
38
+ pyaccesskit/_engines/__init__.py,sha256=EEwx_0Z3Efbk7Tj7aGruRSx5e2sw5ZvmhkxTKVCHuAo,1539
39
+ pyaccesskit/_engines/access.py,sha256=XH0wjqYUTYL4_nB7Xf8nGGwlWZbXttZPzv8ygq0nVEc,11536
40
+ pyaccesskit/_engines/inproc.py,sha256=vbDYUjE93E6G4g2_8JMPY4iehg3cruO888Uc44qeXHw,5632
41
+ pyaccesskit/_engines/probe.py,sha256=VhmFQCRgRTIGErUrFVg9xdL47W0TWPuuiaWkufFYWg8,7800
42
+ pyaccesskit/_ops/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
+ pyaccesskit/_ops/design.py,sha256=3FQV_q6KXvG-2qtp1_7gEteNmGCnfa8wojwJi_sfU2Y,5261
44
+ pyaccesskit/_ops/schema.py,sha256=w1JaKpKgGLrqK3y9sfa2Px_p849Jq5dhADp2ZfdpKO8,18765
45
+ pyaccesskit/_session/__init__.py,sha256=COEPkJJZ211VBdtL1hKrSBjpQw4aHABRzJig0u61HEk,37
46
+ pyaccesskit/_session/protocols.py,sha256=Rmra7Q40_ifzaXzTHG6LZ9Om2r0GFFTcNqtSRJQ0JG8,2281
47
+ pyaccesskit/_session/session.py,sha256=QfNJl3FXEX_xkIzvFqjlBxY3412_E_tQafyOHtmA57M,13910
48
+ pyaccesskit/_text/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ pyaccesskit/_text/codec.py,sha256=mtNijZTAkLb_N1p-dWs9oPJ0j_Pru4DP9Hs9fc6VtiY,4542
50
+ pyaccesskit/_win/__init__.py,sha256=fVmdNR3LqQ4yO5uXmMMqpNse2y8Doh1HerNESLFRvaI,74
51
+ pyaccesskit/_win/access_process.py,sha256=PyNNgJciGvyS4M_DeOfbtnW1M6_3j--0DYl0W6X6Mww,13918
52
+ pyaccesskit/_win/console.py,sha256=aJNJdSr5QGVvK-goiFRzEockVeodsPTQ9IkHACNuUjs,1987
53
+ pyaccesskit/_win/inspector.py,sha256=iCpo8gpO_f7g0Xhl3NgIds_JnSjT449BmU9HqtwQLZY,1654
54
+ pyaccesskit/_win/job.py,sha256=lSORBjfLv5DvsPFCC9VsD3COkSJ6gOXqgjAzdbNHXEE,2419
55
+ pyaccesskit/_win/processes.py,sha256=RCrQtuyOVl8kStgXPsMVWnMeLLRJN7WN8OSvfygcg2k,5087
56
+ pyaccesskit/_win/watchdog.py,sha256=fdD1gpWnlgK-EJ_KnkGsC5h_JuTPwV6_6-g8FhHwrIs,10176
57
+ pyaccesskit/cli/__init__.py,sha256=UYgcIpZjVC72lvjjTrFpORxIo2aGcelRxGxMQ6XdOyk,319
58
+ pyaccesskit/cli/_output.py,sha256=zOB2OqiLRghfQLy7jAkmKNMTqDVQlO38M4MzX0rqd-U,3170
59
+ pyaccesskit/cli/agent.py,sha256=JTLOFjfoR-IDXyqEi1QncTFieuLvCGpKleNx1FnLZNY,3012
60
+ pyaccesskit/cli/app.py,sha256=EEF0ETLx9aKCaL4ELMc9ecsrTeONw-2GJUhY2DOgs7I,1435
61
+ pyaccesskit/cli/cleanup.py,sha256=6lNbgp3BvOe7dvBpemaf_36rlfQfL3sgpmLCTaBSqWo,2127
62
+ pyaccesskit/cli/doctor.py,sha256=OUyE7evPd6RBMl4i4SzFoQGS_iPkuuG5CA5HzqbqU-w,3956
63
+ pyaccesskit/cli/inspection.py,sha256=qnghAMfDkyyXxc5Ghsrtdy1G6Aq14l1t78ONeVkrF6o,8812
64
+ pyaccesskit/forms/__init__.py,sha256=UvUDMCrJK5YFdEi5Le18nTDS3tyHIsJlTmtzWuOL2RE,967
65
+ pyaccesskit/forms/builder.py,sha256=IVHn6g3ERgabLpVbELfplG2BCWipm4_14FpYQKWRT0k,10135
66
+ pyaccesskit/forms/collection.py,sha256=bR09O_C5y1McwHOGNYvv-F-tkTPdAdideyxStkHPKTo,4427
67
+ pyaccesskit/forms/controls.py,sha256=PnmqgffPL0ruTpiI_dQTFnaQOop0L0uP87ME67ivVIU,5330
68
+ pyaccesskit/forms/layout.py,sha256=7IshbmwQ-d7Wh-4fNTjlKJnbiL5DQ1lwPv7NzbIFmwg,10892
69
+ pyaccesskit/forms/spec.py,sha256=LoamddafCn_Do3B71u-x-2C0HDQR4F82Jm-JlmGRCh0,7170
70
+ pyaccesskit/forms/vba.py,sha256=XCvNZ2QmEgYJ-KZjBzoJj1VJElCzfDaBIYi6C8k1hw8,4928
71
+ pyaccesskit/schema/__init__.py,sha256=uerme7BX7U3obaLBHyt5iYHrb8oLH-_lUJUZegxHh0s,1878
72
+ pyaccesskit/schema/_base.py,sha256=lEKSozfcCcq8ZU0YEQv9ZgvyrXfXFWUTTZTU1gACcEw,2070
73
+ pyaccesskit/schema/_reserved_words.py,sha256=49auHhwSN7113MHDYtIVt40JW3b3y_nZ0BOPIURVqa0,4308
74
+ pyaccesskit/schema/columns.py,sha256=v8VwVxeE3Eo7_o7f3Pdkt9-8oRj72gko30mtqpzmXFE,20979
75
+ pyaccesskit/schema/compat.py,sha256=euH8zFW8fU1eguOPnfv2wPbk044Av4mOqefAJEBgDN0,1829
76
+ pyaccesskit/schema/expressions.py,sha256=83RIyxqaN9QVp3UOw-mhc9JRmrm8oydcC4yz3_NgUPE,5859
77
+ pyaccesskit/schema/indexes.py,sha256=eWMrHmM6yPoB6sQz4zG-8PDk8-aPuyf3wY6JDuOon58,3964
78
+ pyaccesskit/schema/names.py,sha256=FDPxz-bOyzG2oL3ny5cy0-5xKRiY9vQkhu0NGnQzKRM,4179
79
+ pyaccesskit/schema/queries.py,sha256=K-zGSnp0nk8KWaiu-rfAdIc8FgW6OOBKb_x5s7Bl4E4,6352
80
+ pyaccesskit/schema/relationships.py,sha256=S8VOkf-CqMd3CT5Kr7M6wzYOPU6i32pX8rb6XPu6Uhw,5632
81
+ pyaccesskit/schema/tables.py,sha256=HPr0b_PwoAq9wn4BL1NjcWDWDDk-khx8XoWfOg5ehNA,7344
82
+ pyaccesskit-0.1.0.dist-info/METADATA,sha256=3YoLUOx13Cgq-rwvQQJnsmqpk0K9AuqXGy1nf9wlMK8,11165
83
+ pyaccesskit-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
84
+ pyaccesskit-0.1.0.dist-info/entry_points.txt,sha256=uej_q9QiApCGzSHB_jdp8YsIEGZfVmeAMomSeH8bASo,53
85
+ pyaccesskit-0.1.0.dist-info/licenses/LICENSE,sha256=k6V4ZLGuzf_kqxWbvrJwgbHBIW52My0HV-aYARZJULw,1065
86
+ pyaccesskit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyaccesskit = pyaccesskit.cli:main
@@ -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.