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