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,319 @@
1
+ """Environment diagnostics: which engines PyAccessKit can use on this machine, and why not.
2
+
3
+ ``pyaccesskit doctor`` prints this report; :func:`diagnose` returns it as data (:meth:`Diagnosis.to_dict`
4
+ gives a JSON-ready form). Without ``probe=True`` nothing is started: the report comes from the registry, file
5
+ headers and loading DAO in-process. With ``probe=True`` a scratch database is built with each usable engine,
6
+ in an Access process owned (and closed) by PyAccessKit.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import dataclasses
12
+ import importlib.metadata
13
+ import platform
14
+ import struct
15
+ import sys
16
+ import tempfile
17
+ import time
18
+ import warnings
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import Any, Literal
22
+
23
+ from pyaccesskit._version import __version__
24
+ from pyaccesskit.errors import AccessNameWarning, PyAccessKitError
25
+
26
+ __all__ = [
27
+ "AccessInstallation",
28
+ "Diagnosis",
29
+ "EngineCheck",
30
+ "OwnedProcessReport",
31
+ "ProbeReport",
32
+ "diagnose",
33
+ ]
34
+
35
+ EngineName = Literal["dao", "access"]
36
+
37
+ _INSTALL_ADVICE = (
38
+ "Install Microsoft Access (Microsoft 365, 2016 or later) for full functionality, or the Microsoft 365 "
39
+ "Access Runtime with a Python of the same bitness for DAO-only work."
40
+ )
41
+ _AC_SYSCMD_RUNTIME = 6
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class EngineCheck:
46
+ """Whether one engine can be used from this Python, with an explanation."""
47
+
48
+ engine: EngineName
49
+ available: bool
50
+ detail: str
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class AccessInstallation:
55
+ """Microsoft Access as registered for automation."""
56
+
57
+ progid: str
58
+ registered_version: str | None
59
+ executable: str | None
60
+ version: str | None
61
+ bits: int | None
62
+ click_to_run: bool
63
+ products: tuple[str, ...] = ()
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class OwnedProcessReport:
68
+ """An Access process recorded in the ownership ledger, and what ``cleanup`` would do with it."""
69
+
70
+ pid: int
71
+ owner_pid: int
72
+ database: str | None
73
+ status: str
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class ProbeReport:
78
+ """The outcome of building a scratch database with one engine."""
79
+
80
+ engine: EngineName
81
+ ok: bool
82
+ seconds: float
83
+ detail: str
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class Diagnosis:
88
+ """Everything ``pyaccesskit doctor`` reports.
89
+
90
+ Attributes:
91
+ auto_engine: What ``engine="auto"`` selects here (``None``: nothing is usable).
92
+ engines: Availability of in-process DAO and of Microsoft Access.
93
+ ace_oledb: ACE OLEDB providers loadable in-process (used for Decimal columns with in-process DAO).
94
+ owned_processes: Ledger entries of Access processes started by PyAccessKit.
95
+ probes: Results of ``probe=True`` runs.
96
+ problems: Reasons PyAccessKit cannot work (or cannot work fully).
97
+ notes: Useful facts and advice that are not problems.
98
+ """
99
+
100
+ pyaccesskit_version: str
101
+ python_version: str
102
+ python_bits: int
103
+ python_executable: str
104
+ operating_system: str
105
+ pywin32_version: str | None
106
+ access: AccessInstallation | None
107
+ engines: tuple[EngineCheck, ...]
108
+ ace_oledb: tuple[str, ...]
109
+ auto_engine: EngineName | None
110
+ owned_processes: tuple[OwnedProcessReport, ...]
111
+ probes: tuple[ProbeReport, ...]
112
+ problems: tuple[str, ...]
113
+ notes: tuple[str, ...]
114
+
115
+ @property
116
+ def usable(self) -> bool:
117
+ """At least one engine is available and every probe that ran succeeded."""
118
+ return self.auto_engine is not None and all(probe.ok for probe in self.probes)
119
+
120
+ def to_dict(self) -> dict[str, Any]:
121
+ """A JSON-ready dictionary (includes ``usable``)."""
122
+ data = dataclasses.asdict(self)
123
+ data["usable"] = self.usable
124
+ return data
125
+
126
+
127
+ def _package_version(name: str) -> str | None:
128
+ try:
129
+ return importlib.metadata.version(name)
130
+ except importlib.metadata.PackageNotFoundError:
131
+ return None
132
+
133
+
134
+ def diagnose(*, probe: bool = False, progid: str = "Access.Application") -> Diagnosis:
135
+ """Inspect this machine and report which PyAccessKit engines work.
136
+
137
+ Args:
138
+ probe: Also build a scratch database with every available engine (starts and closes an owned
139
+ Access process; takes a few seconds).
140
+ progid: The Access ProgID to check (e.g. ``"Access.Application.16"`` on multi-version machines).
141
+ """
142
+ bits = struct.calcsize("P") * 8
143
+ base: dict[str, Any] = {
144
+ "pyaccesskit_version": __version__,
145
+ "python_version": platform.python_version(),
146
+ "python_bits": bits,
147
+ "python_executable": sys.executable,
148
+ "operating_system": platform.platform(),
149
+ "pywin32_version": _package_version("pywin32"),
150
+ }
151
+ problems: list[str] = []
152
+ notes: list[str] = []
153
+ empty: dict[str, Any] = {
154
+ "access": None,
155
+ "engines": (),
156
+ "ace_oledb": (),
157
+ "auto_engine": None,
158
+ "owned_processes": (),
159
+ "probes": (),
160
+ }
161
+
162
+ if sys.platform != "win32":
163
+ problems.append(
164
+ "Microsoft Access automation needs Windows; here PyAccessKit can only build and validate specs."
165
+ )
166
+ return Diagnosis(**base, **empty, problems=tuple(problems), notes=())
167
+ gil_check = getattr(sys, "_is_gil_enabled", None)
168
+ if gil_check is not None and not gil_check():
169
+ problems.append(
170
+ "free-threaded Python builds are not supported (COM apartments need the GIL)"
171
+ )
172
+ try:
173
+ from pyaccesskit._engines import probe as facts
174
+ except ImportError as exc:
175
+ problems.append(f"pywin32 is not usable ({exc}); install it with 'pip install pywin32'")
176
+ return Diagnosis(**base, **empty, problems=tuple(problems), notes=())
177
+
178
+ access_facts = facts.access_facts(progid)
179
+ c2r = facts.click_to_run()
180
+ access = (
181
+ AccessInstallation(
182
+ progid=progid,
183
+ registered_version=access_facts.current_version,
184
+ executable=access_facts.executable,
185
+ version=access_facts.version,
186
+ bits=access_facts.bits,
187
+ click_to_run=c2r is not None,
188
+ products=c2r.products if c2r else (),
189
+ )
190
+ if access_facts.registered
191
+ else None
192
+ )
193
+ dao = facts.inproc_dao()
194
+ dao_check = EngineCheck(
195
+ "dao",
196
+ dao.available,
197
+ f"DAO.DBEngine.120 loads in this {bits}-bit Python" if dao.available else dao.reason,
198
+ )
199
+ if access is None:
200
+ access_detail = f"{progid} is not registered"
201
+ elif access.executable is None:
202
+ access_detail = f"{progid} is registered but its executable was not found"
203
+ else:
204
+ access_detail = f"Access {access.version or '(unknown version)'}, {access.bits or '?'}-bit"
205
+ access_check = EngineCheck("access", access is not None, access_detail)
206
+ ace = tuple(name for name in facts.ACE_OLEDB_PROGIDS if facts.inproc_registration(name)[bits])
207
+
208
+ auto: EngineName | None = "dao" if dao.available else ("access" if access else None)
209
+ if dao.available and access:
210
+ notes.append(
211
+ "engine='auto' uses in-process DAO for schema work and switches to Microsoft Access for forms, "
212
+ "modules and text import/export."
213
+ )
214
+ elif dao.available:
215
+ notes.append(
216
+ "Forms, modules and text import/export need Microsoft Access (not the Runtime)."
217
+ )
218
+ if not ace:
219
+ notes.append(
220
+ "No ACE OLEDB provider is registered for this Python's bitness: Decimal columns cannot be "
221
+ "created in-process."
222
+ )
223
+ elif access:
224
+ other = access.bits if access.bits and access.bits != bits else None
225
+ advice = f" Install a {other}-bit Python to use DAO in-process (faster)." if other else ""
226
+ notes.append(
227
+ "engine='auto' reaches DAO through Microsoft Access, so every session starts an owned, hidden "
228
+ f"MSACCESS.EXE.{advice}"
229
+ )
230
+ else:
231
+ problems.append(f"Neither in-process DAO nor Microsoft Access is usable. {_INSTALL_ADVICE}")
232
+ if access and access.version and not access.version.startswith("16."):
233
+ notes.append(
234
+ f"Access {access.version} is older than the supported floor (16.0, Access 2016); it may work."
235
+ )
236
+
237
+ owned = _owned_processes()
238
+ orphans = sum(1 for entry in owned if entry.status == "would-terminate")
239
+ if orphans:
240
+ notes.append(
241
+ f"{orphans} orphaned Access process(es) from crashed PyAccessKit sessions are still running; "
242
+ "run 'pyaccesskit cleanup' to end them."
243
+ )
244
+
245
+ probes: list[ProbeReport] = []
246
+ if probe:
247
+ if dao.available:
248
+ probes.append(_probe("dao"))
249
+ if access:
250
+ probes.append(_probe("access"))
251
+ problems.extend(f"{p.engine} probe failed: {p.detail}" for p in probes if not p.ok)
252
+
253
+ return Diagnosis(
254
+ **base,
255
+ access=access,
256
+ engines=(dao_check, access_check),
257
+ ace_oledb=ace,
258
+ auto_engine=auto,
259
+ owned_processes=owned,
260
+ probes=tuple(probes),
261
+ problems=tuple(problems),
262
+ notes=tuple(notes),
263
+ )
264
+
265
+
266
+ def _owned_processes() -> tuple[OwnedProcessReport, ...]:
267
+ from pyaccesskit.maintenance import reap_orphans
268
+
269
+ return tuple(
270
+ OwnedProcessReport(
271
+ pid=result.entry.pid,
272
+ owner_pid=result.entry.owner_pid,
273
+ database=result.entry.database,
274
+ status=result.action,
275
+ )
276
+ for result in reap_orphans(dry_run=True)
277
+ )
278
+
279
+
280
+ def _probe(engine: EngineName) -> ProbeReport:
281
+ """Build a scratch database end to end with ``engine``."""
282
+ from pyaccesskit.database import AccessDatabase
283
+ from pyaccesskit.schema import Column
284
+
285
+ start = time.perf_counter()
286
+ detail = "created a table, wrote and read a row"
287
+ try:
288
+ with (
289
+ warnings.catch_warnings(),
290
+ tempfile.TemporaryDirectory(
291
+ prefix="pyaccesskit-probe-", ignore_cleanup_errors=True
292
+ ) as folder,
293
+ AccessDatabase.create(Path(folder) / "probe.accdb", engine=engine) as db,
294
+ ):
295
+ warnings.simplefilter("ignore", AccessNameWarning)
296
+ db.tables.create(
297
+ "PakProbe",
298
+ columns=[
299
+ Column.autonumber("ProbeID", primary_key=True),
300
+ Column.text("ProbeNote", length=20),
301
+ ],
302
+ )
303
+ db.execute("INSERT INTO PakProbe (ProbeNote) VALUES ([note])", {"note": "ok"})
304
+ if db.fetch_all("SELECT ProbeNote FROM PakProbe") != [{"ProbeNote": "ok"}]:
305
+ raise PyAccessKitError("the probe row did not read back correctly")
306
+ if engine == "access":
307
+ db.modules.create(
308
+ "modPakProbe", "Public Function Ping() As Long\n Ping = 1\nEnd Function\n"
309
+ )
310
+ app = db.raw.access
311
+ runtime = bool(app.SysCmd(_AC_SYSCMD_RUNTIME))
312
+ detail = (
313
+ f"Access {app.Version} (build {app.Build}){' Runtime' if runtime else ''}: "
314
+ "created a table and a VBA module, wrote and read a row"
315
+ )
316
+ except Exception as exc:
317
+ message = str(exc) if isinstance(exc, PyAccessKitError) else f"{type(exc).__name__}: {exc}"
318
+ return ProbeReport(engine, False, round(time.perf_counter() - start, 2), message)
319
+ return ProbeReport(engine, True, round(time.perf_counter() - start, 2), detail)
pyaccesskit/enums.py ADDED
@@ -0,0 +1,258 @@
1
+ """Semantic enumerations used throughout the public API.
2
+
3
+ These enums describe Access concepts in Python terms. They are deliberately *not* the raw COM constants
4
+ (``acTextBox = 109`` etc.); the COM adapters translate between the two, so no magic numbers ever appear in
5
+ user code. Every enum is a :class:`~enum.StrEnum`, which keeps specs readable when serialized to JSON/YAML.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import StrEnum
11
+
12
+ __all__ = [
13
+ "ControlKind",
14
+ "DataType",
15
+ "DialogPolicy",
16
+ "Engine",
17
+ "FormView",
18
+ "JoinType",
19
+ "LayoutKind",
20
+ "MacroSecurity",
21
+ "ModuleKind",
22
+ "NumberSize",
23
+ "ObjectKind",
24
+ "PropertyType",
25
+ "QueryKind",
26
+ "RowSourceType",
27
+ "ScrollBars",
28
+ "Section",
29
+ "Transport",
30
+ ]
31
+
32
+
33
+ class Engine(StrEnum):
34
+ """Which automation engine a session may use (see ``AccessDatabase.open(engine=...)``)."""
35
+
36
+ AUTO = "auto"
37
+ """In-process DAO when available, otherwise Access; upgrades to Access for design features."""
38
+ DAO = "dao"
39
+ """In-process DAO only. Never starts ``MSACCESS.EXE``; design features raise ``CapabilityError``."""
40
+ ACCESS = "access"
41
+ """Microsoft Access automation (works with any Python/Office bitness combination)."""
42
+
43
+
44
+ class Transport(StrEnum):
45
+ """How a session currently reaches the database (reported by ``AccessDatabase.transport``)."""
46
+
47
+ DAO_INPROC = "dao-inproc"
48
+ """DAO loaded into the Python process."""
49
+ ACCESS_HOSTED = "access-hosted"
50
+ """DAO hosted by a hidden Access instance; the database is not opened in the Access UI."""
51
+ ACCESS_DESIGN = "access-design"
52
+ """The database is Access's current database; forms, reports and modules are available."""
53
+ MEMORY = "memory"
54
+ """In-memory fake used for tests and dry runs."""
55
+
56
+
57
+ class DataType(StrEnum):
58
+ """Access field data types, named after the Access table designer."""
59
+
60
+ TEXT = "text"
61
+ """Short Text (up to 255 characters)."""
62
+ LONG_TEXT = "long_text"
63
+ """Long Text (formerly Memo)."""
64
+ NUMBER = "number"
65
+ """Number; the storage size is given by :class:`NumberSize`."""
66
+ DECIMAL = "decimal"
67
+ """Number with Field Size = Decimal (fixed precision and scale)."""
68
+ CURRENCY = "currency"
69
+ AUTONUMBER = "autonumber"
70
+ DATE_TIME = "date_time"
71
+ YES_NO = "yes_no"
72
+ HYPERLINK = "hyperlink"
73
+ OLE_OBJECT = "ole_object"
74
+ ATTACHMENT = "attachment"
75
+ CALCULATED = "calculated"
76
+ LARGE_NUMBER = "large_number"
77
+ DATE_TIME_EXTENDED = "date_time_extended"
78
+ BINARY = "binary"
79
+ """Fixed/variable binary (not creatable from the Access UI)."""
80
+ MULTI_VALUE = "multi_value"
81
+ """A multi-valued (complex) lookup field."""
82
+ UNKNOWN = "unknown"
83
+ """A type PyAccessKit does not model yet; the raw DAO type is preserved on introspection."""
84
+
85
+
86
+ class NumberSize(StrEnum):
87
+ """The *Field Size* of a Number field, exactly as offered by the Access table designer.
88
+
89
+ Note that Access's ``INTEGER`` is a **16-bit** integer; the default, ``LONG_INTEGER``, is 32-bit.
90
+ """
91
+
92
+ BYTE = "byte"
93
+ INTEGER = "integer"
94
+ LONG_INTEGER = "long_integer"
95
+ SINGLE = "single"
96
+ DOUBLE = "double"
97
+ REPLICATION_ID = "replication_id"
98
+
99
+
100
+ class JoinType(StrEnum):
101
+ """Default join type of a relationship (as shown in the Relationships window)."""
102
+
103
+ INNER = "inner"
104
+ LEFT = "left"
105
+ """Include all records from the primary table."""
106
+ RIGHT = "right"
107
+ """Include all records from the foreign (related) table."""
108
+
109
+
110
+ class QueryKind(StrEnum):
111
+ """Kind of a saved query (derived from DAO ``QueryDef.Type``)."""
112
+
113
+ SELECT = "select"
114
+ CROSSTAB = "crosstab"
115
+ DELETE = "delete"
116
+ UPDATE = "update"
117
+ APPEND = "append"
118
+ MAKE_TABLE = "make_table"
119
+ DDL = "ddl"
120
+ PASS_THROUGH = "pass_through"
121
+ UNION = "union"
122
+ PASS_THROUGH_BULK = "pass_through_bulk"
123
+ COMPOUND = "compound"
124
+ PROCEDURE = "procedure"
125
+ ACTION = "action"
126
+ UNKNOWN = "unknown"
127
+
128
+
129
+ class ObjectKind(StrEnum):
130
+ """Kinds of objects stored in an Access database."""
131
+
132
+ TABLE = "table"
133
+ QUERY = "query"
134
+ FORM = "form"
135
+ REPORT = "report"
136
+ MACRO = "macro"
137
+ MODULE = "module"
138
+ # Sub-objects used in error messages.
139
+ FIELD = "field"
140
+ INDEX = "index"
141
+ RELATIONSHIP = "relationship"
142
+ PROPERTY = "property"
143
+ CONTROL = "control"
144
+ DATABASE = "database"
145
+
146
+
147
+ class ModuleKind(StrEnum):
148
+ """VBA module kinds."""
149
+
150
+ STANDARD = "standard"
151
+ CLASS = "class"
152
+
153
+
154
+ class FormView(StrEnum):
155
+ """A form's *Default View*."""
156
+
157
+ SINGLE = "single"
158
+ CONTINUOUS = "continuous"
159
+ DATASHEET = "datasheet"
160
+ SPLIT = "split"
161
+
162
+
163
+ class ScrollBars(StrEnum):
164
+ """A form's *Scroll Bars* setting."""
165
+
166
+ NEITHER = "neither"
167
+ HORIZONTAL = "horizontal"
168
+ VERTICAL = "vertical"
169
+ BOTH = "both"
170
+
171
+
172
+ class Section(StrEnum):
173
+ """Form sections a control can be placed in."""
174
+
175
+ DETAIL = "detail"
176
+ HEADER = "header"
177
+ FOOTER = "footer"
178
+
179
+
180
+ class LayoutKind(StrEnum):
181
+ """How a form's controls are arranged when they have no explicit position."""
182
+
183
+ AUTO = "auto"
184
+ """``STACKED`` for single/split forms, ``TABULAR`` for continuous and datasheet forms."""
185
+ STACKED = "stacked"
186
+ """One control per row with its label on the left (like Access's stacked layout)."""
187
+ TABULAR = "tabular"
188
+ """Labels in the form header and one row of controls in the detail section."""
189
+ NONE = "none"
190
+ """No automatic layout: every control must be positioned with ``at=``."""
191
+
192
+
193
+ class RowSourceType(StrEnum):
194
+ """Where a combo box gets its rows from."""
195
+
196
+ TABLE_QUERY = "table_query"
197
+ """A table name, query name or SQL statement."""
198
+ VALUE_LIST = "value_list"
199
+ """A semicolon-separated list of values."""
200
+
201
+
202
+ class ControlKind(StrEnum):
203
+ """Kinds of form controls (for building and introspection)."""
204
+
205
+ LABEL = "label"
206
+ TEXTBOX = "textbox"
207
+ CHECKBOX = "checkbox"
208
+ COMBOBOX = "combobox"
209
+ LISTBOX = "listbox"
210
+ BUTTON = "button"
211
+ OPTION_GROUP = "option_group"
212
+ OPTION_BUTTON = "option_button"
213
+ TOGGLE_BUTTON = "toggle_button"
214
+ SUBFORM = "subform"
215
+ IMAGE = "image"
216
+ LINE = "line"
217
+ RECTANGLE = "rectangle"
218
+ TAB_CONTROL = "tab_control"
219
+ PAGE = "page"
220
+ ATTACHMENT = "attachment"
221
+ OTHER = "other"
222
+
223
+
224
+ class MacroSecurity(StrEnum):
225
+ """What Access may run when PyAccessKit opens a database as the current database."""
226
+
227
+ DISABLE = "disable"
228
+ """Disable all macros and VBA (``msoAutomationSecurityForceDisable``). Default; deterministic."""
229
+ USE_UI = "ui"
230
+ """Follow the user's Trust Center settings (``msoAutomationSecurityByUI``)."""
231
+ ENABLE = "enable"
232
+ """Enable all macros and VBA, including AutoExec (``msoAutomationSecurityLow``)."""
233
+
234
+
235
+ class DialogPolicy(StrEnum):
236
+ """What to do when Access shows a modal dialog during an automated call."""
237
+
238
+ FAIL = "fail"
239
+ """Dismiss the dialog and raise ``AccessDialogError`` (default)."""
240
+ WARN = "warn"
241
+ """Dismiss the dialog and emit an ``AccessDialogWarning``."""
242
+ OFF = "off"
243
+ """Do not watch for dialogs (a blocking dialog can then hang until ``call_timeout``)."""
244
+
245
+
246
+ class PropertyType(StrEnum):
247
+ """Data types for user-defined DAO properties (``CreateProperty``)."""
248
+
249
+ BOOLEAN = "boolean"
250
+ BYTE = "byte"
251
+ INTEGER = "integer"
252
+ LONG = "long"
253
+ CURRENCY = "currency"
254
+ SINGLE = "single"
255
+ DOUBLE = "double"
256
+ DATE_TIME = "date_time"
257
+ TEXT = "text"
258
+ MEMO = "memo"