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
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()
|