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,199 @@
1
+ # pyright: basic
2
+ """The COM call gateway: error translation, dialog monitoring and exact-argument invocation.
3
+
4
+ Every group of COM calls made by a backend runs inside :meth:`Com.op`, which
5
+
6
+ * tells the dialog watchdog that a call is in flight (so a modal dialog is detected, captured and
7
+ dismissed instead of hanging forever);
8
+ * translates ``pywintypes.com_error`` into :mod:`pyaccesskit.errors` exceptions with context;
9
+ * raises :class:`~pyaccesskit.errors.AccessDialogError` (or warns) if Access showed a dialog meanwhile.
10
+
11
+ The ``get``/``put``/``call`` helpers invoke ``IDispatch`` by DISPID with *exactly* the given arguments.
12
+ pywin32's dynamic wrappers cannot call indexed properties such as ``Form.Section(0)``, raise
13
+ ``AttributeError`` for members missing from a control's type info, and pad ``Application.Run``'s 30 optional
14
+ arguments (spikes S6 and S8); these helpers avoid all three problems.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import time
21
+ import warnings
22
+ from collections.abc import Callable, Iterator, Sequence
23
+ from contextlib import contextmanager
24
+ from pathlib import Path
25
+ from typing import Any, Protocol
26
+
27
+ import pythoncom
28
+ import pywintypes
29
+
30
+ from pyaccesskit._com import constants as c
31
+ from pyaccesskit._com.dispatch import wrap
32
+ from pyaccesskit._com.errors import OpContext, translate
33
+ from pyaccesskit._com.variants import normalize
34
+ from pyaccesskit.enums import DialogPolicy, ObjectKind
35
+ from pyaccesskit.errors import (
36
+ AccessDialogError,
37
+ AccessDialogWarning,
38
+ AccessTimeoutError,
39
+ DaoError,
40
+ DialogInfo,
41
+ )
42
+
43
+ __all__ = ["CallMonitor", "Com", "call", "get", "invoke", "put", "retry_busy"]
44
+
45
+ logger = logging.getLogger("pyaccesskit.com")
46
+
47
+ _LCID = 0
48
+ _BUSY = frozenset({c.RPC_E_CALL_REJECTED, c.RPC_E_SERVERCALL_RETRYLATER})
49
+
50
+
51
+ class CallMonitor(Protocol):
52
+ """Implemented by the dialog watchdog of an Access process."""
53
+
54
+ policy: DialogPolicy
55
+
56
+ def begin(self, description: str) -> None:
57
+ """An outermost COM operation starts."""
58
+ ...
59
+
60
+ def end(self) -> tuple[DialogInfo, ...]:
61
+ """The operation ended; return the dialogs observed while it ran."""
62
+ ...
63
+
64
+ @property
65
+ def termination(self) -> tuple[str, str] | None:
66
+ """``(kind, reason)`` if the monitor terminated Access (kind: timeout, dialog, interrupt)."""
67
+ ...
68
+
69
+
70
+ # --------------------------------------------------------------------------------------- invocation
71
+ def invoke(obj: Any, name: str, flags: int, *args: Any) -> Any:
72
+ """``IDispatch::Invoke`` ``name`` on ``obj`` with exactly ``args``; wraps/normalizes the result."""
73
+ ole = obj._oleobj_ if hasattr(obj, "_oleobj_") else obj
74
+ dispid = ole.GetIDsOfNames(name)
75
+ result = ole.Invoke(dispid, _LCID, flags, True, *args)
76
+ return normalize(wrap(result))
77
+
78
+
79
+ def get(obj: Any, name: str, *args: Any) -> Any:
80
+ """Read a (possibly indexed) property: ``get(form, "Section", 0)``."""
81
+ return invoke(obj, name, pythoncom.DISPATCH_PROPERTYGET | pythoncom.DISPATCH_METHOD, *args)
82
+
83
+
84
+ def call(obj: Any, name: str, *args: Any) -> Any:
85
+ """Call a method with exactly ``args`` (no padding of optional parameters)."""
86
+ return invoke(obj, name, pythoncom.DISPATCH_METHOD, *args)
87
+
88
+
89
+ def put(obj: Any, name: str, value: Any) -> None:
90
+ """Set a property."""
91
+ ole = obj._oleobj_ if hasattr(obj, "_oleobj_") else obj
92
+ dispid = ole.GetIDsOfNames(name)
93
+ ole.Invoke(dispid, _LCID, pythoncom.DISPATCH_PROPERTYPUT, False, value)
94
+
95
+
96
+ def retry_busy(fn: Callable[..., Any], *args: Any, timeout: float = 30.0) -> Any:
97
+ """Call ``fn`` and retry while the server rejects calls because it is busy (``RPC_E_CALL_REJECTED``).
98
+
99
+ A rejected call never started executing, so retrying is always safe.
100
+ """
101
+ deadline = time.monotonic() + timeout
102
+ delay = 0.05
103
+ while True:
104
+ try:
105
+ return fn(*args)
106
+ except pywintypes.com_error as exc:
107
+ if exc.args[0] not in _BUSY or time.monotonic() >= deadline:
108
+ raise
109
+ logger.debug("COM server busy; retrying in %.2fs", delay)
110
+ time.sleep(delay)
111
+ delay = min(delay * 2, 1.0)
112
+
113
+
114
+ # ------------------------------------------------------------------------------------------ gateway
115
+ class Com:
116
+ """Per-engine gateway: wraps operations with error translation and dialog monitoring."""
117
+
118
+ def __init__(
119
+ self,
120
+ *,
121
+ monitor: CallMonitor | None = None,
122
+ dao_errors: Callable[[], Sequence[DaoError]] | None = None,
123
+ ) -> None:
124
+ self._monitor = monitor
125
+ self._dao_errors = dao_errors
126
+ self._depth = 0
127
+
128
+ def _read_dao_errors(self) -> Sequence[DaoError]:
129
+ if self._dao_errors is None:
130
+ return ()
131
+ try:
132
+ return self._dao_errors()
133
+ except Exception:
134
+ return ()
135
+
136
+ @contextmanager
137
+ def op(
138
+ self,
139
+ action: str,
140
+ *,
141
+ kind: ObjectKind | None = None,
142
+ name: str | None = None,
143
+ path: Path | None = None,
144
+ sql: str | None = None,
145
+ ) -> Iterator[None]:
146
+ """Run a group of COM calls as one operation (see module docstring)."""
147
+ ctx = OpContext(action, kind, name, path, sql)
148
+ monitor = self._monitor
149
+ outermost = self._depth == 0
150
+ self._depth += 1
151
+ if monitor is not None and outermost:
152
+ monitor.begin(action)
153
+ ended = False
154
+
155
+ def finish() -> tuple[DialogInfo, ...]:
156
+ nonlocal ended
157
+ if ended or monitor is None or not outermost:
158
+ return ()
159
+ ended = True
160
+ return monitor.end()
161
+
162
+ try:
163
+ logger.debug("COM op: %s", action)
164
+ yield
165
+ except pywintypes.com_error as exc:
166
+ dialogs = finish()
167
+ error = translate(exc, ctx, self._read_dao_errors())
168
+ termination = monitor.termination if monitor is not None else None
169
+ if termination is not None and termination[0] == "timeout":
170
+ raise AccessTimeoutError(
171
+ f"cannot {action}: {termination[1]}; the owned Access process was terminated",
172
+ operation=action,
173
+ details=error.details,
174
+ ) from error
175
+ if dialogs:
176
+ raise AccessDialogError(
177
+ f"cannot {action}: Microsoft Access showed a dialog",
178
+ dialogs=dialogs,
179
+ operation=action,
180
+ ) from error
181
+ raise error from exc
182
+ except BaseException:
183
+ finish()
184
+ raise
185
+ finally:
186
+ self._depth -= 1
187
+ dialogs = finish()
188
+ if dialogs and monitor is not None:
189
+ if monitor.policy is DialogPolicy.FAIL:
190
+ raise AccessDialogError(
191
+ f"{action}: Microsoft Access showed a dialog that was dismissed automatically",
192
+ dialogs=dialogs,
193
+ operation=action,
194
+ )
195
+ if monitor.policy is DialogPolicy.WARN:
196
+ for dialog in dialogs:
197
+ warnings.warn(
198
+ f"{action}: {dialog.summary()}", AccessDialogWarning, stacklevel=3
199
+ )
@@ -0,0 +1,164 @@
1
+ # pyright: basic
2
+ """Revocable proxies for the ``db.raw`` escape hatch.
3
+
4
+ Raw COM objects handed to user code are wrapped so that closing (or upgrading) the session can *revoke*
5
+ them: every proxy — including the ones derived from it (``db.raw.dao.TableDefs(0).Fields``...) — drops its
6
+ COM reference and raises :class:`~pyaccesskit.errors.SessionClosedError` afterwards. User variables can
7
+ therefore never keep an Access process alive or be used against a closed database.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import threading
13
+ import weakref
14
+ from collections.abc import Iterator
15
+ from typing import Any
16
+
17
+ from pyaccesskit.errors import SessionClosedError
18
+
19
+ __all__ = ["ProxyRegistry", "RevocableIterator", "RevocableProxy"]
20
+
21
+ _SLOTS = ("_pak_target", "_pak_registry", "_pak_label", "__weakref__")
22
+
23
+
24
+ class ProxyRegistry:
25
+ """Tracks every proxy created for a session so they can all be revoked at once."""
26
+
27
+ def __init__(self) -> None:
28
+ self._proxies: weakref.WeakSet[RevocableProxy | RevocableIterator] = weakref.WeakSet()
29
+ self._lock = threading.Lock()
30
+ self.revoked = False
31
+
32
+ def track(self, proxy: RevocableProxy | RevocableIterator) -> None:
33
+ with self._lock:
34
+ self._proxies.add(proxy)
35
+
36
+ def revoke_all(self) -> int:
37
+ """Revoke every live proxy; returns how many were revoked."""
38
+ with self._lock:
39
+ proxies = list(self._proxies)
40
+ self._proxies.clear()
41
+ self.revoked = True
42
+ for proxy in proxies:
43
+ object.__setattr__(
44
+ proxy, "_pak_target", None
45
+ ) # iterators drop their COM enumerator too
46
+ return len(proxies)
47
+
48
+ def wrap(self, value: Any, label: str) -> Any:
49
+ """Wrap COM objects and bound COM methods; return plain values unchanged."""
50
+ if value is None or isinstance(value, (str, bytes, int, float, bool, tuple, list, dict)):
51
+ return value
52
+ if hasattr(value, "_oleobj_") or callable(value):
53
+ proxy = RevocableProxy(value, self, label)
54
+ self.track(proxy)
55
+ return proxy
56
+ return value
57
+
58
+
59
+ def _unwrap(value: Any) -> Any:
60
+ if isinstance(value, RevocableProxy):
61
+ return value._pak_require()
62
+ if isinstance(value, (list, tuple)):
63
+ return type(value)(_unwrap(item) for item in value)
64
+ return value
65
+
66
+
67
+ class RevocableProxy:
68
+ """A transparent, revocable stand-in for a COM object (or one of its methods)."""
69
+
70
+ __slots__ = _SLOTS
71
+
72
+ def __init__(self, target: Any, registry: ProxyRegistry, label: str) -> None:
73
+ object.__setattr__(self, "_pak_target", target)
74
+ object.__setattr__(self, "_pak_registry", registry)
75
+ object.__setattr__(self, "_pak_label", label)
76
+
77
+ def _pak_require(self) -> Any:
78
+ target = object.__getattribute__(self, "_pak_target")
79
+ if target is None:
80
+ label = object.__getattribute__(self, "_pak_label")
81
+ raise SessionClosedError(
82
+ f"raw COM object {label} was revoked because its PyAccessKit session was closed or upgraded"
83
+ )
84
+ return target
85
+
86
+ def __getattr__(self, name: str) -> Any:
87
+ target = self._pak_require()
88
+ label = object.__getattribute__(self, "_pak_label")
89
+ registry: ProxyRegistry = object.__getattribute__(self, "_pak_registry")
90
+ return registry.wrap(getattr(target, name), f"{label}.{name}")
91
+
92
+ def __setattr__(self, name: str, value: Any) -> None:
93
+ setattr(self._pak_require(), name, _unwrap(value))
94
+
95
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
96
+ target = self._pak_require()
97
+ label = object.__getattribute__(self, "_pak_label")
98
+ registry: ProxyRegistry = object.__getattribute__(self, "_pak_registry")
99
+ result = target(*_unwrap(args), **{k: _unwrap(v) for k, v in kwargs.items()})
100
+ return registry.wrap(result, f"{label}(...)")
101
+
102
+ def __iter__(self) -> Iterator[Any]:
103
+ target = self._pak_require()
104
+ label = object.__getattribute__(self, "_pak_label")
105
+ registry: ProxyRegistry = object.__getattribute__(self, "_pak_registry")
106
+ iterator = RevocableIterator(iter(target), registry, label)
107
+ registry.track(iterator)
108
+ return iterator
109
+
110
+ def __getitem__(self, key: Any) -> Any:
111
+ target = self._pak_require()
112
+ label = object.__getattribute__(self, "_pak_label")
113
+ registry: ProxyRegistry = object.__getattribute__(self, "_pak_registry")
114
+ return registry.wrap(target[_unwrap(key)], f"{label}[{key!r}]")
115
+
116
+ def __len__(self) -> int:
117
+ return len(self._pak_require())
118
+
119
+ def __bool__(self) -> bool:
120
+ return object.__getattribute__(self, "_pak_target") is not None
121
+
122
+ @property
123
+ def revoked(self) -> bool:
124
+ """Whether the underlying COM object has been released."""
125
+ return object.__getattribute__(self, "_pak_target") is None
126
+
127
+ def __repr__(self) -> str:
128
+ label = object.__getattribute__(self, "_pak_label")
129
+ state = "revoked" if self.revoked else "live"
130
+ return f"<pyaccesskit raw {label} ({state})>"
131
+
132
+
133
+ class RevocableIterator:
134
+ """Iterates a raw COM collection; revoking it drops the enumerator, so resuming raises."""
135
+
136
+ __slots__ = ("_pak_index", *_SLOTS)
137
+
138
+ def __init__(self, target: Iterator[Any], registry: ProxyRegistry, label: str) -> None:
139
+ object.__setattr__(self, "_pak_target", target)
140
+ object.__setattr__(self, "_pak_registry", registry)
141
+ object.__setattr__(self, "_pak_label", label)
142
+ object.__setattr__(self, "_pak_index", 0)
143
+
144
+ def __iter__(self) -> RevocableIterator:
145
+ return self
146
+
147
+ def __next__(self) -> Any:
148
+ target = object.__getattribute__(self, "_pak_target")
149
+ label = object.__getattribute__(self, "_pak_label")
150
+ if target is None:
151
+ raise SessionClosedError(
152
+ f"iteration over raw COM object {label} was revoked because its PyAccessKit session was "
153
+ "closed or upgraded"
154
+ )
155
+ item = next(target)
156
+ index = object.__getattribute__(self, "_pak_index")
157
+ object.__setattr__(self, "_pak_index", index + 1)
158
+ registry: ProxyRegistry = object.__getattribute__(self, "_pak_registry")
159
+ return registry.wrap(item, f"{label}[{index}]")
160
+
161
+ @property
162
+ def revoked(self) -> bool:
163
+ """Whether the underlying enumerator has been released."""
164
+ return object.__getattribute__(self, "_pak_target") is None
@@ -0,0 +1,39 @@
1
+ # pyright: basic
2
+ """pywin32 availability and per-thread COM initialisation.
3
+
4
+ This package (``pyaccesskit._com``) is the only place, together with ``_win``, ``_engines`` and the COM
5
+ backends, that imports pywin32. It is imported lazily when a real engine is created, so
6
+ ``import pyaccesskit`` works on any platform.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import threading
13
+
14
+ import pythoncom
15
+
16
+ logger = logging.getLogger("pyaccesskit.com")
17
+
18
+ __all__ = ["ensure_com_initialized"]
19
+
20
+ _RPC_E_CHANGED_MODE = -2147417850 # 0x80010106
21
+ _state = threading.local()
22
+
23
+
24
+ def ensure_com_initialized() -> None:
25
+ """Initialise COM (single-threaded apartment) on the calling thread, once.
26
+
27
+ ``import pythoncom`` already initialises the importing thread; worker threads need this call. COM is
28
+ never uninitialised by PyAccessKit (other code on the thread may still hold COM objects). If the thread
29
+ was initialised as MTA by someone else, calls still work through COM marshalling.
30
+ """
31
+ if getattr(_state, "initialized", False):
32
+ return
33
+ try:
34
+ pythoncom.CoInitializeEx(pythoncom.COINIT_APARTMENTTHREADED)
35
+ except pythoncom.com_error as exc:
36
+ if exc.args[0] != _RPC_E_CHANGED_MODE:
37
+ raise
38
+ logger.debug("thread %s is already a multithreaded COM apartment", threading.get_ident())
39
+ _state.initialized = True
@@ -0,0 +1,72 @@
1
+ # pyright: basic
2
+ """Conversion of values between Python and COM VARIANTs.
3
+
4
+ Access date/times are *wall-clock* values without a time zone. pywin32, however, treats a naive ``datetime``
5
+ as local time and converts it to UTC when it builds a ``VT_DATE`` — shifting the stored value by the UTC
6
+ offset (an integration test caught a 13-hour shift). :func:`to_variant` therefore passes wall-clock values
7
+ as UTC-tagged datetimes, which pywin32 stores unchanged; :func:`normalize` reads the wall-clock back.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from datetime import UTC, date, datetime, time
13
+ from typing import Any
14
+
15
+ import pywintypes
16
+
17
+ __all__ = ["normalize", "to_variant"]
18
+
19
+ _OLE_EPOCH = date(1899, 12, 30)
20
+
21
+
22
+ def to_variant(value: Any) -> Any:
23
+ """Prepare a Python value for COM without time-zone shifts.
24
+
25
+ * naive ``datetime`` → the same wall-clock time, tagged UTC;
26
+ * aware ``datetime`` → converted to local wall-clock time first (Access has no time zones);
27
+ * ``date`` → midnight; ``time`` → a time on the OLE epoch day (how Access stores times of day).
28
+ """
29
+ if isinstance(value, (bytearray, memoryview)):
30
+ return bytes(value)
31
+ if isinstance(value, datetime):
32
+ if value.tzinfo is not None:
33
+ value = value.astimezone().replace(tzinfo=None)
34
+ return value.replace(tzinfo=UTC)
35
+ if isinstance(value, date):
36
+ return datetime(value.year, value.month, value.day, tzinfo=UTC)
37
+ if isinstance(value, time):
38
+ return datetime(
39
+ _OLE_EPOCH.year,
40
+ _OLE_EPOCH.month,
41
+ _OLE_EPOCH.day,
42
+ value.hour,
43
+ value.minute,
44
+ value.second,
45
+ tzinfo=UTC,
46
+ )
47
+ return value
48
+
49
+
50
+ def normalize(value: Any) -> Any:
51
+ """Convert pywin32 return values to plain Python values.
52
+
53
+ * ``pywintypes.datetime`` (VT_DATE) → naive :class:`datetime` with the stored wall-clock time;
54
+ * byte arrays (OLE Object / binary fields arrive as ``memoryview``) → :class:`bytes`;
55
+ * tuples (SAFEARRAYs) are normalised recursively;
56
+ * everything else (``Decimal`` for currency/decimal, ``None`` for Null/Empty...) is returned unchanged.
57
+ """
58
+ if isinstance(value, memoryview):
59
+ return value.tobytes()
60
+ if isinstance(value, pywintypes.TimeType):
61
+ return datetime(
62
+ value.year,
63
+ value.month,
64
+ value.day,
65
+ value.hour,
66
+ value.minute,
67
+ value.second,
68
+ value.microsecond,
69
+ )
70
+ if isinstance(value, tuple):
71
+ return tuple(normalize(item) for item in value)
72
+ return value
@@ -0,0 +1,48 @@
1
+ # pyright: basic
2
+ """Engine construction (COM). Imported lazily by :mod:`pyaccesskit._session`."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+ from pyaccesskit._session.protocols import EnginePlan
9
+
10
+ __all__ = ["open_engine"]
11
+
12
+
13
+ def open_engine(plan: EnginePlan) -> Any:
14
+ """Create the engine described by ``plan`` (see :class:`EnginePlan`)."""
15
+ if plan.kind == "dao":
16
+ from pyaccesskit._engines.inproc import InProcDaoEngine
17
+
18
+ return InProcDaoEngine(
19
+ plan.path,
20
+ create=plan.create,
21
+ readonly=plan.readonly,
22
+ exclusive=plan.exclusive,
23
+ password=plan.password,
24
+ native_defaults=plan.options.apply_native_defaults,
25
+ on_created=plan.on_created,
26
+ )
27
+ from pyaccesskit._engines.access import AccessEngine
28
+ from pyaccesskit._win.access_process import AccessLaunchOptions
29
+
30
+ options = plan.options
31
+ return AccessEngine(
32
+ plan.path,
33
+ create=plan.create,
34
+ readonly=plan.readonly,
35
+ exclusive=plan.exclusive,
36
+ password=plan.password,
37
+ design=plan.design,
38
+ on_created=plan.on_created,
39
+ options=AccessLaunchOptions(
40
+ progid=options.access_progid,
41
+ visible=options.visible,
42
+ macro_security=options.macro_security,
43
+ dialog_policy=options.dialog_policy,
44
+ call_timeout=options.call_timeout,
45
+ quit_timeout=options.quit_timeout,
46
+ kill_on_parent_exit=options.kill_on_parent_exit,
47
+ ),
48
+ )