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/units.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# pyright: reportUnnecessaryIsInstance=false
|
|
2
|
+
# (operators and parse() deliberately type-check arguments at runtime)
|
|
3
|
+
"""Physical lengths for form and report layout.
|
|
4
|
+
|
|
5
|
+
Access measures positions and sizes in **twips** (1/1440 inch, 1/20 point). PyAccessKit lets you work in
|
|
6
|
+
familiar units instead and converts exactly once, when talking to Access::
|
|
7
|
+
|
|
8
|
+
from pyaccesskit.units import cm, mm, inch
|
|
9
|
+
|
|
10
|
+
width = cm(6) + mm(5) # Length(twips=3685)
|
|
11
|
+
width.cm # 6.5 (approximately; lengths are whole twips)
|
|
12
|
+
|
|
13
|
+
A :class:`Length` is an immutable whole number of twips. In specs it serializes to a readable string such
|
|
14
|
+
as ``"6.5cm"`` and parses from strings (``"2cm"``, ``"10mm"``, ``"1.5in"``, ``"12pt"``, ``"300tw"``) or plain
|
|
15
|
+
integers (twips).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import math
|
|
21
|
+
import re
|
|
22
|
+
from typing import TYPE_CHECKING, Any, SupportsFloat
|
|
23
|
+
|
|
24
|
+
from pydantic_core import core_schema
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
|
|
28
|
+
from pydantic.json_schema import JsonSchemaValue
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"TWIPS_PER_CM",
|
|
32
|
+
"TWIPS_PER_INCH",
|
|
33
|
+
"TWIPS_PER_MM",
|
|
34
|
+
"TWIPS_PER_POINT",
|
|
35
|
+
"Length",
|
|
36
|
+
"cm",
|
|
37
|
+
"inch",
|
|
38
|
+
"mm",
|
|
39
|
+
"pt",
|
|
40
|
+
"twips",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
TWIPS_PER_INCH = 1440
|
|
44
|
+
TWIPS_PER_POINT = 20
|
|
45
|
+
TWIPS_PER_CM = TWIPS_PER_INCH / 2.54
|
|
46
|
+
TWIPS_PER_MM = TWIPS_PER_CM / 10
|
|
47
|
+
|
|
48
|
+
_UNIT_FACTORS: dict[str, float] = {
|
|
49
|
+
"tw": 1,
|
|
50
|
+
"twip": 1,
|
|
51
|
+
"twips": 1,
|
|
52
|
+
"pt": TWIPS_PER_POINT,
|
|
53
|
+
"point": TWIPS_PER_POINT,
|
|
54
|
+
"points": TWIPS_PER_POINT,
|
|
55
|
+
"mm": TWIPS_PER_MM,
|
|
56
|
+
"cm": TWIPS_PER_CM,
|
|
57
|
+
"in": TWIPS_PER_INCH,
|
|
58
|
+
"inch": TWIPS_PER_INCH,
|
|
59
|
+
"inches": TWIPS_PER_INCH,
|
|
60
|
+
'"': TWIPS_PER_INCH,
|
|
61
|
+
}
|
|
62
|
+
_PATTERN = re.compile(r"^\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s*([a-zA-Z\"]*)\s*$")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _to_twips(value: SupportsFloat, factor: float) -> int:
|
|
66
|
+
number = float(value)
|
|
67
|
+
if not math.isfinite(number):
|
|
68
|
+
raise ValueError(f"length must be finite, got {value!r}")
|
|
69
|
+
return round(number * factor)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Length:
|
|
73
|
+
"""An immutable length, stored as a whole number of twips.
|
|
74
|
+
|
|
75
|
+
Construct lengths with the helper functions :func:`twips`, :func:`pt`, :func:`mm`, :func:`cm` and
|
|
76
|
+
:func:`inch`, or parse text with :meth:`parse`.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
__slots__ = ("_twips",)
|
|
80
|
+
|
|
81
|
+
_twips: int
|
|
82
|
+
|
|
83
|
+
def __init__(self, twips: int) -> None:
|
|
84
|
+
if isinstance(twips, bool) or not isinstance(twips, int):
|
|
85
|
+
raise TypeError(
|
|
86
|
+
f"Length expects an integer number of twips, got {type(twips).__name__}"
|
|
87
|
+
)
|
|
88
|
+
object.__setattr__(self, "_twips", twips)
|
|
89
|
+
|
|
90
|
+
# --------------------------------------------------------------------------------------- access
|
|
91
|
+
@property
|
|
92
|
+
def twips(self) -> int:
|
|
93
|
+
"""The length in twips (the unit Access uses)."""
|
|
94
|
+
return self._twips
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def points(self) -> float:
|
|
98
|
+
"""The length in typographic points."""
|
|
99
|
+
return self._twips / TWIPS_PER_POINT
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def mm(self) -> float:
|
|
103
|
+
"""The length in millimetres."""
|
|
104
|
+
return self._twips / TWIPS_PER_MM
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def cm(self) -> float:
|
|
108
|
+
"""The length in centimetres."""
|
|
109
|
+
return self._twips / TWIPS_PER_CM
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def inches(self) -> float:
|
|
113
|
+
"""The length in inches."""
|
|
114
|
+
return self._twips / TWIPS_PER_INCH
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------------- conversion
|
|
117
|
+
@classmethod
|
|
118
|
+
def parse(cls, value: str | int | Length) -> Length:
|
|
119
|
+
"""Parse ``"2cm"``, ``"10 mm"``, ``"1.5in"``, ``"12pt"``, ``"300tw"`` or an integer number of twips.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValueError: If the text is not a recognised length.
|
|
123
|
+
"""
|
|
124
|
+
if isinstance(value, Length):
|
|
125
|
+
return value
|
|
126
|
+
if isinstance(value, bool):
|
|
127
|
+
raise TypeError("a boolean is not a length")
|
|
128
|
+
if isinstance(value, int):
|
|
129
|
+
return cls(value)
|
|
130
|
+
match = _PATTERN.match(value)
|
|
131
|
+
if match is None:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"not a length: {value!r} (expected e.g. '2cm', '10mm', '1.5in', '12pt')"
|
|
134
|
+
)
|
|
135
|
+
number, unit = match.group(1), match.group(2).lower()
|
|
136
|
+
if not unit:
|
|
137
|
+
if "." in number:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"a unitless length must be a whole number of twips, got {value!r}"
|
|
140
|
+
)
|
|
141
|
+
return cls(int(number))
|
|
142
|
+
factor = _UNIT_FACTORS.get(unit)
|
|
143
|
+
if factor is None:
|
|
144
|
+
raise ValueError(f"unknown length unit {unit!r} in {value!r}")
|
|
145
|
+
return cls(_to_twips(float(number), factor))
|
|
146
|
+
|
|
147
|
+
def format(self, unit: str = "cm") -> str:
|
|
148
|
+
"""Format in ``unit`` (``"cm"``, ``"mm"``, ``"in"``, ``"pt"`` or ``"tw"``).
|
|
149
|
+
|
|
150
|
+
The result is the *shortest* decimal that parses back to exactly the same number of twips:
|
|
151
|
+
``cm(1.5).format() == "1.5cm"`` and ``Length.parse(x.format(unit)) == x`` for every length and unit.
|
|
152
|
+
"""
|
|
153
|
+
key = unit.lower()
|
|
154
|
+
factor = _UNIT_FACTORS.get(key)
|
|
155
|
+
if factor is None:
|
|
156
|
+
raise ValueError(f"unknown length unit {unit!r}")
|
|
157
|
+
if factor == 1:
|
|
158
|
+
return f"{self._twips}tw"
|
|
159
|
+
value = self._twips / factor
|
|
160
|
+
text = f"{value:.6f}"
|
|
161
|
+
for decimals in range(7):
|
|
162
|
+
text = f"{value:.{decimals}f}"
|
|
163
|
+
if _to_twips(float(text), factor) == self._twips:
|
|
164
|
+
break
|
|
165
|
+
text = text.rstrip("0").rstrip(".") if "." in text else text
|
|
166
|
+
return f"{'0' if text in ('', '-0') else text}{key}"
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------------- arithmetic
|
|
169
|
+
def __add__(self, other: Length) -> Length:
|
|
170
|
+
if not isinstance(other, Length):
|
|
171
|
+
return NotImplemented
|
|
172
|
+
return Length(self._twips + other._twips)
|
|
173
|
+
|
|
174
|
+
def __sub__(self, other: Length) -> Length:
|
|
175
|
+
if not isinstance(other, Length):
|
|
176
|
+
return NotImplemented
|
|
177
|
+
return Length(self._twips - other._twips)
|
|
178
|
+
|
|
179
|
+
def __mul__(self, factor: float) -> Length:
|
|
180
|
+
if isinstance(factor, bool) or not isinstance(factor, (int, float)):
|
|
181
|
+
return NotImplemented
|
|
182
|
+
return Length(_to_twips(self._twips, factor))
|
|
183
|
+
|
|
184
|
+
__rmul__ = __mul__
|
|
185
|
+
|
|
186
|
+
def __truediv__(self, other: float | Length) -> Any:
|
|
187
|
+
if isinstance(other, Length):
|
|
188
|
+
return self._twips / other._twips
|
|
189
|
+
if isinstance(other, bool) or not isinstance(other, (int, float)):
|
|
190
|
+
return NotImplemented
|
|
191
|
+
return Length(_to_twips(self._twips, 1 / other))
|
|
192
|
+
|
|
193
|
+
def __neg__(self) -> Length:
|
|
194
|
+
return Length(-self._twips)
|
|
195
|
+
|
|
196
|
+
def __pos__(self) -> Length:
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
def __abs__(self) -> Length:
|
|
200
|
+
return Length(abs(self._twips))
|
|
201
|
+
|
|
202
|
+
def __bool__(self) -> bool:
|
|
203
|
+
return self._twips != 0
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------------- comparison
|
|
206
|
+
def __eq__(self, other: object) -> bool:
|
|
207
|
+
if not isinstance(other, Length):
|
|
208
|
+
return NotImplemented
|
|
209
|
+
return self._twips == other._twips
|
|
210
|
+
|
|
211
|
+
def __lt__(self, other: Length) -> bool:
|
|
212
|
+
if not isinstance(other, Length):
|
|
213
|
+
return NotImplemented
|
|
214
|
+
return self._twips < other._twips
|
|
215
|
+
|
|
216
|
+
def __le__(self, other: Length) -> bool:
|
|
217
|
+
if not isinstance(other, Length):
|
|
218
|
+
return NotImplemented
|
|
219
|
+
return self._twips <= other._twips
|
|
220
|
+
|
|
221
|
+
def __gt__(self, other: Length) -> bool:
|
|
222
|
+
if not isinstance(other, Length):
|
|
223
|
+
return NotImplemented
|
|
224
|
+
return self._twips > other._twips
|
|
225
|
+
|
|
226
|
+
def __ge__(self, other: Length) -> bool:
|
|
227
|
+
if not isinstance(other, Length):
|
|
228
|
+
return NotImplemented
|
|
229
|
+
return self._twips >= other._twips
|
|
230
|
+
|
|
231
|
+
def __hash__(self) -> int:
|
|
232
|
+
return hash(("Length", self._twips))
|
|
233
|
+
|
|
234
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
235
|
+
raise AttributeError("Length is immutable")
|
|
236
|
+
|
|
237
|
+
def __repr__(self) -> str:
|
|
238
|
+
return f"Length(twips={self._twips})"
|
|
239
|
+
|
|
240
|
+
def __str__(self) -> str:
|
|
241
|
+
return self.format("cm")
|
|
242
|
+
|
|
243
|
+
# ------------------------------------------------------------------------------------- pydantic
|
|
244
|
+
@classmethod
|
|
245
|
+
def __get_pydantic_core_schema__(
|
|
246
|
+
cls, _source: Any, _handler: GetCoreSchemaHandler
|
|
247
|
+
) -> core_schema.CoreSchema:
|
|
248
|
+
def validate(value: Any) -> Length:
|
|
249
|
+
if isinstance(value, (Length, str, int)):
|
|
250
|
+
return cls.parse(value)
|
|
251
|
+
# ValueError (not TypeError) so Pydantic reports a normal ValidationError.
|
|
252
|
+
raise ValueError(
|
|
253
|
+
f"expected a Length, a string like '2cm', or an integer of twips; got {value!r}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return core_schema.no_info_plain_validator_function(
|
|
257
|
+
validate,
|
|
258
|
+
serialization=core_schema.plain_serializer_function_ser_schema(
|
|
259
|
+
lambda length: length.format("cm"), when_used="json"
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def __get_pydantic_json_schema__(
|
|
265
|
+
cls, _schema: core_schema.CoreSchema, _handler: GetJsonSchemaHandler
|
|
266
|
+
) -> JsonSchemaValue:
|
|
267
|
+
return {
|
|
268
|
+
"anyOf": [
|
|
269
|
+
{
|
|
270
|
+
"type": "string",
|
|
271
|
+
"pattern": r"^\s*[+-]?(\d+(\.\d*)?|\.\d+)\s*(tw|twips?|pt|points?|mm|cm|in|inch|inches)\s*$",
|
|
272
|
+
},
|
|
273
|
+
{"type": "integer", "description": "twips"},
|
|
274
|
+
],
|
|
275
|
+
"description": "A length such as '2cm', '10mm', '1.5in', '12pt', or an integer number of twips.",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def twips(value: int) -> Length:
|
|
280
|
+
"""A length of ``value`` twips (1/1440 inch)."""
|
|
281
|
+
return Length(value)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def pt(value: float) -> Length:
|
|
285
|
+
"""A length of ``value`` typographic points (20 twips each)."""
|
|
286
|
+
return Length(_to_twips(value, TWIPS_PER_POINT))
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def mm(value: float) -> Length:
|
|
290
|
+
"""A length of ``value`` millimetres."""
|
|
291
|
+
return Length(_to_twips(value, TWIPS_PER_MM))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def cm(value: float) -> Length:
|
|
295
|
+
"""A length of ``value`` centimetres."""
|
|
296
|
+
return Length(_to_twips(value, TWIPS_PER_CM))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def inch(value: float) -> Length:
|
|
300
|
+
"""A length of ``value`` inches."""
|
|
301
|
+
return Length(_to_twips(value, TWIPS_PER_INCH))
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pyaccesskit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A modern, typed, Pythonic toolkit for building and modifying Microsoft Access databases and applications.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ariedotcodotnz/PyAccessKit
|
|
6
|
+
Project-URL: Documentation, https://ariedotcodotnz.github.io/PyAccessKit/
|
|
7
|
+
Project-URL: Changelog, https://github.com/ariedotcodotnz/PyAccessKit/blob/HEAD/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/ariedotcodotnz/PyAccessKit/issues
|
|
9
|
+
Project-URL: Source, https://github.com/ariedotcodotnz/PyAccessKit
|
|
10
|
+
Author: Arie Joe
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: accdb,access,automation,com,dao,database,microsoft-access,pywin32
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Database
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
28
|
+
Requires-Dist: pywin32>=306; sys_platform == 'win32'
|
|
29
|
+
Requires-Dist: typer>=0.12
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# PyAccessKit
|
|
33
|
+
|
|
34
|
+
A modern, typed, Pythonic toolkit for building and modifying Microsoft Access databases (`.accdb`) and,
|
|
35
|
+
progressively, whole Access applications without raw `pywin32` calls, DAO magic numbers or orphaned
|
|
36
|
+
`MSACCESS.EXE` processes.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from pyaccesskit import AccessDatabase, Column, Expr, Vba, cm
|
|
40
|
+
|
|
41
|
+
with AccessDatabase.create("crm.accdb") as db:
|
|
42
|
+
db.tables.create(
|
|
43
|
+
"Customers",
|
|
44
|
+
columns=[
|
|
45
|
+
Column.autonumber("CustomerID", primary_key=True),
|
|
46
|
+
Column.text("CustomerName", length=200, required=True),
|
|
47
|
+
Column.text("Email", length=255, unique=True),
|
|
48
|
+
Column.date_time("CreatedAt", default=Expr("Now()")),
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
db.tables.create(
|
|
52
|
+
"Orders",
|
|
53
|
+
columns=[
|
|
54
|
+
Column.autonumber("OrderID", primary_key=True),
|
|
55
|
+
Column.number("CustomerID", required=True), # Long Integer, as in Access
|
|
56
|
+
Column.currency("Total", default=0),
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
db.relationships.create("Customers.CustomerID", "Orders.CustomerID", cascade_delete=True)
|
|
60
|
+
db.queries.create("qryCustomers", "SELECT * FROM Customers ORDER BY CustomerName;")
|
|
61
|
+
|
|
62
|
+
with db.forms.create("frmCustomers", record_source="Customers", caption="Customers") as form:
|
|
63
|
+
form.textbox("CustomerName", label="Customer name", width=cm(8))
|
|
64
|
+
form.textbox("Email")
|
|
65
|
+
form.button("cmdClose", caption="Close", on_click=Vba("DoCmd.Close acForm, Me.Name"))
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
When the `with` block ends, the database file appears at `crm.accdb`, and any Access process PyAccessKit
|
|
69
|
+
started is closed. If the block raises, no file is created and nothing is left running.
|
|
70
|
+
|
|
71
|
+
> **Status:** alpha (`0.1.0`). The API may still change between 0.x releases.
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```console
|
|
76
|
+
pip install pyaccesskit
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Requirements:
|
|
80
|
+
|
|
81
|
+
- Windows 10 or 11 and CPython 3.11–3.14 (64- or 32-bit; free-threaded builds are not supported).
|
|
82
|
+
- Microsoft Access 2016 or later, including Microsoft 365. Alternatively, the Microsoft 365 Access Runtime
|
|
83
|
+
with a Python of the same bitness, for schema and data work only.
|
|
84
|
+
|
|
85
|
+
Check your machine with:
|
|
86
|
+
|
|
87
|
+
```console
|
|
88
|
+
pyaccesskit doctor # which engines work here, and why not
|
|
89
|
+
pyaccesskit doctor --probe # also builds a scratch database end to end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The spec models (`TableSpec`, `Column`, `FormSpec`, units, layout…) are pure Python and also import on
|
|
93
|
+
Linux and macOS, so you can validate specs in CI without Access.
|
|
94
|
+
|
|
95
|
+
## What you can do in 0.1
|
|
96
|
+
|
|
97
|
+
| Area | Highlights | Stability |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| Lifecycle | Atomic `create()`, `open()` (read-only/shared, exclusive, password), guaranteed cleanup, `db.raw` escape hatch | stable-track |
|
|
100
|
+
| Tables | Every common column type, defaults as Python values or `Expr`, validation rules, captions, formats, input masks; indexes (composite, descending, unique, primary); add, drop and rename columns and indexes; `to_spec()` round-trips | stable-track |
|
|
101
|
+
| Relationships | Single and composite keys, referential integrity, cascades, join types, all checked before Access sees them | stable-track |
|
|
102
|
+
| Queries & data | Saved queries (select, action, union, crosstab, pass-through), parameters bound by name, `execute()` / `fetch_all()` | stable-track |
|
|
103
|
+
| Forms | Labels, text boxes, check boxes, combo boxes and buttons; stacked or tabular layout; VBA event procedures; atomic build-then-swap replacement | provisional |
|
|
104
|
+
| Modules & text I/O | Standard and class modules; `SaveAsText`/`LoadFromText` for forms, reports, macros, queries and modules, stored as UTF-8 | provisional |
|
|
105
|
+
| Decimal columns | `Column.decimal(precision=…, scale=…)` via ADO DDL | provisional |
|
|
106
|
+
|
|
107
|
+
Reports, linked tables, VBA references, and the declarative `build`/`plan`/`apply` workflow are planned
|
|
108
|
+
(see the [roadmap](https://ariedotcodotnz.github.io/PyAccessKit/#roadmap)).
|
|
109
|
+
|
|
110
|
+
## Engines
|
|
111
|
+
|
|
112
|
+
PyAccessKit reaches the database in one of three ways and picks one for you (`engine="auto"`):
|
|
113
|
+
|
|
114
|
+
| Engine | When | Notes |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| In-process DAO | Python and Office have the **same bitness** | Fastest; never starts `MSACCESS.EXE`; enough for tables, relationships, queries and data |
|
|
117
|
+
| Access-hosted DAO | Bitness differs, or `engine="access"` | A hidden Access instance that PyAccessKit owns hosts DAO; the database is **not** opened in the Access UI, so no startup code runs |
|
|
118
|
+
| Design session | First use of forms, modules or text I/O | The database becomes Access's current database; handles such as `db.tables["X"]` keep working across the switch |
|
|
119
|
+
|
|
120
|
+
| Python | Office | In-process DAO | Access transports |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| 64-bit | 64-bit | ✅ | ✅ |
|
|
123
|
+
| 32-bit | 32-bit | ✅ | ✅ |
|
|
124
|
+
| 64-bit | 32-bit (common with Microsoft 365) | ❌ | ✅ |
|
|
125
|
+
| 32-bit | 64-bit | ❌ | ✅ |
|
|
126
|
+
|
|
127
|
+
`pyaccesskit doctor` explains what applies to your machine.
|
|
128
|
+
|
|
129
|
+
## Safety guarantees
|
|
130
|
+
|
|
131
|
+
- **Never touches your own Access windows.** PyAccessKit always starts a *new* Access instance
|
|
132
|
+
(`CoCreateInstanceEx`, local server). It never attaches to a running Access (which `Dispatch()` does),
|
|
133
|
+
so it can never close your work.
|
|
134
|
+
- **No orphaned `MSACCESS.EXE`.** Every Access process it starts is identified by PID, creation time and
|
|
135
|
+
image, and placed in a kill-on-close job object. If Python crashes, Windows ends the process. The process
|
|
136
|
+
is also recorded in an ownership ledger: `pyaccesskit cleanup` ends leftovers from crashed sessions, and
|
|
137
|
+
only when their Python owner is gone. Processes it did not start are never touched.
|
|
138
|
+
- **Exceptions clean up too.** Closing runs every cleanup step even if one fails. Cleanup failures are
|
|
139
|
+
attached as notes to the exception that caused the close, never masking it.
|
|
140
|
+
- **Atomic creation.** `AccessDatabase.create()` builds in a hidden sibling file and moves it into place
|
|
141
|
+
only on success. Tables are created all-or-nothing, and forms are built under a temporary name and
|
|
142
|
+
swapped in.
|
|
143
|
+
- **No hangs on hidden dialogs.** A watchdog watches the windows of *our* Access process. Unexpected
|
|
144
|
+
dialogs are dismissed and reported as `AccessDialogError` with their text. Calls that exceed
|
|
145
|
+
`call_timeout` end the owned process, and Ctrl+C works during long calls.
|
|
146
|
+
- **No startup code by default.** Databases are opened with macros disabled. Read-only sessions never open
|
|
147
|
+
the database in the Access UI, so `AutoExec` and startup forms do not run.
|
|
148
|
+
|
|
149
|
+
## Errors you can act on
|
|
150
|
+
|
|
151
|
+
COM errors are translated into specific exceptions. Each one carries what PyAccessKit was doing, the
|
|
152
|
+
object involved, and the original Access or DAO error:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from pyaccesskit import AccessDatabase, IntegrityViolationError
|
|
156
|
+
|
|
157
|
+
with AccessDatabase.open("crm.accdb") as db:
|
|
158
|
+
try:
|
|
159
|
+
db.execute("INSERT INTO Orders (CustomerID, Total) VALUES (999, 10)")
|
|
160
|
+
except IntegrityViolationError as exc:
|
|
161
|
+
print(exc) # what failed, with Access's own explanation
|
|
162
|
+
print(exc.details.number) # 3201: there is no related record in Customers
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Most mistakes are caught before Access is involved at all. A duplicate table name, an unknown column in a
|
|
166
|
+
relationship, or incompatible key types each raise a precise `ObjectExistsError`, `SpecError` or
|
|
167
|
+
`RelationshipError`.
|
|
168
|
+
|
|
169
|
+
## Command line
|
|
170
|
+
|
|
171
|
+
```console
|
|
172
|
+
pyaccesskit doctor [--json] [--probe] # environment report (exit code 3 if nothing works)
|
|
173
|
+
pyaccesskit inspect DB [--json] [--counts] # tables, relationships, queries, objects; read-only
|
|
174
|
+
pyaccesskit cleanup [--dry-run] [--json] # end orphaned Access processes started by PyAccessKit
|
|
175
|
+
pyaccesskit guide [--path] # the guide for AI coding agents
|
|
176
|
+
pyaccesskit schema [KIND] # JSON Schema of the specs
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Exit codes: `0` success, `1` error, `2` usage error, `3` environment unusable.
|
|
180
|
+
|
|
181
|
+
## Documentation
|
|
182
|
+
|
|
183
|
+
- [Getting started](https://ariedotcodotnz.github.io/PyAccessKit/getting-started/) and [recipes](https://ariedotcodotnz.github.io/PyAccessKit/guides/recipes/)
|
|
184
|
+
- **[Building with AI agents](https://ariedotcodotnz.github.io/PyAccessKit/agents/)**: `pyaccesskit guide` prints a version-matched guide
|
|
185
|
+
that lets coding agents write Access applications with PyAccessKit; `pyaccesskit schema` prints the
|
|
186
|
+
JSON Schemas of the specs; the site publishes `llms.txt` and `llms-full.txt`.
|
|
187
|
+
- Concepts: [engines](https://ariedotcodotnz.github.io/PyAccessKit/concepts/engines/), [lifecycle](https://ariedotcodotnz.github.io/PyAccessKit/concepts/lifecycle/),
|
|
188
|
+
[specs](https://ariedotcodotnz.github.io/PyAccessKit/concepts/specs/)
|
|
189
|
+
- Guides: [tables](https://ariedotcodotnz.github.io/PyAccessKit/guides/tables/), [relationships](https://ariedotcodotnz.github.io/PyAccessKit/guides/relationships/),
|
|
190
|
+
[queries](https://ariedotcodotnz.github.io/PyAccessKit/guides/queries/), [forms](https://ariedotcodotnz.github.io/PyAccessKit/guides/forms/), [modules](https://ariedotcodotnz.github.io/PyAccessKit/guides/modules/),
|
|
191
|
+
[text I/O](https://ariedotcodotnz.github.io/PyAccessKit/guides/text-io/)
|
|
192
|
+
- Reference: [errors](https://ariedotcodotnz.github.io/PyAccessKit/reference/errors/), [data types](https://ariedotcodotnz.github.io/PyAccessKit/reference/data-types/),
|
|
193
|
+
[command line](https://ariedotcodotnz.github.io/PyAccessKit/reference/cli/), and an API reference generated from the docstrings
|
|
194
|
+
- [Environment & troubleshooting](https://ariedotcodotnz.github.io/PyAccessKit/environment/), [FAQ](https://ariedotcodotnz.github.io/PyAccessKit/faq/),
|
|
195
|
+
[contributing](https://ariedotcodotnz.github.io/PyAccessKit/contributing/), [architecture decisions](https://github.com/ariedotcodotnz/PyAccessKit/tree/HEAD/docs/adr)
|
|
196
|
+
- Examples: [`examples/`](https://github.com/ariedotcodotnz/PyAccessKit/tree/HEAD/examples) (`04_inventory_app.py` is the complete, tested application the agent
|
|
197
|
+
guide walks through)
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT. See [LICENSE](https://github.com/ariedotcodotnz/PyAccessKit/blob/HEAD/LICENSE).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
pyaccesskit/AGENT_GUIDE.md,sha256=aoLu67-zUau0vFG5o_98ZFnnVLeIOKSlmysuG_033m8,23262
|
|
2
|
+
pyaccesskit/__init__.py,sha256=mWCH8BYs6BCaIyOy0I52j6UAmtaJRLeB4YupitMVdoQ,3714
|
|
3
|
+
pyaccesskit/__main__.py,sha256=-wO3_oGdJ1BGc_y9RMNWRX1MOPhFqZcY-fCWgXu1cZU,138
|
|
4
|
+
pyaccesskit/_ledger.py,sha256=9q3UKnn_xC5qQ5TgyJw1CExByqieEGY22sIA4-ne0h4,5472
|
|
5
|
+
pyaccesskit/_version.py,sha256=Y_2bqYDoRNq0PnhUrLbcwDTa_O-tiNwjoBWEyVHCbyk,97
|
|
6
|
+
pyaccesskit/database.py,sha256=6iSzKwTWcfhYB6xtCkCl8Xguvvfv4apa-6F1aCfROIE,11135
|
|
7
|
+
pyaccesskit/diagnostics.py,sha256=uloHujGHFtKH8v-1F-pHu09JGy11cb54Z3pL1u87fQQ,11372
|
|
8
|
+
pyaccesskit/enums.py,sha256=VPmMAv1stpjOEXLMScQL7t4AOXk4EjSI1DLe7bw6NWw,7405
|
|
9
|
+
pyaccesskit/errors.py,sha256=rN3rTHE2svxomhX33WrCJ9sB30gxv51-RtwcGYXMsGU,13220
|
|
10
|
+
pyaccesskit/maintenance.py,sha256=I2s7OMT-ZgXIJAMnZC3_TwZxZlBPWj80LlbhCavtZiQ,1267
|
|
11
|
+
pyaccesskit/modules.py,sha256=nEvOq96BRZ1l-t5KS1I8zBJRl5dqd4_WUWNPj1IL2rE,3670
|
|
12
|
+
pyaccesskit/objects.py,sha256=5FnK5CZWDBx79zWYecNK4R8FXrTXMjlTU6bLB3erQGo,3242
|
|
13
|
+
pyaccesskit/options.py,sha256=zcm_yezWvdbVmVN-RfjdjQlHi6cab-XGdhp_qb2nzOY,1871
|
|
14
|
+
pyaccesskit/properties.py,sha256=EWEExLazI8SMuOCTJHea_7LAtJnRlFWCQSvmhOBrCqg,2820
|
|
15
|
+
pyaccesskit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
pyaccesskit/queries.py,sha256=YGBF5i_ykKZMWJfKfQqeZsUp-0KOtA5uf7-2assqi_s,6629
|
|
17
|
+
pyaccesskit/relationships.py,sha256=-7gW36qpvQCYgTkFevlu7b8ZzqVevXZRaDMaXvulQAg,4754
|
|
18
|
+
pyaccesskit/tables.py,sha256=eWNsJj6MJdvBBLt2T-Fy8UDtKHEU2tqRYF7I6eZ4w-A,11619
|
|
19
|
+
pyaccesskit/units.py,sha256=GNN6qcPspZEjGlrSK7A5-7A2wH1gojt2mxUWAfcxb7Y,10125
|
|
20
|
+
pyaccesskit/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
pyaccesskit/_backends/protocols.py,sha256=-dmQTGpvyZ2gD78-FK_3KN-7CoqDEulo-HGLkpgzNSw,10475
|
|
22
|
+
pyaccesskit/_backends/access/__init__.py,sha256=XoP3TE17S_WWjVhanyMrmZ1Wnj6nUuH2Uk-2oYst7BA,72
|
|
23
|
+
pyaccesskit/_backends/access/design.py,sha256=_MMXPz-IiaVdA-Kdr_M_uHdmo92qnLp-uBX5LlWVUOA,17368
|
|
24
|
+
pyaccesskit/_backends/dao/__init__.py,sha256=pdzTRkF-LVtPiuGfRJBl4zAs8eZiweby26Tteg9fvJo,57
|
|
25
|
+
pyaccesskit/_backends/dao/profile.py,sha256=hHxKK_lnN58a96_qZoA_9ZwEUylrBGciQNboEc7bwUc,1640
|
|
26
|
+
pyaccesskit/_backends/dao/schema.py,sha256=vWe892shAl5HSF7MrCnHoWlBrD3fNC3Qfpa_ybAY6Ck,33780
|
|
27
|
+
pyaccesskit/_backends/dao/typemap.py,sha256=KaGCKcD3ZlcBh7atIaPvkgcchIjSdibi4DrunDZfT8o,12918
|
|
28
|
+
pyaccesskit/_backends/fake/__init__.py,sha256=VDcYa1B2vZAWMnoJW9qG6di9rMYy_8j5MJZGkUx9zOU,86
|
|
29
|
+
pyaccesskit/_backends/fake/backend.py,sha256=rI39xSbgc_kC-XvGLO80cYati-aibi64gMOd3pGsIc0,27386
|
|
30
|
+
pyaccesskit/_com/__init__.py,sha256=2yeD-cfAEkAQtCJCFIuH-OkjfHV3ehT8U892SwS0LU4,89
|
|
31
|
+
pyaccesskit/_com/constants.py,sha256=PrTiwyb_Jd-LpL4qpkjzE05Z6B9ou3b6UlZiZqhjEwk,10145
|
|
32
|
+
pyaccesskit/_com/dispatch.py,sha256=lGblOMRuLM4KlZ0Qmyick8-jhviIRZHjo1gsEdvPwow,2054
|
|
33
|
+
pyaccesskit/_com/errors.py,sha256=uu4ihD7JbZubK0AW52hs3usp-9J2yC6MrZqL4rrikzw,6747
|
|
34
|
+
pyaccesskit/_com/gateway.py,sha256=N6KThEiPnge8OWOPQ_i_jQKNkBB961IdCCMycSZTnKg,7209
|
|
35
|
+
pyaccesskit/_com/raw.py,sha256=xj-Uxqnw9JyE1FWphJH3pP_3B0YAYPK7lz2lEKPzw8s,6472
|
|
36
|
+
pyaccesskit/_com/runtime.py,sha256=Oulp7DBjGOXBaWy2YtYAXIpBkR2FjKiRc10OxTspFEc,1365
|
|
37
|
+
pyaccesskit/_com/variants.py,sha256=CbgD9HSbc1Vr5CkwYyWWfIvEgmq8YlLBWLG16p5_fGw,2606
|
|
38
|
+
pyaccesskit/_engines/__init__.py,sha256=EEwx_0Z3Efbk7Tj7aGruRSx5e2sw5ZvmhkxTKVCHuAo,1539
|
|
39
|
+
pyaccesskit/_engines/access.py,sha256=XH0wjqYUTYL4_nB7Xf8nGGwlWZbXttZPzv8ygq0nVEc,11536
|
|
40
|
+
pyaccesskit/_engines/inproc.py,sha256=vbDYUjE93E6G4g2_8JMPY4iehg3cruO888Uc44qeXHw,5632
|
|
41
|
+
pyaccesskit/_engines/probe.py,sha256=VhmFQCRgRTIGErUrFVg9xdL47W0TWPuuiaWkufFYWg8,7800
|
|
42
|
+
pyaccesskit/_ops/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
|
+
pyaccesskit/_ops/design.py,sha256=3FQV_q6KXvG-2qtp1_7gEteNmGCnfa8wojwJi_sfU2Y,5261
|
|
44
|
+
pyaccesskit/_ops/schema.py,sha256=w1JaKpKgGLrqK3y9sfa2Px_p849Jq5dhADp2ZfdpKO8,18765
|
|
45
|
+
pyaccesskit/_session/__init__.py,sha256=COEPkJJZ211VBdtL1hKrSBjpQw4aHABRzJig0u61HEk,37
|
|
46
|
+
pyaccesskit/_session/protocols.py,sha256=Rmra7Q40_ifzaXzTHG6LZ9Om2r0GFFTcNqtSRJQ0JG8,2281
|
|
47
|
+
pyaccesskit/_session/session.py,sha256=QfNJl3FXEX_xkIzvFqjlBxY3412_E_tQafyOHtmA57M,13910
|
|
48
|
+
pyaccesskit/_text/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
|
+
pyaccesskit/_text/codec.py,sha256=mtNijZTAkLb_N1p-dWs9oPJ0j_Pru4DP9Hs9fc6VtiY,4542
|
|
50
|
+
pyaccesskit/_win/__init__.py,sha256=fVmdNR3LqQ4yO5uXmMMqpNse2y8Doh1HerNESLFRvaI,74
|
|
51
|
+
pyaccesskit/_win/access_process.py,sha256=PyNNgJciGvyS4M_DeOfbtnW1M6_3j--0DYl0W6X6Mww,13918
|
|
52
|
+
pyaccesskit/_win/console.py,sha256=aJNJdSr5QGVvK-goiFRzEockVeodsPTQ9IkHACNuUjs,1987
|
|
53
|
+
pyaccesskit/_win/inspector.py,sha256=iCpo8gpO_f7g0Xhl3NgIds_JnSjT449BmU9HqtwQLZY,1654
|
|
54
|
+
pyaccesskit/_win/job.py,sha256=lSORBjfLv5DvsPFCC9VsD3COkSJ6gOXqgjAzdbNHXEE,2419
|
|
55
|
+
pyaccesskit/_win/processes.py,sha256=RCrQtuyOVl8kStgXPsMVWnMeLLRJN7WN8OSvfygcg2k,5087
|
|
56
|
+
pyaccesskit/_win/watchdog.py,sha256=fdD1gpWnlgK-EJ_KnkGsC5h_JuTPwV6_6-g8FhHwrIs,10176
|
|
57
|
+
pyaccesskit/cli/__init__.py,sha256=UYgcIpZjVC72lvjjTrFpORxIo2aGcelRxGxMQ6XdOyk,319
|
|
58
|
+
pyaccesskit/cli/_output.py,sha256=zOB2OqiLRghfQLy7jAkmKNMTqDVQlO38M4MzX0rqd-U,3170
|
|
59
|
+
pyaccesskit/cli/agent.py,sha256=JTLOFjfoR-IDXyqEi1QncTFieuLvCGpKleNx1FnLZNY,3012
|
|
60
|
+
pyaccesskit/cli/app.py,sha256=EEF0ETLx9aKCaL4ELMc9ecsrTeONw-2GJUhY2DOgs7I,1435
|
|
61
|
+
pyaccesskit/cli/cleanup.py,sha256=6lNbgp3BvOe7dvBpemaf_36rlfQfL3sgpmLCTaBSqWo,2127
|
|
62
|
+
pyaccesskit/cli/doctor.py,sha256=OUyE7evPd6RBMl4i4SzFoQGS_iPkuuG5CA5HzqbqU-w,3956
|
|
63
|
+
pyaccesskit/cli/inspection.py,sha256=qnghAMfDkyyXxc5Ghsrtdy1G6Aq14l1t78ONeVkrF6o,8812
|
|
64
|
+
pyaccesskit/forms/__init__.py,sha256=UvUDMCrJK5YFdEi5Le18nTDS3tyHIsJlTmtzWuOL2RE,967
|
|
65
|
+
pyaccesskit/forms/builder.py,sha256=IVHn6g3ERgabLpVbELfplG2BCWipm4_14FpYQKWRT0k,10135
|
|
66
|
+
pyaccesskit/forms/collection.py,sha256=bR09O_C5y1McwHOGNYvv-F-tkTPdAdideyxStkHPKTo,4427
|
|
67
|
+
pyaccesskit/forms/controls.py,sha256=PnmqgffPL0ruTpiI_dQTFnaQOop0L0uP87ME67ivVIU,5330
|
|
68
|
+
pyaccesskit/forms/layout.py,sha256=7IshbmwQ-d7Wh-4fNTjlKJnbiL5DQ1lwPv7NzbIFmwg,10892
|
|
69
|
+
pyaccesskit/forms/spec.py,sha256=LoamddafCn_Do3B71u-x-2C0HDQR4F82Jm-JlmGRCh0,7170
|
|
70
|
+
pyaccesskit/forms/vba.py,sha256=XCvNZ2QmEgYJ-KZjBzoJj1VJElCzfDaBIYi6C8k1hw8,4928
|
|
71
|
+
pyaccesskit/schema/__init__.py,sha256=uerme7BX7U3obaLBHyt5iYHrb8oLH-_lUJUZegxHh0s,1878
|
|
72
|
+
pyaccesskit/schema/_base.py,sha256=lEKSozfcCcq8ZU0YEQv9ZgvyrXfXFWUTTZTU1gACcEw,2070
|
|
73
|
+
pyaccesskit/schema/_reserved_words.py,sha256=49auHhwSN7113MHDYtIVt40JW3b3y_nZ0BOPIURVqa0,4308
|
|
74
|
+
pyaccesskit/schema/columns.py,sha256=v8VwVxeE3Eo7_o7f3Pdkt9-8oRj72gko30mtqpzmXFE,20979
|
|
75
|
+
pyaccesskit/schema/compat.py,sha256=euH8zFW8fU1eguOPnfv2wPbk044Av4mOqefAJEBgDN0,1829
|
|
76
|
+
pyaccesskit/schema/expressions.py,sha256=83RIyxqaN9QVp3UOw-mhc9JRmrm8oydcC4yz3_NgUPE,5859
|
|
77
|
+
pyaccesskit/schema/indexes.py,sha256=eWMrHmM6yPoB6sQz4zG-8PDk8-aPuyf3wY6JDuOon58,3964
|
|
78
|
+
pyaccesskit/schema/names.py,sha256=FDPxz-bOyzG2oL3ny5cy0-5xKRiY9vQkhu0NGnQzKRM,4179
|
|
79
|
+
pyaccesskit/schema/queries.py,sha256=K-zGSnp0nk8KWaiu-rfAdIc8FgW6OOBKb_x5s7Bl4E4,6352
|
|
80
|
+
pyaccesskit/schema/relationships.py,sha256=S8VOkf-CqMd3CT5Kr7M6wzYOPU6i32pX8rb6XPu6Uhw,5632
|
|
81
|
+
pyaccesskit/schema/tables.py,sha256=HPr0b_PwoAq9wn4BL1NjcWDWDDk-khx8XoWfOg5ehNA,7344
|
|
82
|
+
pyaccesskit-0.1.0.dist-info/METADATA,sha256=3YoLUOx13Cgq-rwvQQJnsmqpk0K9AuqXGy1nf9wlMK8,11165
|
|
83
|
+
pyaccesskit-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
84
|
+
pyaccesskit-0.1.0.dist-info/entry_points.txt,sha256=uej_q9QiApCGzSHB_jdp8YsIEGZfVmeAMomSeH8bASo,53
|
|
85
|
+
pyaccesskit-0.1.0.dist-info/licenses/LICENSE,sha256=k6V4ZLGuzf_kqxWbvrJwgbHBIW52My0HV-aYARZJULw,1065
|
|
86
|
+
pyaccesskit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arie Joe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|