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
@@ -0,0 +1,390 @@
1
+ """Mapping between column specs and DAO field definitions (pure Python; no COM).
2
+
3
+ The DAO adapter reads each field into a plain :class:`FieldRead` and turns specs into a :class:`FieldPlan`,
4
+ so the whole mapping is unit-testable. Type codes and attribute flags are the ACEDAO type-library values
5
+ (see ``_com/constants.py`` and ADR 0001).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from decimal import Decimal
12
+ from typing import Any
13
+
14
+ from pyaccesskit.enums import NumberSize, PropertyType
15
+ from pyaccesskit.schema import (
16
+ AutoNumberColumn,
17
+ ColumnBase,
18
+ CurrencyColumn,
19
+ DateTimeColumn,
20
+ DecimalColumn,
21
+ Expr,
22
+ HyperlinkColumn,
23
+ LongTextColumn,
24
+ NumberColumn,
25
+ OleObjectColumn,
26
+ TextColumn,
27
+ YesNoColumn,
28
+ parse_literal,
29
+ render_literal,
30
+ )
31
+ from pyaccesskit.schema.columns import UnsupportedColumn
32
+
33
+ __all__ = [
34
+ "DAO_PROPERTY_TYPES",
35
+ "FieldPlan",
36
+ "FieldRead",
37
+ "column_from_field",
38
+ "plan_field",
39
+ "property_type_for",
40
+ ]
41
+
42
+ # DataTypeEnum
43
+ DB_BOOLEAN, DB_BYTE, DB_INTEGER, DB_LONG, DB_CURRENCY, DB_SINGLE, DB_DOUBLE, DB_DATE = (
44
+ 1,
45
+ 2,
46
+ 3,
47
+ 4,
48
+ 5,
49
+ 6,
50
+ 7,
51
+ 8,
52
+ )
53
+ DB_BINARY, DB_TEXT, DB_LONGBINARY, DB_MEMO, DB_GUID, DB_BIGINT, DB_DECIMAL = (
54
+ 9,
55
+ 10,
56
+ 11,
57
+ 12,
58
+ 15,
59
+ 16,
60
+ 20,
61
+ )
62
+ DB_DATETIME_EXTENDED, DB_ATTACHMENT = 26, 101
63
+ # FieldAttributeEnum
64
+ DB_FIXED_FIELD, DB_VARIABLE_FIELD, DB_AUTOINCR_FIELD, DB_HYPERLINK_FIELD = 1, 2, 16, 32768
65
+ DB_DESCENDING = 1
66
+
67
+ REPLICATION_ID_DEFAULT = "GenGUID()"
68
+ AC_CHECKBOX = 106
69
+ DECIMAL_PLACES_AUTO = 255
70
+
71
+ _NUMBER_TYPES = {
72
+ NumberSize.BYTE: DB_BYTE,
73
+ NumberSize.INTEGER: DB_INTEGER,
74
+ NumberSize.LONG_INTEGER: DB_LONG,
75
+ NumberSize.SINGLE: DB_SINGLE,
76
+ NumberSize.DOUBLE: DB_DOUBLE,
77
+ NumberSize.REPLICATION_ID: DB_GUID,
78
+ }
79
+ _SIZE_OF = {code: size for size, code in _NUMBER_TYPES.items()}
80
+
81
+ DAO_PROPERTY_TYPES: dict[PropertyType, int] = {
82
+ PropertyType.BOOLEAN: DB_BOOLEAN,
83
+ PropertyType.BYTE: DB_BYTE,
84
+ PropertyType.INTEGER: DB_INTEGER,
85
+ PropertyType.LONG: DB_LONG,
86
+ PropertyType.CURRENCY: DB_CURRENCY,
87
+ PropertyType.SINGLE: DB_SINGLE,
88
+ PropertyType.DOUBLE: DB_DOUBLE,
89
+ PropertyType.DATE_TIME: DB_DATE,
90
+ PropertyType.TEXT: DB_TEXT,
91
+ PropertyType.MEMO: DB_MEMO,
92
+ }
93
+
94
+ # Access-defined properties PyAccessKit writes, with the DAO type Access itself uses for them.
95
+ KNOWN_PROPERTY_TYPES: dict[str, int] = {
96
+ "description": DB_TEXT,
97
+ "caption": DB_TEXT,
98
+ "format": DB_TEXT,
99
+ "inputmask": DB_TEXT,
100
+ "decimalplaces": DB_BYTE,
101
+ "unicodecompression": DB_BOOLEAN,
102
+ "textformat": DB_BYTE,
103
+ "displaycontrol": DB_INTEGER,
104
+ "usemdimode": DB_BYTE,
105
+ "webdesignmode": DB_BYTE,
106
+ "showdocumenttabs": DB_BOOLEAN,
107
+ "appicon": DB_TEXT,
108
+ "apptitle": DB_TEXT,
109
+ "startupform": DB_TEXT,
110
+ }
111
+
112
+
113
+ def property_type_for(name: str, value: Any, explicit: PropertyType | None = None) -> int:
114
+ """The DAO type to create a property with: explicit, known Access property, or inferred."""
115
+ if explicit is not None:
116
+ return DAO_PROPERTY_TYPES[explicit]
117
+ known = KNOWN_PROPERTY_TYPES.get(name.casefold())
118
+ if known is not None:
119
+ if known == DB_TEXT and isinstance(value, str) and len(value) > 255:
120
+ return DB_MEMO
121
+ return known
122
+ if isinstance(value, bool):
123
+ return DB_BOOLEAN
124
+ if isinstance(value, int):
125
+ return DB_LONG if -(2**31) <= value < 2**31 else DB_DOUBLE
126
+ if isinstance(value, float):
127
+ return DB_DOUBLE
128
+ if isinstance(value, Decimal):
129
+ return DB_CURRENCY
130
+ if isinstance(value, str) and len(value) > 255:
131
+ return DB_MEMO
132
+ if value is not None and hasattr(value, "year"):
133
+ return DB_DATE
134
+ return DB_TEXT
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class FieldPlan:
139
+ """How to create one column with DAO (``ado_decimal`` columns are created with ADO DDL instead)."""
140
+
141
+ dao_type: int
142
+ size: int | None
143
+ attributes: int
144
+ default: str | None
145
+ allow_zero_length: bool | None
146
+ append_only: bool
147
+ ado_decimal: bool = False
148
+ post_properties: tuple[tuple[str, int, Any], ...] = ()
149
+
150
+
151
+ def _post_properties(column: ColumnBase) -> list[tuple[str, int, Any]]:
152
+ props: list[tuple[str, int, Any]] = []
153
+ for name, value in (
154
+ ("Description", column.description),
155
+ ("Caption", column.caption),
156
+ ("Format", column.format),
157
+ ("InputMask", getattr(column, "input_mask", None)),
158
+ ):
159
+ if value is not None:
160
+ props.append((name, property_type_for(name, value), value))
161
+ decimal_places = getattr(column, "decimal_places", None)
162
+ if decimal_places is not None:
163
+ props.append(("DecimalPlaces", DB_BYTE, decimal_places))
164
+ if isinstance(column, (TextColumn, LongTextColumn, HyperlinkColumn)):
165
+ props.append(("UnicodeCompression", DB_BOOLEAN, column.unicode_compression))
166
+ if isinstance(column, LongTextColumn) and column.rich_text:
167
+ props.append(("TextFormat", DB_BYTE, 1))
168
+ if isinstance(column, YesNoColumn):
169
+ props.append(("DisplayControl", DB_INTEGER, AC_CHECKBOX))
170
+ for name, value in column.properties.items():
171
+ props.append((name, property_type_for(name, value), value))
172
+ return props
173
+
174
+
175
+ def plan_field(column: ColumnBase) -> FieldPlan:
176
+ """Plan the DAO definition of ``column``.
177
+
178
+ Raises:
179
+ ValueError: For columns that cannot be created (introspection-only types).
180
+ """
181
+ default = render_literal(column.default) if column.default is not None else None
182
+ allow_zero: bool | None = getattr(column, "allow_zero_length", None)
183
+ post = tuple(_post_properties(column))
184
+ if isinstance(column, TextColumn):
185
+ return FieldPlan(
186
+ DB_TEXT,
187
+ column.length,
188
+ DB_VARIABLE_FIELD,
189
+ default,
190
+ allow_zero,
191
+ False,
192
+ post_properties=post,
193
+ )
194
+ if isinstance(column, LongTextColumn):
195
+ return FieldPlan(
196
+ DB_MEMO,
197
+ None,
198
+ DB_VARIABLE_FIELD,
199
+ default,
200
+ allow_zero,
201
+ column.append_only,
202
+ post_properties=post,
203
+ )
204
+ if isinstance(column, HyperlinkColumn):
205
+ attributes = DB_VARIABLE_FIELD | DB_HYPERLINK_FIELD
206
+ return FieldPlan(
207
+ DB_MEMO, None, attributes, default, allow_zero, False, post_properties=post
208
+ )
209
+ if isinstance(column, NumberColumn):
210
+ return FieldPlan(
211
+ _NUMBER_TYPES[column.size],
212
+ None,
213
+ DB_FIXED_FIELD,
214
+ default,
215
+ None,
216
+ False,
217
+ post_properties=post,
218
+ )
219
+ if isinstance(column, DecimalColumn):
220
+ return FieldPlan(
221
+ DB_DECIMAL,
222
+ None,
223
+ DB_FIXED_FIELD,
224
+ default,
225
+ None,
226
+ False,
227
+ ado_decimal=True,
228
+ post_properties=post,
229
+ )
230
+ if isinstance(column, CurrencyColumn):
231
+ return FieldPlan(
232
+ DB_CURRENCY, None, DB_FIXED_FIELD, default, None, False, post_properties=post
233
+ )
234
+ if isinstance(column, AutoNumberColumn):
235
+ if column.replication_id:
236
+ return FieldPlan(
237
+ DB_GUID,
238
+ None,
239
+ DB_FIXED_FIELD,
240
+ REPLICATION_ID_DEFAULT,
241
+ None,
242
+ False,
243
+ post_properties=post,
244
+ )
245
+ attributes = DB_FIXED_FIELD | DB_AUTOINCR_FIELD
246
+ return FieldPlan(DB_LONG, None, attributes, None, None, False, post_properties=post)
247
+ if isinstance(column, DateTimeColumn):
248
+ return FieldPlan(DB_DATE, None, DB_FIXED_FIELD, default, None, False, post_properties=post)
249
+ if isinstance(column, YesNoColumn):
250
+ return FieldPlan(
251
+ DB_BOOLEAN, None, DB_FIXED_FIELD, default, None, False, post_properties=post
252
+ )
253
+ if isinstance(column, OleObjectColumn):
254
+ return FieldPlan(
255
+ DB_LONGBINARY, None, DB_VARIABLE_FIELD, None, None, False, post_properties=post
256
+ )
257
+ raise ValueError(
258
+ f"columns of type {column.data_type.value!r} can be read but not created by PyAccessKit"
259
+ )
260
+
261
+
262
+ @dataclass(frozen=True)
263
+ class FieldRead:
264
+ """Plain data read from a DAO field (and its Access properties)."""
265
+
266
+ name: str
267
+ dao_type: int
268
+ size: int
269
+ attributes: int
270
+ required: bool
271
+ allow_zero_length: bool
272
+ default: str
273
+ validation_rule: str
274
+ validation_text: str
275
+ append_only: bool = False
276
+ expression: str = ""
277
+ properties: dict[str, Any] = field(default_factory=dict[str, Any])
278
+
279
+
280
+ def _prop(read: FieldRead, name: str) -> Any:
281
+ return read.properties.get(name.casefold())
282
+
283
+
284
+ def _text(value: Any) -> str | None:
285
+ return value if isinstance(value, str) and value != "" else None
286
+
287
+
288
+ def column_from_field(read: FieldRead) -> ColumnBase:
289
+ """Rebuild the canonical column spec of a DAO field."""
290
+ common: dict[str, Any] = {
291
+ "name": read.name,
292
+ "description": _text(_prop(read, "Description")),
293
+ "caption": _text(_prop(read, "Caption")),
294
+ "format": _text(_prop(read, "Format")),
295
+ "validation_rule": _text(read.validation_rule),
296
+ "validation_text": _text(read.validation_text) if _text(read.validation_rule) else None,
297
+ }
298
+ default_text = read.default.strip() if read.default else ""
299
+ default: Any = parse_literal(default_text) if default_text else None
300
+ input_mask = _text(_prop(read, "InputMask"))
301
+ places = _prop(read, "DecimalPlaces")
302
+ decimal_places = None if places is None or int(places) == DECIMAL_PLACES_AUTO else int(places)
303
+ unicode_value = _prop(read, "UnicodeCompression")
304
+ unicode_compression = True if unicode_value is None else bool(unicode_value)
305
+ auto = bool(read.attributes & DB_AUTOINCR_FIELD)
306
+
307
+ if read.expression:
308
+ return UnsupportedColumn(**common, dao_type=read.dao_type, detail="calculated field")
309
+ code = read.dao_type
310
+ if code == DB_TEXT:
311
+ return TextColumn(
312
+ **common,
313
+ required=read.required,
314
+ default=default,
315
+ length=read.size,
316
+ allow_zero_length=read.allow_zero_length,
317
+ unicode_compression=unicode_compression,
318
+ input_mask=input_mask,
319
+ )
320
+ if code == DB_MEMO:
321
+ if read.attributes & DB_HYPERLINK_FIELD:
322
+ return HyperlinkColumn(
323
+ **common,
324
+ required=read.required,
325
+ default=default,
326
+ allow_zero_length=read.allow_zero_length,
327
+ unicode_compression=unicode_compression,
328
+ )
329
+ text_format = _prop(read, "TextFormat")
330
+ return LongTextColumn(
331
+ **common,
332
+ required=read.required,
333
+ default=default,
334
+ rich_text=bool(text_format) and int(text_format) == 1,
335
+ append_only=read.append_only,
336
+ allow_zero_length=read.allow_zero_length,
337
+ unicode_compression=unicode_compression,
338
+ )
339
+ if code == DB_LONG and auto:
340
+ return AutoNumberColumn(**common)
341
+ if code == DB_GUID and default_text.casefold() == REPLICATION_ID_DEFAULT.casefold():
342
+ return AutoNumberColumn(**common, replication_id=True)
343
+ if code in _SIZE_OF:
344
+ size = _SIZE_OF[code]
345
+ return NumberColumn(
346
+ **common,
347
+ required=read.required,
348
+ default=default
349
+ if size is not NumberSize.REPLICATION_ID or isinstance(default, Expr)
350
+ else None,
351
+ size=size,
352
+ decimal_places=decimal_places,
353
+ input_mask=input_mask,
354
+ )
355
+ if code == DB_DECIMAL:
356
+ precision = _prop(read, "Precision")
357
+ scale = _prop(read, "Scale")
358
+ return DecimalColumn(
359
+ **common,
360
+ required=read.required,
361
+ default=default,
362
+ precision=int(precision) if precision else 18,
363
+ scale=int(scale) if scale is not None else 0,
364
+ decimal_places=decimal_places,
365
+ input_mask=input_mask,
366
+ )
367
+ if code == DB_CURRENCY:
368
+ return CurrencyColumn(
369
+ **common,
370
+ required=read.required,
371
+ default=default,
372
+ decimal_places=decimal_places,
373
+ input_mask=input_mask,
374
+ )
375
+ if code == DB_DATE:
376
+ return DateTimeColumn(
377
+ **common, required=read.required, default=default, input_mask=input_mask
378
+ )
379
+ if code == DB_BOOLEAN:
380
+ return YesNoColumn(**common, required=read.required, default=default)
381
+ if code == DB_LONGBINARY:
382
+ return OleObjectColumn(**common, required=read.required)
383
+ details = {
384
+ DB_BIGINT: "Large Number",
385
+ DB_DATETIME_EXTENDED: "Date/Time Extended",
386
+ DB_ATTACHMENT: "Attachment",
387
+ DB_BINARY: "Binary",
388
+ }
389
+ detail = details.get(code, "multi-valued field" if 102 <= code <= 109 else f"DAO type {code}")
390
+ return UnsupportedColumn(**common, dao_type=code, detail=detail)
@@ -0,0 +1,3 @@
1
+ from pyaccesskit._backends.fake.backend import FakeBackend
2
+
3
+ __all__ = ["FakeBackend"]