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/errors.py ADDED
@@ -0,0 +1,407 @@
1
+ """Exception and warning hierarchy.
2
+
3
+ Every exception PyAccessKit raises derives from :class:`PyAccessKitError`. Low-level COM failures are
4
+ translated into specific subclasses; the original ``pywintypes.com_error`` is always kept as
5
+ ``__cause__`` and summarised in :attr:`PyAccessKitError.details`, so nothing is lost for debugging.
6
+
7
+ Several classes also inherit a matching built-in exception (``FileNotFoundError``, ``LookupError``,
8
+ ``ValueError``...) so idiomatic ``except`` clauses keep working.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from pyaccesskit.enums import ObjectKind
19
+
20
+ __all__ = [
21
+ "AccessApplicationError",
22
+ "AccessDialogError",
23
+ "AccessDialogWarning",
24
+ "AccessNameWarning",
25
+ "AccessNotInstalledError",
26
+ "AccessProcessDiedError",
27
+ "AccessRuntimeOnlyError",
28
+ "AccessTimeoutError",
29
+ "CapabilityError",
30
+ "CleanupError",
31
+ "ComError",
32
+ "DaoError",
33
+ "DaoNotAvailableError",
34
+ "DatabaseError",
35
+ "DatabaseExistsError",
36
+ "DatabaseLockedError",
37
+ "DatabaseNotFoundError",
38
+ "DialogInfo",
39
+ "EngineUnavailableError",
40
+ "EnvironmentProblem",
41
+ "ErrorDetails",
42
+ "IntegrityViolationError",
43
+ "InvalidPasswordError",
44
+ "MissingParameterError",
45
+ "ObjectError",
46
+ "ObjectExistsError",
47
+ "ObjectInUseError",
48
+ "ObjectNotFoundError",
49
+ "PyAccessKitError",
50
+ "PyAccessKitWarning",
51
+ "QueryError",
52
+ "ReadOnlyError",
53
+ "RelationshipError",
54
+ "SchemaError",
55
+ "SessionClosedError",
56
+ "SessionError",
57
+ "SpecError",
58
+ "SqlSyntaxError",
59
+ "UnrecognizedFormatError",
60
+ "WrongThreadError",
61
+ ]
62
+
63
+
64
+ # --------------------------------------------------------------------------------------------- details
65
+ @dataclass(frozen=True)
66
+ class DaoError:
67
+ """One entry of DAO's ``DBEngine.Errors`` collection."""
68
+
69
+ number: int
70
+ description: str
71
+ source: str
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class ErrorDetails:
76
+ """Low-level details of the COM error behind a PyAccessKit exception.
77
+
78
+ Attributes:
79
+ hresult: The HRESULT returned by ``IDispatch::Invoke`` (usually ``DISP_E_EXCEPTION``).
80
+ scode: The exception's SCODE (for Access/DAO errors ``0x800A0000 | number``).
81
+ number: The Access/DAO error number (e.g. ``3010``), when the error came from Access or DAO.
82
+ source: Error source, e.g. ``"DAO.TableDefs"`` (``None`` for Access application errors).
83
+ description: The error text reported by Access/DAO.
84
+ dao_errors: Snapshot of ``DBEngine.Errors`` when it matched this error.
85
+ """
86
+
87
+ hresult: int | None = None
88
+ scode: int | None = None
89
+ number: int | None = None
90
+ source: str | None = None
91
+ description: str | None = None
92
+ dao_errors: tuple[DaoError, ...] = ()
93
+
94
+ def summary(self) -> str:
95
+ """Return a one-line, human-readable summary."""
96
+ parts: list[str] = []
97
+ if self.number is not None:
98
+ kind = "DAO" if (self.source or "").startswith("DAO") else "Access"
99
+ parts.append(f"{kind} error {self.number}")
100
+ elif self.scode is not None:
101
+ parts.append(f"SCODE 0x{self.scode & 0xFFFFFFFF:08X}")
102
+ elif self.hresult is not None:
103
+ parts.append(f"HRESULT 0x{self.hresult & 0xFFFFFFFF:08X}")
104
+ if self.description:
105
+ parts.append(self.description.strip())
106
+ return ": ".join(parts)
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class DialogInfo:
111
+ """A modal dialog that Access showed during an automated call."""
112
+
113
+ title: str
114
+ text: str
115
+ buttons: tuple[str, ...] = ()
116
+ action: str = ""
117
+ """How PyAccessKit dealt with it (e.g. ``"closed"``, ``"clicked 'No'"``, ``"terminated Access"``)."""
118
+
119
+ def summary(self) -> str:
120
+ """Return a one-line summary such as ``'Microsoft Access: The expression ... [OK]'``."""
121
+ buttons = f" [{', '.join(self.buttons)}]" if self.buttons else ""
122
+ text = f": {self.text}" if self.text else ""
123
+ return f"{self.title or '(untitled dialog)'}{text}{buttons}"
124
+
125
+
126
+ # ------------------------------------------------------------------------------------------------ base
127
+ class PyAccessKitError(Exception):
128
+ """Base class of every exception raised by PyAccessKit.
129
+
130
+ Attributes:
131
+ message: Human-readable description.
132
+ operation: What PyAccessKit was doing, e.g. ``"create table 'Customers'"``.
133
+ details: COM-level details when the error originated in Access/DAO.
134
+ """
135
+
136
+ def __init__(
137
+ self, message: str, *, operation: str | None = None, details: ErrorDetails | None = None
138
+ ) -> None:
139
+ super().__init__(message)
140
+ self.message = message
141
+ self.operation = operation
142
+ self.details = details
143
+
144
+ def __str__(self) -> str:
145
+ return self.message
146
+
147
+
148
+ class PyAccessKitWarning(UserWarning):
149
+ """Base class of PyAccessKit warnings."""
150
+
151
+
152
+ class AccessNameWarning(PyAccessKitWarning):
153
+ """A name is legal but likely to cause trouble (reserved word, spaces, special characters...)."""
154
+
155
+
156
+ class AccessDialogWarning(PyAccessKitWarning):
157
+ """Access showed a dialog that was dismissed automatically (``dialog_policy="warn"``)."""
158
+
159
+
160
+ # ------------------------------------------------------------------------------------------ environment
161
+ class EnvironmentProblem(PyAccessKitError):
162
+ """The machine cannot do what was asked (missing software, bitness mismatch...)."""
163
+
164
+
165
+ class EngineUnavailableError(EnvironmentProblem):
166
+ """No usable automation engine for the requested operation.
167
+
168
+ Attributes:
169
+ diagnosis: Explanation and advice (installed products, bitness, what to try).
170
+ """
171
+
172
+ def __init__(
173
+ self,
174
+ message: str,
175
+ *,
176
+ diagnosis: str | None = None,
177
+ operation: str | None = None,
178
+ details: ErrorDetails | None = None,
179
+ ) -> None:
180
+ super().__init__(message, operation=operation, details=details)
181
+ self.diagnosis = diagnosis
182
+
183
+ def __str__(self) -> str:
184
+ return f"{self.message}\n{self.diagnosis}" if self.diagnosis else self.message
185
+
186
+
187
+ class AccessNotInstalledError(EngineUnavailableError):
188
+ """Microsoft Access (``Access.Application``) is not installed or cannot be started."""
189
+
190
+
191
+ class DaoNotAvailableError(EngineUnavailableError):
192
+ """In-process DAO (``DAO.DBEngine.120``) cannot be loaded into this Python process."""
193
+
194
+
195
+ class AccessRuntimeOnlyError(EngineUnavailableError):
196
+ """Only the Access Runtime is installed; design features need full Microsoft Access."""
197
+
198
+
199
+ # ------------------------------------------------------------------------------------------- session
200
+ class SessionError(PyAccessKitError):
201
+ """The session cannot perform the operation in its current state."""
202
+
203
+
204
+ class SessionClosedError(SessionError):
205
+ """The database session has been closed."""
206
+
207
+
208
+ class WrongThreadError(SessionError):
209
+ """A session was used from a thread other than the one that created it (COM apartment rule)."""
210
+
211
+
212
+ class CapabilityError(SessionError):
213
+ """The operation needs a capability the session does not have (e.g. forms with ``engine="dao"``)."""
214
+
215
+
216
+ class ReadOnlyError(SessionError):
217
+ """A modification was attempted on a session opened with ``readonly=True``."""
218
+
219
+
220
+ # ------------------------------------------------------------------------------------------ database
221
+ class DatabaseError(PyAccessKitError):
222
+ """A problem with the database file itself.
223
+
224
+ Attributes:
225
+ path: The database path involved, when known.
226
+ """
227
+
228
+ def __init__(
229
+ self,
230
+ message: str,
231
+ *,
232
+ path: Path | str | None = None,
233
+ operation: str | None = None,
234
+ details: ErrorDetails | None = None,
235
+ ) -> None:
236
+ super().__init__(message, operation=operation, details=details)
237
+ self.path = Path(path) if path is not None else None
238
+
239
+
240
+ class DatabaseNotFoundError(DatabaseError, FileNotFoundError):
241
+ """The database file does not exist."""
242
+
243
+
244
+ class DatabaseExistsError(DatabaseError, FileExistsError):
245
+ """The database file already exists (pass ``overwrite=True`` to replace it)."""
246
+
247
+
248
+ class DatabaseLockedError(DatabaseError):
249
+ """The database is in use (opened exclusively elsewhere, or locked)."""
250
+
251
+
252
+ class InvalidPasswordError(DatabaseError):
253
+ """The database password is wrong or missing."""
254
+
255
+
256
+ class UnrecognizedFormatError(DatabaseError):
257
+ """The file is not an Access database (or is damaged / from an unsupported version)."""
258
+
259
+
260
+ # -------------------------------------------------------------------------------------------- objects
261
+ class ObjectError(PyAccessKitError):
262
+ """A problem with a named database object.
263
+
264
+ Attributes:
265
+ kind: Kind of object (table, query, form...).
266
+ name: Object name.
267
+ """
268
+
269
+ def __init__(
270
+ self,
271
+ message: str,
272
+ *,
273
+ kind: ObjectKind | None = None,
274
+ name: str | None = None,
275
+ operation: str | None = None,
276
+ details: ErrorDetails | None = None,
277
+ ) -> None:
278
+ super().__init__(message, operation=operation, details=details)
279
+ self.kind = kind
280
+ self.name = name
281
+
282
+
283
+ class ObjectNotFoundError(ObjectError, LookupError):
284
+ """The named object does not exist."""
285
+
286
+
287
+ class ObjectExistsError(ObjectError):
288
+ """An object with that name already exists (tables and queries share one namespace)."""
289
+
290
+
291
+ class ObjectInUseError(ObjectError):
292
+ """The object is locked or open elsewhere."""
293
+
294
+
295
+ # ----------------------------------------------------------------------------------- specs & schema
296
+ class SpecError(PyAccessKitError, ValueError):
297
+ """A specification is invalid. Raised by PyAccessKit *before* anything is sent to Access.
298
+
299
+ Attributes:
300
+ problems: Individual problems (e.g. from Pydantic validation), each ``"location: message"``.
301
+ """
302
+
303
+ def __init__(
304
+ self, message: str, *, problems: tuple[str, ...] = (), operation: str | None = None
305
+ ) -> None:
306
+ super().__init__(message, operation=operation)
307
+ self.problems = problems
308
+
309
+ def __str__(self) -> str:
310
+ if not self.problems:
311
+ return self.message
312
+ return self.message + "".join(f"\n - {problem}" for problem in self.problems)
313
+
314
+
315
+ class SchemaError(PyAccessKitError):
316
+ """The database engine rejected a schema change."""
317
+
318
+
319
+ class RelationshipError(SchemaError):
320
+ """A relationship cannot be created as specified (types, keys, index budget...)."""
321
+
322
+
323
+ class IntegrityViolationError(SchemaError):
324
+ """Existing data violates the referential integrity rule being created."""
325
+
326
+
327
+ # -------------------------------------------------------------------------------------------- queries
328
+ class QueryError(PyAccessKitError):
329
+ """A SQL statement or saved query failed.
330
+
331
+ Attributes:
332
+ sql: The SQL text involved, when known.
333
+ """
334
+
335
+ def __init__(
336
+ self,
337
+ message: str,
338
+ *,
339
+ sql: str | None = None,
340
+ operation: str | None = None,
341
+ details: ErrorDetails | None = None,
342
+ ) -> None:
343
+ super().__init__(message, operation=operation, details=details)
344
+ self.sql = sql
345
+
346
+
347
+ class SqlSyntaxError(QueryError):
348
+ """Access SQL could not parse the statement."""
349
+
350
+
351
+ class MissingParameterError(QueryError):
352
+ """The statement has parameters that were not supplied (DAO error 3061)."""
353
+
354
+
355
+ # ---------------------------------------------------------------------------------- Access application
356
+ class AccessApplicationError(PyAccessKitError):
357
+ """A problem with the Microsoft Access application process."""
358
+
359
+
360
+ class AccessDialogError(AccessApplicationError):
361
+ """Access showed a modal dialog that would have blocked automation.
362
+
363
+ Attributes:
364
+ dialogs: The dialogs that were observed (title, text, buttons, how they were handled).
365
+ """
366
+
367
+ def __init__(
368
+ self,
369
+ message: str,
370
+ *,
371
+ dialogs: tuple[DialogInfo, ...] = (),
372
+ operation: str | None = None,
373
+ details: ErrorDetails | None = None,
374
+ ) -> None:
375
+ super().__init__(message, operation=operation, details=details)
376
+ self.dialogs = dialogs
377
+
378
+ def __str__(self) -> str:
379
+ return self.message + "".join(f"\n - {dialog.summary()}" for dialog in self.dialogs)
380
+
381
+
382
+ class AccessTimeoutError(AccessApplicationError):
383
+ """An Access call exceeded ``call_timeout``; the owned Access process was terminated."""
384
+
385
+
386
+ class AccessProcessDiedError(AccessApplicationError):
387
+ """The Access process exited or was terminated while PyAccessKit was using it."""
388
+
389
+
390
+ # ------------------------------------------------------------------------------------------ cleanup
391
+ class CleanupError(PyAccessKitError):
392
+ """One or more steps failed while closing a session (all remaining steps still ran).
393
+
394
+ Attributes:
395
+ errors: The exceptions raised by the failing cleanup steps.
396
+ """
397
+
398
+ def __init__(self, message: str, *, errors: tuple[BaseException, ...] = ()) -> None:
399
+ super().__init__(message)
400
+ self.errors = errors
401
+
402
+ def __str__(self) -> str:
403
+ return self.message + "".join(f"\n - {type(e).__name__}: {e}" for e in self.errors)
404
+
405
+
406
+ class ComError(PyAccessKitError):
407
+ """A COM error PyAccessKit has no more specific translation for (details are preserved)."""
@@ -0,0 +1,45 @@
1
+ """Form specifications, layout and the fluent :class:`FormBuilder` (pure Python; no Access needed)."""
2
+
3
+ from pyaccesskit.forms.builder import FormBuilder
4
+ from pyaccesskit.forms.controls import (
5
+ ButtonSpec,
6
+ CheckBoxSpec,
7
+ ComboBoxSpec,
8
+ ControlBase,
9
+ ControlSpec,
10
+ LabelSpec,
11
+ TextBoxSpec,
12
+ )
13
+ from pyaccesskit.forms.layout import (
14
+ DEFAULT_METRICS,
15
+ LayoutMetrics,
16
+ Rect,
17
+ ResolvedControl,
18
+ ResolvedForm,
19
+ ResolvedLabel,
20
+ layout_form,
21
+ )
22
+ from pyaccesskit.forms.spec import FormSpec, label_name_for
23
+ from pyaccesskit.forms.vba import EventBinding, Vba
24
+
25
+ __all__ = [
26
+ "DEFAULT_METRICS",
27
+ "ButtonSpec",
28
+ "CheckBoxSpec",
29
+ "ComboBoxSpec",
30
+ "ControlBase",
31
+ "ControlSpec",
32
+ "EventBinding",
33
+ "FormBuilder",
34
+ "FormSpec",
35
+ "LabelSpec",
36
+ "LayoutMetrics",
37
+ "Rect",
38
+ "ResolvedControl",
39
+ "ResolvedForm",
40
+ "ResolvedLabel",
41
+ "TextBoxSpec",
42
+ "Vba",
43
+ "label_name_for",
44
+ "layout_form",
45
+ ]