lora-python 0.2.0__cp38-abi3-macosx_10_12_x86_64.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.
@@ -0,0 +1,133 @@
1
+ """lora-python — typed Python bindings for the Lora graph engine.
2
+
3
+ Two public classes:
4
+
5
+ - ``Database`` : synchronous, PyO3-backed. Holds an in-memory graph and
6
+ runs queries with the GIL released.
7
+ - ``AsyncDatabase``: pure-Python asyncio wrapper. Delegates to a
8
+ background thread via ``asyncio.to_thread`` so the
9
+ event loop stays responsive during heavier queries.
10
+
11
+ Both classes share the same public result / value / param shapes (see
12
+ ``lora_python.types``) so switching between sync and async usage is a
13
+ method-name change, nothing else.
14
+
15
+ Example
16
+ -------
17
+ >>> from lora_python import Database
18
+ >>> db = Database.create()
19
+ >>> db.execute("CREATE (:Person {name: $n})", {"n": "Alice"})
20
+ >>> r = db.execute("MATCH (n:Person) RETURN n.name AS name")
21
+ >>> r["rows"]
22
+ [{'name': 'Alice'}]
23
+
24
+ Async:
25
+
26
+ >>> import asyncio
27
+ >>> from lora_python import AsyncDatabase
28
+ >>> async def main():
29
+ ... db = await AsyncDatabase.create()
30
+ ... await db.execute("CREATE (:Person {name: 'Alice'})")
31
+ ... return await db.execute("MATCH (n:Person) RETURN n.name AS name")
32
+ >>> asyncio.run(main())["rows"]
33
+ [{'name': 'Alice'}]
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ from ._native import (
39
+ Database,
40
+ LoraError,
41
+ LoraQueryError,
42
+ InvalidParamsError,
43
+ __version__,
44
+ )
45
+ from ._async import AsyncDatabase
46
+ from . import types
47
+ from .types import (
48
+ LoraParam,
49
+ LoraParams,
50
+ LoraValue,
51
+ QueryResult,
52
+ LoraNode,
53
+ LoraRelationship,
54
+ LoraPath,
55
+ LoraDate,
56
+ LoraTime,
57
+ LoraLocalTime,
58
+ LoraDateTime,
59
+ LoraLocalDateTime,
60
+ LoraDuration,
61
+ LoraPoint,
62
+ LoraCartesianPoint,
63
+ LoraCartesianPoint3D,
64
+ LoraWgs84Point,
65
+ LoraWgs84Point3D,
66
+ LoraVector,
67
+ LoraVectorCoordinateType,
68
+ date,
69
+ time,
70
+ localtime,
71
+ datetime,
72
+ localdatetime,
73
+ duration,
74
+ cartesian,
75
+ cartesian_3d,
76
+ wgs84,
77
+ wgs84_3d,
78
+ vector,
79
+ is_node,
80
+ is_relationship,
81
+ is_path,
82
+ is_point,
83
+ is_temporal,
84
+ is_vector,
85
+ )
86
+
87
+ __all__ = [
88
+ "Database",
89
+ "AsyncDatabase",
90
+ "LoraError",
91
+ "LoraQueryError",
92
+ "InvalidParamsError",
93
+ "__version__",
94
+ "types",
95
+ # re-exports
96
+ "LoraParam",
97
+ "LoraParams",
98
+ "LoraValue",
99
+ "QueryResult",
100
+ "LoraNode",
101
+ "LoraRelationship",
102
+ "LoraPath",
103
+ "LoraDate",
104
+ "LoraTime",
105
+ "LoraLocalTime",
106
+ "LoraDateTime",
107
+ "LoraLocalDateTime",
108
+ "LoraDuration",
109
+ "LoraPoint",
110
+ "LoraCartesianPoint",
111
+ "LoraCartesianPoint3D",
112
+ "LoraWgs84Point",
113
+ "LoraWgs84Point3D",
114
+ "LoraVector",
115
+ "LoraVectorCoordinateType",
116
+ "date",
117
+ "time",
118
+ "localtime",
119
+ "datetime",
120
+ "localdatetime",
121
+ "duration",
122
+ "cartesian",
123
+ "cartesian_3d",
124
+ "wgs84",
125
+ "wgs84_3d",
126
+ "vector",
127
+ "is_node",
128
+ "is_relationship",
129
+ "is_path",
130
+ "is_point",
131
+ "is_temporal",
132
+ "is_vector",
133
+ ]
lora_python/_async.py ADDED
@@ -0,0 +1,113 @@
1
+ """Async-compatible Database wrapper.
2
+
3
+ The PyO3 ``Database`` is synchronous — the engine itself is synchronous
4
+ Rust — but it releases the GIL while running a query, which means the
5
+ heavy work can safely be hoisted off the asyncio event-loop thread.
6
+
7
+ ``AsyncDatabase`` does exactly that: each ``await db.execute(...)`` call
8
+ dispatches the sync ``Database.execute`` onto a worker thread via
9
+ ``asyncio.to_thread`` on Python 3.9+, or the equivalent
10
+ ``loop.run_in_executor`` polyfill on 3.8. The event loop stays free to
11
+ service other coroutines while the engine runs.
12
+
13
+ This is the pragmatic, well-understood pattern for async-wrapping a
14
+ CPU-bound Rust function in Python. It requires no unsafe lifetime
15
+ juggling and stays trivially debuggable.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import contextvars
22
+ import functools
23
+ import sys
24
+ from typing import Any, Callable, Mapping, Optional, TypeVar
25
+
26
+ from ._native import Database as _Database
27
+ from .types import LoraParams, QueryResult
28
+
29
+ _T = TypeVar("_T")
30
+
31
+
32
+ # `asyncio.to_thread` landed in Python 3.9 (bpo-32309). Provide a direct
33
+ # equivalent on 3.8 so the non-blocking behaviour is identical: dispatch
34
+ # the call onto the running loop's default executor with the current
35
+ # context copied over, just like the CPython implementation.
36
+ if sys.version_info >= (3, 9):
37
+ _to_thread = asyncio.to_thread # type: ignore[attr-defined]
38
+ else: # pragma: no cover — exercised in the 3.8 CI leg
39
+
40
+ async def _to_thread(
41
+ func: Callable[..., _T], /, *args: Any, **kwargs: Any
42
+ ) -> _T:
43
+ loop = asyncio.get_running_loop()
44
+ ctx = contextvars.copy_context()
45
+ return await loop.run_in_executor(
46
+ None, functools.partial(ctx.run, func, *args, **kwargs)
47
+ )
48
+
49
+
50
+ class AsyncDatabase:
51
+ """asyncio-compatible handle to an in-memory Lora database.
52
+
53
+ All methods delegate to the sync ``Database`` on a worker thread so
54
+ the event loop is never blocked by engine work. Methods are coroutines
55
+ so normal async usage looks like::
56
+
57
+ db = await AsyncDatabase.create()
58
+ result = await db.execute("MATCH (n) RETURN n")
59
+
60
+ Concurrency: a single ``AsyncDatabase`` wraps a single ``Database``;
61
+ concurrent ``execute`` coroutines serialise on the underlying engine's
62
+ mutex but do not block the event loop while waiting.
63
+ """
64
+
65
+ __slots__ = ("_inner",)
66
+
67
+ def __init__(self, inner: _Database) -> None:
68
+ self._inner = inner
69
+
70
+ @classmethod
71
+ async def create(cls) -> "AsyncDatabase":
72
+ """Construct a fresh in-memory database. Async for API symmetry."""
73
+ # Construction is cheap (Arc::new + Mutex::new) so we stay on-thread.
74
+ return cls(_Database())
75
+
76
+ async def execute(
77
+ self,
78
+ query: str,
79
+ params: Optional[Mapping[str, Any]] = None,
80
+ ) -> QueryResult:
81
+ """Run a Lora query on a background thread.
82
+
83
+ Returns ``{"columns": [...], "rows": [...]}``. Raises
84
+ ``LoraQueryError`` on engine failure or ``InvalidParamsError``
85
+ on a malformed parameter.
86
+ """
87
+ # The helper runs the callable on the loop's default
88
+ # ThreadPoolExecutor. Since Database.execute releases the GIL,
89
+ # other coroutines on the same event loop are free to progress.
90
+ return await _to_thread(
91
+ self._inner.execute,
92
+ query,
93
+ dict(params) if params is not None else None,
94
+ )
95
+
96
+ async def clear(self) -> None:
97
+ """Drop every node and relationship."""
98
+ await _to_thread(self._inner.clear)
99
+
100
+ @property
101
+ def node_count(self) -> int:
102
+ return self._inner.node_count
103
+
104
+ @property
105
+ def relationship_count(self) -> int:
106
+ return self._inner.relationship_count
107
+
108
+ def __repr__(self) -> str: # pragma: no cover — cosmetic
109
+ return (
110
+ f"<lora_python.AsyncDatabase "
111
+ f"nodes={self._inner.node_count} "
112
+ f"relationships={self._inner.relationship_count}>"
113
+ )
Binary file
@@ -0,0 +1,39 @@
1
+ """Type stubs for the PyO3 extension module.
2
+
3
+ Mirrors what the native ``_native`` module exposes. The high-level
4
+ ``lora_python`` package re-exports these with richer types from
5
+ ``lora_python.types``.
6
+ """
7
+
8
+ from typing import Any, Mapping, Optional
9
+
10
+ from .types import QueryResult
11
+
12
+ __version__: str
13
+
14
+ class LoraError(Exception):
15
+ """Base class for Lora engine errors."""
16
+
17
+ class LoraQueryError(LoraError):
18
+ """Parse / analyze / execute failure."""
19
+
20
+ class InvalidParamsError(LoraError):
21
+ """A parameter value could not be mapped to a Lora value."""
22
+
23
+ class Database:
24
+ """In-memory Lora graph database (sync, PyO3)."""
25
+
26
+ def __init__(self) -> None: ...
27
+ @staticmethod
28
+ def create() -> "Database": ...
29
+ def execute(
30
+ self,
31
+ query: str,
32
+ params: Optional[Mapping[str, Any]] = None,
33
+ ) -> QueryResult: ...
34
+ def clear(self) -> None: ...
35
+ @property
36
+ def node_count(self) -> int: ...
37
+ @property
38
+ def relationship_count(self) -> int: ...
39
+ def __repr__(self) -> str: ...
lora_python/py.typed ADDED
File without changes
lora_python/types.py ADDED
@@ -0,0 +1,342 @@
1
+ """Typed Python value model for lora-python.
2
+
3
+ Kept conceptually aligned with the shared TS contract used by
4
+ ``lora-node`` / ``lora-wasm`` (see ``crates/shared-ts/types.ts``).
5
+ Python uses pragmatic representations:
6
+
7
+ - Scalars pass through as Python natives (``None``, ``bool``, ``int``,
8
+ ``float``, ``str``).
9
+ - Lists and maps come back as ``list`` / ``dict``.
10
+ - Graph, temporal, and spatial values come back as ``TypedDict``s with
11
+ a ``kind`` discriminator. They're plain dicts at runtime — no class
12
+ wrappers to unpack — and narrow cleanly under ``typing.TYPE_CHECKING``.
13
+
14
+ If a caller wants structured objects, the ``is_node`` / ``is_temporal``
15
+ helpers make narrowing explicit.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, List, Literal, Mapping, TypedDict, Union
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Forward-declare the recursive value union.
24
+ # ---------------------------------------------------------------------------
25
+
26
+ # ``LoraValue`` is recursive; PEP 604 union syntax works on 3.10+, but we
27
+ # fall back to ``Union`` for 3.8/3.9 support.
28
+ LoraValue = Union[
29
+ None,
30
+ bool,
31
+ int,
32
+ float,
33
+ str,
34
+ List["LoraValue"],
35
+ Mapping[str, "LoraValue"],
36
+ "LoraNode",
37
+ "LoraRelationship",
38
+ "LoraPath",
39
+ "LoraDate",
40
+ "LoraTime",
41
+ "LoraLocalTime",
42
+ "LoraDateTime",
43
+ "LoraLocalDateTime",
44
+ "LoraDuration",
45
+ "LoraPoint",
46
+ "LoraVector",
47
+ ]
48
+
49
+ LoraParam = Union[
50
+ None,
51
+ bool,
52
+ int,
53
+ float,
54
+ str,
55
+ List["LoraParam"],
56
+ Mapping[str, "LoraParam"],
57
+ "LoraDate",
58
+ "LoraTime",
59
+ "LoraLocalTime",
60
+ "LoraDateTime",
61
+ "LoraLocalDateTime",
62
+ "LoraDuration",
63
+ "LoraPoint",
64
+ "LoraVector",
65
+ ]
66
+
67
+ LoraParams = Mapping[str, LoraParam]
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Structural values — TypedDicts with a `kind` discriminator.
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ class LoraNode(TypedDict):
75
+ kind: Literal["node"]
76
+ id: int
77
+ labels: List[str]
78
+ properties: Mapping[str, LoraValue]
79
+
80
+
81
+ class LoraRelationship(TypedDict):
82
+ kind: Literal["relationship"]
83
+ id: int
84
+ startId: int
85
+ endId: int
86
+ type: str
87
+ properties: Mapping[str, LoraValue]
88
+
89
+
90
+ class LoraPath(TypedDict):
91
+ kind: Literal["path"]
92
+ nodes: List[int]
93
+ rels: List[int]
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Temporal — ISO-8601 tagged.
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ class LoraDate(TypedDict):
102
+ kind: Literal["date"]
103
+ iso: str
104
+
105
+
106
+ class LoraTime(TypedDict):
107
+ kind: Literal["time"]
108
+ iso: str
109
+
110
+
111
+ class LoraLocalTime(TypedDict):
112
+ kind: Literal["localtime"]
113
+ iso: str
114
+
115
+
116
+ class LoraDateTime(TypedDict):
117
+ kind: Literal["datetime"]
118
+ iso: str
119
+
120
+
121
+ class LoraLocalDateTime(TypedDict):
122
+ kind: Literal["localdatetime"]
123
+ iso: str
124
+
125
+
126
+ class LoraDuration(TypedDict):
127
+ kind: Literal["duration"]
128
+ iso: str
129
+
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # Spatial
133
+ # ---------------------------------------------------------------------------
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Point variants.
138
+ #
139
+ # Python's ``TypedDict`` cannot model "required only on some variants" the
140
+ # way a TS discriminated union can, so we declare one dict per CRS and
141
+ # union them into ``LoraPoint``. Narrow via ``is_point`` + ``srid`` / ``crs``.
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ class LoraCartesianPoint(TypedDict):
146
+ kind: Literal["point"]
147
+ srid: Literal[7203]
148
+ crs: Literal["cartesian"]
149
+ x: float
150
+ y: float
151
+
152
+
153
+ class LoraCartesianPoint3D(TypedDict):
154
+ kind: Literal["point"]
155
+ srid: Literal[9157]
156
+ crs: Literal["cartesian-3D"]
157
+ x: float
158
+ y: float
159
+ z: float
160
+
161
+
162
+ class LoraWgs84Point(TypedDict):
163
+ kind: Literal["point"]
164
+ srid: Literal[4326]
165
+ crs: Literal["WGS-84-2D"]
166
+ x: float
167
+ y: float
168
+ longitude: float
169
+ latitude: float
170
+
171
+
172
+ class LoraWgs84Point3D(TypedDict):
173
+ kind: Literal["point"]
174
+ srid: Literal[4979]
175
+ crs: Literal["WGS-84-3D"]
176
+ x: float
177
+ y: float
178
+ z: float
179
+ longitude: float
180
+ latitude: float
181
+ height: float
182
+
183
+
184
+ LoraPoint = Union[
185
+ LoraCartesianPoint,
186
+ LoraCartesianPoint3D,
187
+ LoraWgs84Point,
188
+ LoraWgs84Point3D,
189
+ ]
190
+
191
+
192
+ # ---------------------------------------------------------------------------
193
+ # Vector
194
+ # ---------------------------------------------------------------------------
195
+
196
+
197
+ LoraVectorCoordinateType = Literal[
198
+ "FLOAT64", "FLOAT32", "INTEGER", "INTEGER32", "INTEGER16", "INTEGER8"
199
+ ]
200
+
201
+
202
+ class LoraVector(TypedDict):
203
+ kind: Literal["vector"]
204
+ dimension: int
205
+ coordinateType: LoraVectorCoordinateType
206
+ values: List[float]
207
+
208
+
209
+ # ---------------------------------------------------------------------------
210
+ # Query result
211
+ # ---------------------------------------------------------------------------
212
+
213
+
214
+ class QueryResult(TypedDict):
215
+ columns: List[str]
216
+ rows: List[Mapping[str, LoraValue]]
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # Constructors for param-side temporal/spatial values.
221
+ # ---------------------------------------------------------------------------
222
+
223
+
224
+ def date(iso: str) -> LoraDate:
225
+ return {"kind": "date", "iso": iso}
226
+
227
+
228
+ def time(iso: str) -> LoraTime:
229
+ return {"kind": "time", "iso": iso}
230
+
231
+
232
+ def localtime(iso: str) -> LoraLocalTime:
233
+ return {"kind": "localtime", "iso": iso}
234
+
235
+
236
+ def datetime(iso: str) -> LoraDateTime:
237
+ return {"kind": "datetime", "iso": iso}
238
+
239
+
240
+ def localdatetime(iso: str) -> LoraLocalDateTime:
241
+ return {"kind": "localdatetime", "iso": iso}
242
+
243
+
244
+ def duration(iso: str) -> LoraDuration:
245
+ return {"kind": "duration", "iso": iso}
246
+
247
+
248
+ def vector(
249
+ values: List[float],
250
+ dimension: int,
251
+ coordinate_type: LoraVectorCoordinateType,
252
+ ) -> LoraVector:
253
+ """Build a LoraVector param/value in the canonical tagged shape."""
254
+ return {
255
+ "kind": "vector",
256
+ "dimension": dimension,
257
+ "coordinateType": coordinate_type,
258
+ "values": list(values),
259
+ }
260
+
261
+
262
+ def cartesian(x: float, y: float) -> LoraCartesianPoint:
263
+ return {"kind": "point", "srid": 7203, "crs": "cartesian", "x": x, "y": y}
264
+
265
+
266
+ def cartesian_3d(x: float, y: float, z: float) -> LoraCartesianPoint3D:
267
+ return {
268
+ "kind": "point",
269
+ "srid": 9157,
270
+ "crs": "cartesian-3D",
271
+ "x": x,
272
+ "y": y,
273
+ "z": z,
274
+ }
275
+
276
+
277
+ def wgs84(longitude: float, latitude: float) -> LoraWgs84Point:
278
+ return {
279
+ "kind": "point",
280
+ "srid": 4326,
281
+ "crs": "WGS-84-2D",
282
+ "x": longitude,
283
+ "y": latitude,
284
+ "longitude": longitude,
285
+ "latitude": latitude,
286
+ }
287
+
288
+
289
+ def wgs84_3d(
290
+ longitude: float, latitude: float, height: float
291
+ ) -> LoraWgs84Point3D:
292
+ return {
293
+ "kind": "point",
294
+ "srid": 4979,
295
+ "crs": "WGS-84-3D",
296
+ "x": longitude,
297
+ "y": latitude,
298
+ "z": height,
299
+ "longitude": longitude,
300
+ "latitude": latitude,
301
+ "height": height,
302
+ }
303
+
304
+
305
+ # ---------------------------------------------------------------------------
306
+ # Narrowing helpers.
307
+ # ---------------------------------------------------------------------------
308
+
309
+
310
+ def _is_tagged(v: Any, expected: str) -> bool:
311
+ return isinstance(v, dict) and v.get("kind") == expected
312
+
313
+
314
+ def is_node(v: Any) -> bool:
315
+ """Return True if ``v`` is a Lora node value."""
316
+ return _is_tagged(v, "node")
317
+
318
+
319
+ def is_relationship(v: Any) -> bool:
320
+ return _is_tagged(v, "relationship")
321
+
322
+
323
+ def is_path(v: Any) -> bool:
324
+ return _is_tagged(v, "path")
325
+
326
+
327
+ def is_point(v: Any) -> bool:
328
+ return _is_tagged(v, "point")
329
+
330
+
331
+ _TEMPORAL_KINDS = frozenset(
332
+ {"date", "time", "localtime", "datetime", "localdatetime", "duration"}
333
+ )
334
+
335
+
336
+ def is_temporal(v: Any) -> bool:
337
+ return isinstance(v, dict) and v.get("kind") in _TEMPORAL_KINDS
338
+
339
+
340
+ def is_vector(v: Any) -> bool:
341
+ """Return True if ``v`` is a Lora VECTOR value."""
342
+ return _is_tagged(v, "vector")