duckling-orm 0.0.3__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.
- duckling/__init__.py +123 -0
- duckling/connection.py +173 -0
- duckling/document.py +598 -0
- duckling/exceptions.py +33 -0
- duckling/fields.py +261 -0
- duckling/init.py +142 -0
- duckling/operators.py +148 -0
- duckling/query.py +391 -0
- duckling_orm-0.0.3.dist-info/METADATA +349 -0
- duckling_orm-0.0.3.dist-info/RECORD +13 -0
- duckling_orm-0.0.3.dist-info/WHEEL +5 -0
- duckling_orm-0.0.3.dist-info/licenses/LICENSE +202 -0
- duckling_orm-0.0.3.dist-info/top_level.txt +1 -0
duckling/__init__.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Duckling — A Beanie-inspired ORM for DuckDB.
|
|
3
|
+
|
|
4
|
+
from duckling import Document, init_duckling, Indexed
|
|
5
|
+
|
|
6
|
+
class User(Document):
|
|
7
|
+
name: str
|
|
8
|
+
email: Indexed(str, unique=True)
|
|
9
|
+
age: int = 0
|
|
10
|
+
|
|
11
|
+
class Settings:
|
|
12
|
+
table_name = "users"
|
|
13
|
+
|
|
14
|
+
await init_duckling(database=":memory:", document_models=[User])
|
|
15
|
+
|
|
16
|
+
user = User(name="Alice", email="alice@example.com", age=30)
|
|
17
|
+
await user.insert()
|
|
18
|
+
|
|
19
|
+
users = await User.find(User.age > 25).sort("+name").limit(10).to_list()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
from .connection import DucklingSession, get_session
|
|
25
|
+
from .document import Document
|
|
26
|
+
from .exceptions import (
|
|
27
|
+
CollectionNotFound,
|
|
28
|
+
ConnectionError,
|
|
29
|
+
DocumentAlreadyExists,
|
|
30
|
+
DocumentNotFound,
|
|
31
|
+
DucklingError,
|
|
32
|
+
InvalidQueryError,
|
|
33
|
+
NotInitializedError,
|
|
34
|
+
ValidationError,
|
|
35
|
+
)
|
|
36
|
+
from .fields import (
|
|
37
|
+
Expression,
|
|
38
|
+
FieldProxy,
|
|
39
|
+
Indexed,
|
|
40
|
+
IndexSpec,
|
|
41
|
+
SortDirection,
|
|
42
|
+
)
|
|
43
|
+
from .init import init_duckling, init_duckling_sync
|
|
44
|
+
from .operators import (
|
|
45
|
+
And,
|
|
46
|
+
Between,
|
|
47
|
+
Eq,
|
|
48
|
+
Gt,
|
|
49
|
+
Gte,
|
|
50
|
+
ILike,
|
|
51
|
+
In,
|
|
52
|
+
IsNotNull,
|
|
53
|
+
IsNull,
|
|
54
|
+
Like,
|
|
55
|
+
Lt,
|
|
56
|
+
Lte,
|
|
57
|
+
Ne,
|
|
58
|
+
Not,
|
|
59
|
+
NotIn,
|
|
60
|
+
Or,
|
|
61
|
+
Raw,
|
|
62
|
+
)
|
|
63
|
+
from .query import (
|
|
64
|
+
Avg,
|
|
65
|
+
Count,
|
|
66
|
+
CountDistinct,
|
|
67
|
+
FindQuery,
|
|
68
|
+
Max,
|
|
69
|
+
Min,
|
|
70
|
+
Sum,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
# Core
|
|
75
|
+
"Document",
|
|
76
|
+
"init_duckling",
|
|
77
|
+
"init_duckling_sync",
|
|
78
|
+
# Session
|
|
79
|
+
"DucklingSession",
|
|
80
|
+
"get_session",
|
|
81
|
+
# Fields
|
|
82
|
+
"Indexed",
|
|
83
|
+
"IndexSpec",
|
|
84
|
+
"SortDirection",
|
|
85
|
+
"FieldProxy",
|
|
86
|
+
"Expression",
|
|
87
|
+
# Query
|
|
88
|
+
"FindQuery",
|
|
89
|
+
# Operators
|
|
90
|
+
"And",
|
|
91
|
+
"Or",
|
|
92
|
+
"Not",
|
|
93
|
+
"In",
|
|
94
|
+
"NotIn",
|
|
95
|
+
"Between",
|
|
96
|
+
"Like",
|
|
97
|
+
"ILike",
|
|
98
|
+
"Eq",
|
|
99
|
+
"Ne",
|
|
100
|
+
"Gt",
|
|
101
|
+
"Gte",
|
|
102
|
+
"Lt",
|
|
103
|
+
"Lte",
|
|
104
|
+
"IsNull",
|
|
105
|
+
"IsNotNull",
|
|
106
|
+
"Raw",
|
|
107
|
+
# Aggregation
|
|
108
|
+
"Count",
|
|
109
|
+
"CountDistinct",
|
|
110
|
+
"Sum",
|
|
111
|
+
"Avg",
|
|
112
|
+
"Min",
|
|
113
|
+
"Max",
|
|
114
|
+
# Exceptions
|
|
115
|
+
"DucklingError",
|
|
116
|
+
"DocumentNotFound",
|
|
117
|
+
"DocumentAlreadyExists",
|
|
118
|
+
"NotInitializedError",
|
|
119
|
+
"CollectionNotFound",
|
|
120
|
+
"InvalidQueryError",
|
|
121
|
+
"ValidationError",
|
|
122
|
+
"ConnectionError",
|
|
123
|
+
]
|
duckling/connection.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""DuckDB connection management for Duckling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import threading
|
|
7
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
import duckdb
|
|
12
|
+
|
|
13
|
+
from .exceptions import ConnectionError, NotInitializedError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DucklingSession:
|
|
17
|
+
"""
|
|
18
|
+
Manages DuckDB connections and provides both sync and async access.
|
|
19
|
+
|
|
20
|
+
This is a singleton that holds the DuckDB connection used by all
|
|
21
|
+
Document models registered with Duckling.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_instance: Optional[DucklingSession] = None
|
|
25
|
+
_lock = threading.Lock()
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._connection: Optional[duckdb.DuckDBPyConnection] = None
|
|
29
|
+
self._database: Optional[str] = None
|
|
30
|
+
self._config: dict[str, Any] = {}
|
|
31
|
+
self._initialized = False
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_instance(cls) -> DucklingSession:
|
|
35
|
+
if cls._instance is None:
|
|
36
|
+
with cls._lock:
|
|
37
|
+
if cls._instance is None:
|
|
38
|
+
cls._instance = cls()
|
|
39
|
+
return cls._instance
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def reset(cls) -> None:
|
|
43
|
+
"""Reset the singleton (useful for testing)."""
|
|
44
|
+
if cls._instance and cls._instance._connection:
|
|
45
|
+
try:
|
|
46
|
+
cls._instance._connection.close()
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
cls._instance = None
|
|
50
|
+
|
|
51
|
+
def connect(
|
|
52
|
+
self,
|
|
53
|
+
database: str = ":memory:",
|
|
54
|
+
read_only: bool = False,
|
|
55
|
+
config: Optional[dict[str, Any]] = None,
|
|
56
|
+
) -> duckdb.DuckDBPyConnection:
|
|
57
|
+
"""Establish a connection to DuckDB."""
|
|
58
|
+
try:
|
|
59
|
+
self._database = database
|
|
60
|
+
self._config = config or {}
|
|
61
|
+
self._connection = duckdb.connect(
|
|
62
|
+
database=database,
|
|
63
|
+
read_only=read_only,
|
|
64
|
+
config=self._config,
|
|
65
|
+
)
|
|
66
|
+
self._initialized = True
|
|
67
|
+
return self._connection
|
|
68
|
+
except Exception as e:
|
|
69
|
+
raise ConnectionError(f"Failed to connect to DuckDB: {e}") from e
|
|
70
|
+
|
|
71
|
+
def use_connection(self, connection: duckdb.DuckDBPyConnection) -> None:
|
|
72
|
+
"""Use an existing DuckDB connection."""
|
|
73
|
+
self._connection = connection
|
|
74
|
+
self._database = None
|
|
75
|
+
self._config = {}
|
|
76
|
+
self._initialized = True
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def connection(self) -> duckdb.DuckDBPyConnection:
|
|
80
|
+
"""Get the active DuckDB connection."""
|
|
81
|
+
if not self._initialized or self._connection is None:
|
|
82
|
+
raise NotInitializedError(
|
|
83
|
+
"Duckling is not initialized. Call `await init_duckling(...)` first."
|
|
84
|
+
)
|
|
85
|
+
return self._connection
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def is_initialized(self) -> bool:
|
|
89
|
+
return self._initialized
|
|
90
|
+
|
|
91
|
+
def execute(self, query: str, params: Optional[list] = None) -> duckdb.DuckDBPyConnection:
|
|
92
|
+
"""Execute a SQL query synchronously."""
|
|
93
|
+
conn = self.connection
|
|
94
|
+
if params:
|
|
95
|
+
return conn.execute(query, params)
|
|
96
|
+
return conn.execute(query)
|
|
97
|
+
|
|
98
|
+
async def async_execute(self, query: str, params: Optional[list] = None) -> Any:
|
|
99
|
+
"""Execute a SQL query asynchronously via thread pool."""
|
|
100
|
+
return await asyncio.to_thread(self.execute, query, params)
|
|
101
|
+
|
|
102
|
+
def fetchall(self, query: str, params: Optional[list] = None) -> list[tuple]:
|
|
103
|
+
"""Execute and fetch all results synchronously."""
|
|
104
|
+
result = self.execute(query, params)
|
|
105
|
+
return result.fetchall()
|
|
106
|
+
|
|
107
|
+
async def async_fetchall(self, query: str, params: Optional[list] = None) -> list[tuple]:
|
|
108
|
+
"""Execute and fetch all results asynchronously."""
|
|
109
|
+
return await asyncio.to_thread(self.fetchall, query, params)
|
|
110
|
+
|
|
111
|
+
def fetchone(self, query: str, params: Optional[list] = None) -> Optional[tuple]:
|
|
112
|
+
"""Execute and fetch one result synchronously."""
|
|
113
|
+
result = self.execute(query, params)
|
|
114
|
+
return result.fetchone()
|
|
115
|
+
|
|
116
|
+
async def async_fetchone(self, query: str, params: Optional[list] = None) -> Optional[tuple]:
|
|
117
|
+
"""Execute and fetch one result asynchronously."""
|
|
118
|
+
return await asyncio.to_thread(self.fetchone, query, params)
|
|
119
|
+
|
|
120
|
+
def fetchdf(self, query: str, params: Optional[list] = None):
|
|
121
|
+
"""Execute and return results as a pandas DataFrame."""
|
|
122
|
+
result = self.execute(query, params)
|
|
123
|
+
return result.fetchdf()
|
|
124
|
+
|
|
125
|
+
async def async_fetchdf(self, query: str, params: Optional[list] = None):
|
|
126
|
+
"""Execute and return results as a DataFrame asynchronously."""
|
|
127
|
+
return await asyncio.to_thread(self.fetchdf, query, params)
|
|
128
|
+
|
|
129
|
+
@contextmanager
|
|
130
|
+
def transaction(self):
|
|
131
|
+
"""Synchronous transaction context manager."""
|
|
132
|
+
conn = self.connection
|
|
133
|
+
conn.execute("BEGIN TRANSACTION")
|
|
134
|
+
try:
|
|
135
|
+
yield conn
|
|
136
|
+
conn.execute("COMMIT")
|
|
137
|
+
except Exception:
|
|
138
|
+
conn.execute("ROLLBACK")
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
@asynccontextmanager
|
|
142
|
+
async def async_transaction(self):
|
|
143
|
+
"""Asynchronous transaction context manager."""
|
|
144
|
+
conn = self.connection
|
|
145
|
+
|
|
146
|
+
def begin():
|
|
147
|
+
conn.execute("BEGIN TRANSACTION")
|
|
148
|
+
|
|
149
|
+
def commit():
|
|
150
|
+
conn.execute("COMMIT")
|
|
151
|
+
|
|
152
|
+
def rollback():
|
|
153
|
+
conn.execute("ROLLBACK")
|
|
154
|
+
|
|
155
|
+
await asyncio.to_thread(begin)
|
|
156
|
+
try:
|
|
157
|
+
yield conn
|
|
158
|
+
await asyncio.to_thread(commit)
|
|
159
|
+
except Exception:
|
|
160
|
+
await asyncio.to_thread(rollback)
|
|
161
|
+
raise
|
|
162
|
+
|
|
163
|
+
def close(self) -> None:
|
|
164
|
+
"""Close the connection."""
|
|
165
|
+
if self._connection:
|
|
166
|
+
self._connection.close()
|
|
167
|
+
self._connection = None
|
|
168
|
+
self._initialized = False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_session() -> DucklingSession:
|
|
172
|
+
"""Get the current Duckling session."""
|
|
173
|
+
return DucklingSession.get_instance()
|