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.
- pyaccesskit/AGENT_GUIDE.md +455 -0
- pyaccesskit/__init__.py +167 -0
- pyaccesskit/__main__.py +6 -0
- pyaccesskit/_backends/__init__.py +0 -0
- pyaccesskit/_backends/access/__init__.py +1 -0
- pyaccesskit/_backends/access/design.py +415 -0
- pyaccesskit/_backends/dao/__init__.py +1 -0
- pyaccesskit/_backends/dao/profile.py +40 -0
- pyaccesskit/_backends/dao/schema.py +805 -0
- pyaccesskit/_backends/dao/typemap.py +390 -0
- pyaccesskit/_backends/fake/__init__.py +3 -0
- pyaccesskit/_backends/fake/backend.py +680 -0
- pyaccesskit/_backends/protocols.py +339 -0
- pyaccesskit/_com/__init__.py +1 -0
- pyaccesskit/_com/constants.py +394 -0
- pyaccesskit/_com/dispatch.py +50 -0
- pyaccesskit/_com/errors.py +184 -0
- pyaccesskit/_com/gateway.py +199 -0
- pyaccesskit/_com/raw.py +164 -0
- pyaccesskit/_com/runtime.py +39 -0
- pyaccesskit/_com/variants.py +72 -0
- pyaccesskit/_engines/__init__.py +48 -0
- pyaccesskit/_engines/access.py +300 -0
- pyaccesskit/_engines/inproc.py +148 -0
- pyaccesskit/_engines/probe.py +231 -0
- pyaccesskit/_ledger.py +158 -0
- pyaccesskit/_ops/__init__.py +0 -0
- pyaccesskit/_ops/design.py +127 -0
- pyaccesskit/_ops/schema.py +471 -0
- pyaccesskit/_session/__init__.py +1 -0
- pyaccesskit/_session/protocols.py +78 -0
- pyaccesskit/_session/session.py +354 -0
- pyaccesskit/_text/__init__.py +0 -0
- pyaccesskit/_text/codec.py +114 -0
- pyaccesskit/_version.py +3 -0
- pyaccesskit/_win/__init__.py +1 -0
- pyaccesskit/_win/access_process.py +348 -0
- pyaccesskit/_win/console.py +56 -0
- pyaccesskit/_win/inspector.py +53 -0
- pyaccesskit/_win/job.py +65 -0
- pyaccesskit/_win/processes.py +159 -0
- pyaccesskit/_win/watchdog.py +253 -0
- pyaccesskit/cli/__init__.py +10 -0
- pyaccesskit/cli/_output.py +101 -0
- pyaccesskit/cli/agent.py +99 -0
- pyaccesskit/cli/app.py +54 -0
- pyaccesskit/cli/cleanup.py +56 -0
- pyaccesskit/cli/doctor.py +101 -0
- pyaccesskit/cli/inspection.py +223 -0
- pyaccesskit/database.py +296 -0
- pyaccesskit/diagnostics.py +319 -0
- pyaccesskit/enums.py +258 -0
- pyaccesskit/errors.py +407 -0
- pyaccesskit/forms/__init__.py +45 -0
- pyaccesskit/forms/builder.py +295 -0
- pyaccesskit/forms/collection.py +117 -0
- pyaccesskit/forms/controls.py +157 -0
- pyaccesskit/forms/layout.py +300 -0
- pyaccesskit/forms/spec.py +169 -0
- pyaccesskit/forms/vba.py +138 -0
- pyaccesskit/maintenance.py +32 -0
- pyaccesskit/modules.py +101 -0
- pyaccesskit/objects.py +81 -0
- pyaccesskit/options.py +40 -0
- pyaccesskit/properties.py +74 -0
- pyaccesskit/py.typed +0 -0
- pyaccesskit/queries.py +190 -0
- pyaccesskit/relationships.py +143 -0
- pyaccesskit/schema/__init__.py +73 -0
- pyaccesskit/schema/_base.py +55 -0
- pyaccesskit/schema/_reserved_words.py +55 -0
- pyaccesskit/schema/columns.py +609 -0
- pyaccesskit/schema/compat.py +57 -0
- pyaccesskit/schema/expressions.py +162 -0
- pyaccesskit/schema/indexes.py +114 -0
- pyaccesskit/schema/names.py +122 -0
- pyaccesskit/schema/queries.py +192 -0
- pyaccesskit/schema/relationships.py +132 -0
- pyaccesskit/schema/tables.py +178 -0
- pyaccesskit/tables.py +333 -0
- pyaccesskit/units.py +301 -0
- pyaccesskit-0.1.0.dist-info/METADATA +201 -0
- pyaccesskit-0.1.0.dist-info/RECORD +86 -0
- pyaccesskit-0.1.0.dist-info/WHEEL +4 -0
- pyaccesskit-0.1.0.dist-info/entry_points.txt +2 -0
- pyaccesskit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""The typed seam between PyAccessKit's COM-free core and its COM adapters.
|
|
2
|
+
|
|
3
|
+
Backends exchange **specs and plain data only** — never COM objects. Operations are deliberately sized
|
|
4
|
+
like the steps of a future ``plan``/``apply`` (create table, add column, create relationship...).
|
|
5
|
+
|
|
6
|
+
Implementations:
|
|
7
|
+
|
|
8
|
+
* ``_backends.dao`` — DAO over any transport (in-process, Access-hosted, or ``CurrentDb``);
|
|
9
|
+
* ``_backends.access`` — ``Access.Application`` design features;
|
|
10
|
+
* ``_backends.fake`` — in-memory, used by the test-suite and kept honest by the contract tests.
|
|
11
|
+
|
|
12
|
+
Backends *may* assume their inputs are normalized specs whose names were validated; semantic
|
|
13
|
+
pre-validation (does the table exist? are the relationship types compatible?) happens in ``_ops`` so that
|
|
14
|
+
every backend reports problems the same way. Backends still translate engine errors into
|
|
15
|
+
:mod:`pyaccesskit.errors` exceptions.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Literal, Protocol
|
|
24
|
+
|
|
25
|
+
from pyaccesskit.enums import (
|
|
26
|
+
ControlKind,
|
|
27
|
+
DataType,
|
|
28
|
+
ObjectKind,
|
|
29
|
+
PropertyType,
|
|
30
|
+
QueryKind,
|
|
31
|
+
Section,
|
|
32
|
+
Transport,
|
|
33
|
+
)
|
|
34
|
+
from pyaccesskit.forms.layout import ResolvedForm
|
|
35
|
+
from pyaccesskit.schema import (
|
|
36
|
+
ColumnSpec,
|
|
37
|
+
IndexSpec,
|
|
38
|
+
PropertyValue,
|
|
39
|
+
QuerySpec,
|
|
40
|
+
RelationshipSpec,
|
|
41
|
+
TableSpec,
|
|
42
|
+
)
|
|
43
|
+
from pyaccesskit.units import Length
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"ControlInfo",
|
|
47
|
+
"DatabaseInfo",
|
|
48
|
+
"DesignBackend",
|
|
49
|
+
"FetchResult",
|
|
50
|
+
"ParameterInfo",
|
|
51
|
+
"PropertyTarget",
|
|
52
|
+
"QueryInfo",
|
|
53
|
+
"SchemaBackend",
|
|
54
|
+
"TableInfo",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class TableInfo:
|
|
60
|
+
"""Summary of a table (cheap to list)."""
|
|
61
|
+
|
|
62
|
+
name: str
|
|
63
|
+
is_linked: bool = False
|
|
64
|
+
is_system: bool = False
|
|
65
|
+
is_hidden: bool = False
|
|
66
|
+
connect: str | None = None
|
|
67
|
+
source_table: str | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class QueryInfo:
|
|
72
|
+
"""Summary of a saved query."""
|
|
73
|
+
|
|
74
|
+
name: str
|
|
75
|
+
kind: QueryKind
|
|
76
|
+
is_hidden: bool = False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class ParameterInfo:
|
|
81
|
+
"""A query parameter (name without brackets)."""
|
|
82
|
+
|
|
83
|
+
name: str
|
|
84
|
+
data_type: DataType
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class FetchResult:
|
|
89
|
+
"""Rows returned by a SELECT: column names plus row tuples."""
|
|
90
|
+
|
|
91
|
+
columns: tuple[str, ...]
|
|
92
|
+
rows: list[tuple[Any, ...]]
|
|
93
|
+
|
|
94
|
+
def as_dicts(self) -> list[dict[str, Any]]:
|
|
95
|
+
"""Rows as ``{column: value}`` dictionaries."""
|
|
96
|
+
return [dict(zip(self.columns, row, strict=True)) for row in self.rows]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class ControlInfo:
|
|
101
|
+
"""A control read back from a saved form."""
|
|
102
|
+
|
|
103
|
+
name: str
|
|
104
|
+
kind: ControlKind
|
|
105
|
+
section: Section | None
|
|
106
|
+
left: Length
|
|
107
|
+
top: Length
|
|
108
|
+
width: Length
|
|
109
|
+
height: Length
|
|
110
|
+
control_source: str | None = None
|
|
111
|
+
caption: str | None = None
|
|
112
|
+
parent: str | None = None
|
|
113
|
+
"""For attached labels: the control the label belongs to."""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class DatabaseInfo:
|
|
118
|
+
"""Facts about the open database."""
|
|
119
|
+
|
|
120
|
+
path: Path
|
|
121
|
+
version: str
|
|
122
|
+
transport: Transport
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class PropertyTarget:
|
|
127
|
+
"""Addresses the object whose DAO ``Properties`` collection is used."""
|
|
128
|
+
|
|
129
|
+
kind: Literal["database", "table", "field", "query"]
|
|
130
|
+
name: str | None = None
|
|
131
|
+
field: str | None = None
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def database(cls) -> PropertyTarget:
|
|
135
|
+
"""The database itself (``AppTitle``, ``StartUpForm``...)."""
|
|
136
|
+
return cls("database")
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def table(cls, name: str) -> PropertyTarget:
|
|
140
|
+
"""A table."""
|
|
141
|
+
return cls("table", name)
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def column(cls, table: str, field: str) -> PropertyTarget:
|
|
145
|
+
"""A field of a table."""
|
|
146
|
+
return cls("field", table, field)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def query(cls, name: str) -> PropertyTarget:
|
|
150
|
+
"""A saved query."""
|
|
151
|
+
return cls("query", name)
|
|
152
|
+
|
|
153
|
+
def describe(self) -> str:
|
|
154
|
+
"""Human-readable description for messages."""
|
|
155
|
+
if self.kind == "database":
|
|
156
|
+
return "the database"
|
|
157
|
+
if self.kind == "field":
|
|
158
|
+
return f"field {self.field!r} of table {self.name!r}"
|
|
159
|
+
return f"{self.kind} {self.name!r}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class SchemaBackend(Protocol):
|
|
163
|
+
"""Schema and data operations (implemented with DAO)."""
|
|
164
|
+
|
|
165
|
+
def database_info(self) -> DatabaseInfo:
|
|
166
|
+
"""Facts about the open database."""
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------------------------ tables
|
|
170
|
+
def list_tables(self) -> list[TableInfo]:
|
|
171
|
+
"""All tables, including system and linked tables."""
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
def read_table(self, name: str) -> TableSpec:
|
|
175
|
+
"""The normalized spec of a local table (relationship-owned indexes excluded)."""
|
|
176
|
+
...
|
|
177
|
+
|
|
178
|
+
def count_indexes(self, table: str) -> int:
|
|
179
|
+
"""Number of indexes on ``table`` *including* hidden relationship indexes (Access limit: 32)."""
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
def create_table(self, spec: TableSpec) -> None:
|
|
183
|
+
"""Create a table from a normalized spec, atomically (all or nothing)."""
|
|
184
|
+
...
|
|
185
|
+
|
|
186
|
+
def drop_table(self, name: str) -> None:
|
|
187
|
+
"""Delete a table."""
|
|
188
|
+
...
|
|
189
|
+
|
|
190
|
+
def rename_table(self, old: str, new: str) -> None:
|
|
191
|
+
"""Rename a table."""
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
def add_column(self, table: str, column: ColumnSpec) -> None:
|
|
195
|
+
"""Append a column (index shorthands are handled by the caller)."""
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
def drop_column(self, table: str, column: str) -> None:
|
|
199
|
+
"""Delete a column."""
|
|
200
|
+
...
|
|
201
|
+
|
|
202
|
+
def rename_column(self, table: str, old: str, new: str) -> None:
|
|
203
|
+
"""Rename a column."""
|
|
204
|
+
...
|
|
205
|
+
|
|
206
|
+
def create_index(self, table: str, index: IndexSpec) -> None:
|
|
207
|
+
"""Create an index."""
|
|
208
|
+
...
|
|
209
|
+
|
|
210
|
+
def drop_index(self, table: str, name: str) -> None:
|
|
211
|
+
"""Delete an index."""
|
|
212
|
+
...
|
|
213
|
+
|
|
214
|
+
# ----------------------------------------------------------------------------- relationships
|
|
215
|
+
def list_relationships(self) -> list[RelationshipSpec]:
|
|
216
|
+
"""All (non-inherited) relationships, normalized."""
|
|
217
|
+
...
|
|
218
|
+
|
|
219
|
+
def create_relationship(self, spec: RelationshipSpec) -> None:
|
|
220
|
+
"""Create a relationship from a normalized, pre-validated spec."""
|
|
221
|
+
...
|
|
222
|
+
|
|
223
|
+
def drop_relationship(self, name: str) -> None:
|
|
224
|
+
"""Delete a relationship."""
|
|
225
|
+
...
|
|
226
|
+
|
|
227
|
+
# ----------------------------------------------------------------------------------- queries
|
|
228
|
+
def list_queries(self) -> list[QueryInfo]:
|
|
229
|
+
"""All saved queries (including hidden ``~`` queries)."""
|
|
230
|
+
...
|
|
231
|
+
|
|
232
|
+
def read_query(self, name: str) -> QuerySpec:
|
|
233
|
+
"""The spec of a saved query (SQL as Access stored it)."""
|
|
234
|
+
...
|
|
235
|
+
|
|
236
|
+
def query_parameters(self, name: str) -> list[ParameterInfo]:
|
|
237
|
+
"""Declared and implicit parameters of a saved query."""
|
|
238
|
+
...
|
|
239
|
+
|
|
240
|
+
def create_query(self, spec: QuerySpec) -> None:
|
|
241
|
+
"""Create a saved query."""
|
|
242
|
+
...
|
|
243
|
+
|
|
244
|
+
def set_query_sql(self, name: str, sql: str) -> None:
|
|
245
|
+
"""Replace the SQL of a saved query."""
|
|
246
|
+
...
|
|
247
|
+
|
|
248
|
+
def rename_query(self, old: str, new: str) -> None:
|
|
249
|
+
"""Rename a saved query."""
|
|
250
|
+
...
|
|
251
|
+
|
|
252
|
+
def drop_query(self, name: str) -> None:
|
|
253
|
+
"""Delete a saved query."""
|
|
254
|
+
...
|
|
255
|
+
|
|
256
|
+
# -------------------------------------------------------------------------------- properties
|
|
257
|
+
def get_property(self, target: PropertyTarget, name: str) -> PropertyValue:
|
|
258
|
+
"""Read a DAO property. Raises ``ObjectNotFoundError`` (kind PROPERTY) if it does not exist."""
|
|
259
|
+
...
|
|
260
|
+
|
|
261
|
+
def set_property(
|
|
262
|
+
self,
|
|
263
|
+
target: PropertyTarget,
|
|
264
|
+
name: str,
|
|
265
|
+
value: PropertyValue,
|
|
266
|
+
type: PropertyType | None = None,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Set (creating if needed) a DAO property."""
|
|
269
|
+
...
|
|
270
|
+
|
|
271
|
+
def delete_property(self, target: PropertyTarget, name: str) -> None:
|
|
272
|
+
"""Delete a user-defined DAO property."""
|
|
273
|
+
...
|
|
274
|
+
|
|
275
|
+
def list_properties(self, target: PropertyTarget) -> dict[str, PropertyValue]:
|
|
276
|
+
"""All readable properties of the target."""
|
|
277
|
+
...
|
|
278
|
+
|
|
279
|
+
# -------------------------------------------------------------------------------------- data
|
|
280
|
+
def execute(self, sql: str, params: Mapping[str, Any] | None = None) -> int:
|
|
281
|
+
"""Run an action statement; returns the number of records affected."""
|
|
282
|
+
...
|
|
283
|
+
|
|
284
|
+
def fetch(
|
|
285
|
+
self, sql: str, params: Mapping[str, Any] | None = None, *, limit: int | None = None
|
|
286
|
+
) -> FetchResult:
|
|
287
|
+
"""Run a SELECT and return its rows."""
|
|
288
|
+
...
|
|
289
|
+
|
|
290
|
+
def execute_saved(self, name: str, params: Mapping[str, Any] | None = None) -> int:
|
|
291
|
+
"""Run a saved action query."""
|
|
292
|
+
...
|
|
293
|
+
|
|
294
|
+
def fetch_saved(
|
|
295
|
+
self, name: str, params: Mapping[str, Any] | None = None, *, limit: int | None = None
|
|
296
|
+
) -> FetchResult:
|
|
297
|
+
"""Return the rows of a saved select query."""
|
|
298
|
+
...
|
|
299
|
+
|
|
300
|
+
# --------------------------------------------------------------------------------- documents
|
|
301
|
+
def list_documents(self, kind: ObjectKind) -> list[str]:
|
|
302
|
+
"""Names of forms, reports, macros or modules (read through DAO containers; no Access needed)."""
|
|
303
|
+
...
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class DesignBackend(Protocol):
|
|
307
|
+
"""Operations that need Microsoft Access itself (``Access.Application``)."""
|
|
308
|
+
|
|
309
|
+
def list_objects(self, kind: ObjectKind) -> list[str]:
|
|
310
|
+
"""Names of the forms, reports, macros or modules in the database."""
|
|
311
|
+
...
|
|
312
|
+
|
|
313
|
+
def delete_object(self, kind: ObjectKind, name: str) -> None:
|
|
314
|
+
"""Delete a form, report, macro or module."""
|
|
315
|
+
...
|
|
316
|
+
|
|
317
|
+
def rename_object(self, kind: ObjectKind, old: str, new: str) -> None:
|
|
318
|
+
"""Rename a form, report, macro or module."""
|
|
319
|
+
...
|
|
320
|
+
|
|
321
|
+
def export_text(self, kind: ObjectKind, name: str) -> bytes:
|
|
322
|
+
"""``SaveAsText`` bytes of an object (native encoding; see ``_text.codec``)."""
|
|
323
|
+
...
|
|
324
|
+
|
|
325
|
+
def import_text(self, kind: ObjectKind, name: str, data: bytes) -> None:
|
|
326
|
+
"""``LoadFromText`` from native-encoded bytes (replaces an existing object)."""
|
|
327
|
+
...
|
|
328
|
+
|
|
329
|
+
def build_form(self, form: ResolvedForm, *, replace: bool) -> None:
|
|
330
|
+
"""Create (or atomically replace) a form from a resolved layout."""
|
|
331
|
+
...
|
|
332
|
+
|
|
333
|
+
def form_controls(self, name: str) -> list[ControlInfo]:
|
|
334
|
+
"""Read the controls of a saved form."""
|
|
335
|
+
...
|
|
336
|
+
|
|
337
|
+
def check_form_opens(self, name: str) -> None:
|
|
338
|
+
"""Open the form in Form view (hidden) and close it again; raises if Access reports an error."""
|
|
339
|
+
...
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""COM boundary (pywin32). Imported lazily; never imported by ``import pyaccesskit``."""
|