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/modules.py ADDED
@@ -0,0 +1,101 @@
1
+ """VBA modules: the ``db.modules`` collection (creating and reading code needs Microsoft Access)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import TYPE_CHECKING
7
+
8
+ from pyaccesskit._ops import design as design_ops
9
+ from pyaccesskit.enums import ModuleKind, ObjectKind
10
+ from pyaccesskit.errors import ObjectNotFoundError
11
+
12
+ if TYPE_CHECKING:
13
+ from pyaccesskit._session.session import Session
14
+
15
+ __all__ = ["Module", "ModuleCollection"]
16
+
17
+
18
+ class Module:
19
+ """A standard or class module (a live, name-based handle)."""
20
+
21
+ def __init__(self, session: Session, name: str) -> None:
22
+ self._session = session
23
+ self._name = name
24
+
25
+ @property
26
+ def name(self) -> str:
27
+ """The module name."""
28
+ return self._name
29
+
30
+ @property
31
+ def kind(self) -> ModuleKind:
32
+ """Standard or class module."""
33
+ return design_ops.read_module(self._session.design(), self._name)[0]
34
+
35
+ @property
36
+ def code(self) -> str:
37
+ """The module's VBA source (without Access's class-module header)."""
38
+ return design_ops.read_module(self._session.design(), self._name)[1]
39
+
40
+ @code.setter
41
+ def code(self, value: str) -> None:
42
+ self._session.check_writable(f"update module {self._name!r}")
43
+ design_ops.create_module(self._session.design(), self._name, value, self.kind, replace=True)
44
+
45
+ def rename(self, new_name: str) -> None:
46
+ """Rename the module."""
47
+ self._session.check_writable(f"rename module {self._name!r}")
48
+ self._session.design().rename_object(ObjectKind.MODULE, self._name, new_name)
49
+ self._name = new_name
50
+
51
+ def drop(self) -> None:
52
+ """Delete the module."""
53
+ self._session.check_writable(f"drop module {self._name!r}")
54
+ self._session.design().delete_object(ObjectKind.MODULE, self._name)
55
+
56
+ def __repr__(self) -> str:
57
+ return f"<Module {self._name!r}>"
58
+
59
+
60
+ class ModuleCollection:
61
+ """``db.modules``: standard and class modules (form/report modules are managed with their object)."""
62
+
63
+ def __init__(self, session: Session) -> None:
64
+ self._session = session
65
+
66
+ def names(self) -> list[str]:
67
+ """Module names (read through DAO; does not need Microsoft Access)."""
68
+ return self._session.schema().list_documents(ObjectKind.MODULE)
69
+
70
+ def __iter__(self) -> Iterator[Module]:
71
+ return iter([Module(self._session, name) for name in self.names()])
72
+
73
+ def __len__(self) -> int:
74
+ return len(self.names())
75
+
76
+ def __contains__(self, name: object) -> bool:
77
+ return isinstance(name, str) and any(n.casefold() == name.casefold() for n in self.names())
78
+
79
+ def __getitem__(self, name: str) -> Module:
80
+ for actual in self.names():
81
+ if actual.casefold() == name.casefold():
82
+ return Module(self._session, actual)
83
+ raise ObjectNotFoundError(
84
+ f"module {name!r} does not exist", kind=ObjectKind.MODULE, name=name
85
+ )
86
+
87
+ def create(
88
+ self, name: str, code: str, *, kind: ModuleKind = ModuleKind.STANDARD, replace: bool = False
89
+ ) -> Module:
90
+ """Create a VBA module from source code.
91
+
92
+ ``Option Compare Database`` is added if missing (as Access does); the code is otherwise stored as
93
+ given. VBA source uses the Windows ANSI code page: characters outside it raise ``SpecError``.
94
+ """
95
+ self._session.check_writable(f"create module {name!r}")
96
+ design_ops.create_module(self._session.design(), name, code, kind, replace=replace)
97
+ return Module(self._session, name)
98
+
99
+ def drop(self, name: str) -> None:
100
+ """Delete a module."""
101
+ self[name].drop()
pyaccesskit/objects.py ADDED
@@ -0,0 +1,81 @@
1
+ """Generic management of Access objects and ``SaveAsText``/``LoadFromText`` text I/O (``db.objects``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from pyaccesskit._ops import design as design_ops
10
+ from pyaccesskit.enums import ObjectKind
11
+ from pyaccesskit.errors import SpecError
12
+
13
+ if TYPE_CHECKING:
14
+ from pyaccesskit._session.session import Session
15
+
16
+ __all__ = ["AccessObjects"]
17
+
18
+
19
+ class AccessObjects:
20
+ """Forms, reports, macros and modules as Access sees them, including raw text import/export.
21
+
22
+ Text is exchanged as ``str`` with LF line endings and stored on disk as UTF-8; PyAccessKit converts to and
23
+ from the encodings Access requires (UTF-16 for forms/reports/queries/macros, ANSI for VBA modules).
24
+ """
25
+
26
+ def __init__(self, session: Session) -> None:
27
+ self._session = session
28
+
29
+ @staticmethod
30
+ def _kind(kind: ObjectKind | str) -> ObjectKind:
31
+ value = ObjectKind(kind)
32
+ if value not in (*design_ops.DESIGN_KINDS, ObjectKind.QUERY):
33
+ raise SpecError(f"{value.value} objects are not supported here")
34
+ return value
35
+
36
+ def names(self, kind: ObjectKind | str) -> list[str]:
37
+ """Names of all objects of ``kind`` (read through DAO where possible)."""
38
+ value = self._kind(kind)
39
+ if value is ObjectKind.QUERY:
40
+ return [q.name for q in self._session.schema().list_queries()]
41
+ return self._session.schema().list_documents(value)
42
+
43
+ def export_text(self, kind: ObjectKind | str, name: str) -> str:
44
+ """``SaveAsText`` an object."""
45
+ return design_ops.export_object(self._session.design(), self._kind(kind), name)
46
+
47
+ def import_text(
48
+ self, kind: ObjectKind | str, name: str, text: str, *, replace: bool = False
49
+ ) -> None:
50
+ """``LoadFromText`` an object (Access replaces an existing object only when ``replace=True``)."""
51
+ self._session.check_writable(f"import {name!r}")
52
+ design_ops.import_object(
53
+ self._session.design(), self._kind(kind), name, text, replace=replace
54
+ )
55
+
56
+ def save_text(self, kind: ObjectKind | str, name: str, path: str | os.PathLike[str]) -> Path:
57
+ """Export an object to a UTF-8 text file; returns the path."""
58
+ target = Path(path)
59
+ target.write_text(self.export_text(kind, name), encoding="utf-8", newline="\n")
60
+ return target
61
+
62
+ def load_text(
63
+ self,
64
+ kind: ObjectKind | str,
65
+ name: str,
66
+ path: str | os.PathLike[str],
67
+ *,
68
+ replace: bool = False,
69
+ ) -> None:
70
+ """Import an object from a UTF-8 text file."""
71
+ self.import_text(kind, name, Path(path).read_text(encoding="utf-8"), replace=replace)
72
+
73
+ def delete(self, kind: ObjectKind | str, name: str) -> None:
74
+ """Delete a form, report, macro or module."""
75
+ self._session.check_writable(f"delete {name!r}")
76
+ self._session.design().delete_object(self._kind(kind), name)
77
+
78
+ def rename(self, kind: ObjectKind | str, old: str, new: str) -> None:
79
+ """Rename a form, report, macro or module."""
80
+ self._session.check_writable(f"rename {old!r}")
81
+ self._session.design().rename_object(self._kind(kind), old, new)
pyaccesskit/options.py ADDED
@@ -0,0 +1,40 @@
1
+ """Session options (how PyAccessKit starts and supervises Microsoft Access)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from pyaccesskit.enums import DialogPolicy, MacroSecurity
8
+
9
+ __all__ = ["SessionOptions"]
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class SessionOptions:
14
+ """Advanced settings for a database session. The defaults are right for build scripts.
15
+
16
+ Attributes:
17
+ visible: Show the Access window (useful for debugging). Dialog detection then only considers
18
+ standard dialog windows.
19
+ macro_security: What Access may run when it opens a database as its current database. The default,
20
+ ``DISABLE``, never runs AutoExec macros, startup-form code or VBA — whatever the user's Trust
21
+ Center says.
22
+ dialog_policy: What to do when Access shows a modal dialog during an automated call.
23
+ call_timeout: Seconds one operation may take before the owned Access process is terminated
24
+ (``None`` = no limit).
25
+ quit_timeout: Seconds to wait for Access to exit after ``Quit`` before terminating it.
26
+ kill_on_parent_exit: Put Access in a Windows job object so it dies with this Python process.
27
+ access_progid: ProgID used to start Access (e.g. ``"Access.Application.16"`` on multi-version
28
+ machines).
29
+ apply_native_defaults: When DAO creates a database, add the properties Access itself writes to new
30
+ databases (tabbed documents, themed controls...), so the result matches ``engine="access"``.
31
+ """
32
+
33
+ visible: bool = False
34
+ macro_security: MacroSecurity = MacroSecurity.DISABLE
35
+ dialog_policy: DialogPolicy = DialogPolicy.FAIL
36
+ call_timeout: float | None = 600.0
37
+ quit_timeout: float = 30.0
38
+ kill_on_parent_exit: bool = True
39
+ access_progid: str = "Access.Application"
40
+ apply_native_defaults: bool = True
@@ -0,0 +1,74 @@
1
+ """Access to DAO property collections (the escape hatch for properties PyAccessKit does not model)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from pyaccesskit._backends.protocols import PropertyTarget
8
+ from pyaccesskit.enums import PropertyType
9
+ from pyaccesskit.errors import ObjectNotFoundError
10
+ from pyaccesskit.schema import PropertyValue
11
+
12
+ if TYPE_CHECKING:
13
+ from pyaccesskit._session.session import Session
14
+
15
+ __all__ = ["PropertyBag"]
16
+
17
+
18
+ class PropertyBag:
19
+ """The DAO ``Properties`` of a database, table, field or query.
20
+
21
+ Reading a property that does not exist raises :class:`~pyaccesskit.errors.ObjectNotFoundError` (a
22
+ ``LookupError``); :meth:`get` returns a default instead. Setting a missing property creates it — with the
23
+ DAO type Access itself uses for well-known properties (``Description``, ``AppTitle``, ``UseMDIMode``...)
24
+ or one inferred from the value, unless ``type`` is given::
25
+
26
+ db.properties["AppTitle"] = "Customer Manager"
27
+ db.tables["Customers"].properties.set("SubdatasheetName", "[None]")
28
+ """
29
+
30
+ def __init__(self, session: Session, target: PropertyTarget) -> None:
31
+ self._session = session
32
+ self._target = target
33
+
34
+ def __getitem__(self, name: str) -> PropertyValue:
35
+ return self._session.schema().get_property(self._target, name)
36
+
37
+ def get(self, name: str, default: PropertyValue = None) -> PropertyValue:
38
+ """The property value, or ``default`` if it does not exist."""
39
+ try:
40
+ return self[name]
41
+ except ObjectNotFoundError:
42
+ return default
43
+
44
+ def __contains__(self, name: object) -> bool:
45
+ if not isinstance(name, str):
46
+ return False
47
+ try:
48
+ self[name]
49
+ except ObjectNotFoundError:
50
+ return False
51
+ return True
52
+
53
+ def __setitem__(self, name: str, value: PropertyValue) -> None:
54
+ self.set(name, value)
55
+
56
+ def set(self, name: str, value: PropertyValue, type: PropertyType | None = None) -> None:
57
+ """Set (creating if necessary) a property; ``type`` forces the DAO type of a new property."""
58
+ self._session.check_writable(f"set property {name!r}")
59
+ self._session.schema().set_property(self._target, name, value, type)
60
+
61
+ def __delitem__(self, name: str) -> None:
62
+ self.delete(name)
63
+
64
+ def delete(self, name: str) -> None:
65
+ """Delete a user-defined property."""
66
+ self._session.check_writable(f"delete property {name!r}")
67
+ self._session.schema().delete_property(self._target, name)
68
+
69
+ def to_dict(self) -> dict[str, PropertyValue]:
70
+ """All readable properties."""
71
+ return self._session.schema().list_properties(self._target)
72
+
73
+ def __repr__(self) -> str:
74
+ return f"<PropertyBag of {self._target.describe()}>"
pyaccesskit/py.typed ADDED
File without changes
pyaccesskit/queries.py ADDED
@@ -0,0 +1,190 @@
1
+ """Saved queries: the ``db.queries`` collection and :class:`Query` handles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator, Mapping
6
+ from typing import TYPE_CHECKING, Any, overload
7
+
8
+ from pyaccesskit._backends.protocols import ParameterInfo, PropertyTarget
9
+ from pyaccesskit._ops import schema as ops
10
+ from pyaccesskit.enums import ObjectKind, QueryKind
11
+ from pyaccesskit.errors import ObjectNotFoundError
12
+ from pyaccesskit.properties import PropertyBag
13
+ from pyaccesskit.schema import PassThroughOptions, QuerySpec
14
+ from pyaccesskit.schema._base import build
15
+
16
+ if TYPE_CHECKING:
17
+ from pyaccesskit._session.session import Session
18
+
19
+ __all__ = ["Query", "QueryCollection"]
20
+
21
+
22
+ class Query:
23
+ """A saved query (a live, name-based handle)."""
24
+
25
+ def __init__(self, session: Session, name: str) -> None:
26
+ self._session = session
27
+ self._name = name
28
+
29
+ @property
30
+ def name(self) -> str:
31
+ """The query name."""
32
+ return self._name
33
+
34
+ def to_spec(self) -> QuerySpec:
35
+ """The query's current definition (SQL as Access stored it)."""
36
+ return self._session.schema().read_query(self._name)
37
+
38
+ @property
39
+ def sql(self) -> str:
40
+ """The SQL text (Access reformats SQL when saving it)."""
41
+ return self.to_spec().sql
42
+
43
+ @sql.setter
44
+ def sql(self, value: str) -> None:
45
+ self._session.check_writable(f"update query {self._name!r}")
46
+ spec = build(QuerySpec, f"query {self._name!r}", name=self._name, sql=value)
47
+ self._session.schema().set_query_sql(self._name, spec.normalized().sql)
48
+
49
+ @property
50
+ def kind(self) -> QueryKind:
51
+ """Select, update, append, crosstab... as classified by Access."""
52
+ for info in self._session.schema().list_queries():
53
+ if info.name.casefold() == self._name.casefold():
54
+ return info.kind
55
+ raise ObjectNotFoundError(
56
+ f"query {self._name!r} does not exist", kind=ObjectKind.QUERY, name=self._name
57
+ )
58
+
59
+ @property
60
+ def parameters(self) -> list[ParameterInfo]:
61
+ """Declared and implicit parameters."""
62
+ return self._session.schema().query_parameters(self._name)
63
+
64
+ @property
65
+ def description(self) -> str | None:
66
+ """The query *Description*."""
67
+ return self.to_spec().description
68
+
69
+ @property
70
+ def properties(self) -> PropertyBag:
71
+ """The query's DAO properties."""
72
+ return PropertyBag(self._session, PropertyTarget.query(self._name))
73
+
74
+ def execute(self, params: Mapping[str, Any] | None = None) -> int:
75
+ """Run an action query; returns the number of affected rows."""
76
+ self._session.check_writable(f"run query {self._name!r}")
77
+ return self._session.schema().execute_saved(self._name, params)
78
+
79
+ def fetch(
80
+ self, params: Mapping[str, Any] | None = None, *, limit: int | None = None
81
+ ) -> list[dict[str, Any]]:
82
+ """Return the rows of a select query as dictionaries."""
83
+ return self._session.schema().fetch_saved(self._name, params, limit=limit).as_dicts()
84
+
85
+ def rename(self, new_name: str) -> None:
86
+ """Rename the query."""
87
+ self._session.check_writable(f"rename query {self._name!r}")
88
+ self._name = ops.rename_query(self._session.schema(), self._name, new_name)
89
+
90
+ def drop(self) -> None:
91
+ """Delete the query."""
92
+ self._session.check_writable(f"drop query {self._name!r}")
93
+ ops.drop_query(self._session.schema(), self._name)
94
+
95
+ def __repr__(self) -> str:
96
+ return f"<Query {self._name!r}>"
97
+
98
+
99
+ class QueryCollection:
100
+ """``db.queries``: saved queries (hidden ``~`` queries are excluded unless asked for)."""
101
+
102
+ def __init__(self, session: Session) -> None:
103
+ self._session = session
104
+
105
+ def names(self, *, include_hidden: bool = False) -> list[str]:
106
+ """Query names."""
107
+ return [
108
+ q.name
109
+ for q in self._session.schema().list_queries()
110
+ if include_hidden or not q.is_hidden
111
+ ]
112
+
113
+ def __iter__(self) -> Iterator[Query]:
114
+ return iter([Query(self._session, name) for name in self.names()])
115
+
116
+ def __len__(self) -> int:
117
+ return len(self.names())
118
+
119
+ def __contains__(self, name: object) -> bool:
120
+ return isinstance(name, str) and self.get(name) is not None
121
+
122
+ def get(self, name: str) -> Query | None:
123
+ """The query called ``name`` (case-insensitive), or ``None``."""
124
+ actual = ops.find_query(self._session.schema(), name)
125
+ return Query(self._session, actual) if actual is not None else None
126
+
127
+ def __getitem__(self, name: str) -> Query:
128
+ query = self.get(name)
129
+ if query is None:
130
+ raise ObjectNotFoundError(
131
+ f"query {name!r} does not exist", kind=ObjectKind.QUERY, name=name
132
+ )
133
+ return query
134
+
135
+ @overload
136
+ def create(self, spec: QuerySpec, /, *, replace: bool = False) -> Query: ...
137
+
138
+ @overload
139
+ def create(
140
+ self, name: str, sql: str, /, *, description: str | None = None, replace: bool = False
141
+ ) -> Query: ...
142
+
143
+ def create(
144
+ self,
145
+ spec_or_name: QuerySpec | str,
146
+ sql: str | None = None,
147
+ /,
148
+ *,
149
+ description: str | None = None,
150
+ replace: bool = False,
151
+ ) -> Query:
152
+ """Create a saved query (``replace=True`` updates the SQL of an existing one)."""
153
+ if isinstance(spec_or_name, QuerySpec):
154
+ spec = spec_or_name
155
+ else:
156
+ spec = build(
157
+ QuerySpec,
158
+ f"query {spec_or_name!r}",
159
+ name=spec_or_name,
160
+ sql=sql or "",
161
+ description=description,
162
+ )
163
+ self._session.check_writable(f"create query {spec.name!r}")
164
+ ops.create_or_replace_query(self._session.schema(), spec, replace=replace)
165
+ return self[spec.name]
166
+
167
+ def create_pass_through(
168
+ self,
169
+ name: str,
170
+ sql: str,
171
+ *,
172
+ connect: str,
173
+ returns_records: bool = True,
174
+ timeout: int = 60,
175
+ replace: bool = False,
176
+ ) -> Query:
177
+ """Create an ODBC pass-through query (``connect`` must start with ``ODBC;``)."""
178
+ options = build(
179
+ PassThroughOptions,
180
+ f"pass-through query {name!r}",
181
+ connect=connect,
182
+ returns_records=returns_records,
183
+ timeout=timeout,
184
+ )
185
+ spec = build(QuerySpec, f"query {name!r}", name=name, sql=sql, pass_through=options)
186
+ return self.create(spec, replace=replace)
187
+
188
+ def drop(self, name: str) -> None:
189
+ """Delete a saved query."""
190
+ self[name].drop()
@@ -0,0 +1,143 @@
1
+ """Relationships: the ``db.relationships`` collection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import TYPE_CHECKING, overload
7
+
8
+ from pyaccesskit._ops import schema as ops
9
+ from pyaccesskit.enums import JoinType, ObjectKind
10
+ from pyaccesskit.errors import ObjectNotFoundError
11
+ from pyaccesskit.schema import ColumnRef, RelationshipSpec
12
+
13
+ if TYPE_CHECKING:
14
+ from pyaccesskit._session.session import Session
15
+
16
+ __all__ = ["Relationship", "RelationshipCollection"]
17
+
18
+
19
+ class Relationship:
20
+ """A relationship (a live, name-based handle)."""
21
+
22
+ def __init__(self, session: Session, name: str) -> None:
23
+ self._session = session
24
+ self._name = name
25
+
26
+ @property
27
+ def name(self) -> str:
28
+ """The relationship name."""
29
+ return self._name
30
+
31
+ def to_spec(self) -> RelationshipSpec:
32
+ """The relationship's definition."""
33
+ for spec in self._session.schema().list_relationships():
34
+ if spec.effective_name.casefold() == self._name.casefold():
35
+ return spec
36
+ raise ObjectNotFoundError(
37
+ f"relationship {self._name!r} does not exist",
38
+ kind=ObjectKind.RELATIONSHIP,
39
+ name=self._name,
40
+ )
41
+
42
+ def drop(self) -> None:
43
+ """Delete the relationship."""
44
+ self._session.check_writable(f"drop relationship {self._name!r}")
45
+ ops.drop_relationship(self._session.schema(), self._name)
46
+
47
+ def __repr__(self) -> str:
48
+ return f"<Relationship {self._name!r}>"
49
+
50
+
51
+ class RelationshipCollection:
52
+ """``db.relationships``."""
53
+
54
+ def __init__(self, session: Session) -> None:
55
+ self._session = session
56
+
57
+ def specs(self) -> list[RelationshipSpec]:
58
+ """Every relationship as a spec."""
59
+ return self._session.schema().list_relationships()
60
+
61
+ def names(self) -> list[str]:
62
+ """Relationship names."""
63
+ return [spec.effective_name for spec in self.specs()]
64
+
65
+ def __iter__(self) -> Iterator[Relationship]:
66
+ return iter([Relationship(self._session, name) for name in self.names()])
67
+
68
+ def __len__(self) -> int:
69
+ return len(self.names())
70
+
71
+ def __contains__(self, name: object) -> bool:
72
+ return isinstance(name, str) and any(n.casefold() == name.casefold() for n in self.names())
73
+
74
+ def __getitem__(self, name: str) -> Relationship:
75
+ for actual in self.names():
76
+ if actual.casefold() == name.casefold():
77
+ return Relationship(self._session, actual)
78
+ raise ObjectNotFoundError(
79
+ f"relationship {name!r} does not exist", kind=ObjectKind.RELATIONSHIP, name=name
80
+ )
81
+
82
+ @overload
83
+ def create(self, spec: RelationshipSpec, /) -> Relationship: ...
84
+
85
+ @overload
86
+ def create(
87
+ self,
88
+ primary: ColumnRef,
89
+ foreign: ColumnRef,
90
+ /,
91
+ *,
92
+ name: str | None = None,
93
+ enforce_integrity: bool = True,
94
+ cascade_update: bool = False,
95
+ cascade_delete: bool = False,
96
+ one_to_one: bool = False,
97
+ join: JoinType = JoinType.INNER,
98
+ ) -> Relationship: ...
99
+
100
+ def create(
101
+ self,
102
+ primary: RelationshipSpec | ColumnRef,
103
+ foreign: ColumnRef | None = None,
104
+ /,
105
+ *,
106
+ name: str | None = None,
107
+ enforce_integrity: bool = True,
108
+ cascade_update: bool = False,
109
+ cascade_delete: bool = False,
110
+ one_to_one: bool = False,
111
+ join: JoinType = JoinType.INNER,
112
+ ) -> Relationship:
113
+ """Create a relationship from the primary ("one") side to the foreign ("many") side.
114
+
115
+ Example::
116
+
117
+ db.relationships.create("Customers.CustomerID", "Orders.CustomerID", cascade_delete=True)
118
+
119
+ The primary columns need a primary key or unique index. The default name follows Access's
120
+ convention (primary table + foreign table).
121
+ """
122
+ if isinstance(primary, RelationshipSpec):
123
+ spec = primary
124
+ else:
125
+ if foreign is None:
126
+ raise TypeError("create() needs both the primary and the foreign side")
127
+ spec = RelationshipSpec.between(
128
+ primary,
129
+ foreign,
130
+ name=name,
131
+ enforce_integrity=enforce_integrity,
132
+ cascade_update=cascade_update,
133
+ cascade_delete=cascade_delete,
134
+ one_to_one=one_to_one,
135
+ join=join,
136
+ )
137
+ self._session.check_writable(f"create relationship {spec.effective_name!r}")
138
+ created = ops.create_relationship(self._session.schema(), spec)
139
+ return Relationship(self._session, created.effective_name)
140
+
141
+ def drop(self, name: str) -> None:
142
+ """Delete a relationship."""
143
+ self[name].drop()