hammad-python 0.0.30__py3-none-any.whl → 0.0.32__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.
- ham/__init__.py +200 -0
- {hammad_python-0.0.30.dist-info → hammad_python-0.0.32.dist-info}/METADATA +6 -32
- hammad_python-0.0.32.dist-info/RECORD +6 -0
- hammad/__init__.py +0 -84
- hammad/_internal.py +0 -256
- hammad/_main.py +0 -226
- hammad/cache/__init__.py +0 -40
- hammad/cache/base_cache.py +0 -181
- hammad/cache/cache.py +0 -169
- hammad/cache/decorators.py +0 -261
- hammad/cache/file_cache.py +0 -80
- hammad/cache/ttl_cache.py +0 -74
- hammad/cli/__init__.py +0 -33
- hammad/cli/animations.py +0 -573
- hammad/cli/plugins.py +0 -867
- hammad/cli/styles/__init__.py +0 -55
- hammad/cli/styles/settings.py +0 -139
- hammad/cli/styles/types.py +0 -358
- hammad/cli/styles/utils.py +0 -634
- hammad/data/__init__.py +0 -90
- hammad/data/collections/__init__.py +0 -49
- hammad/data/collections/collection.py +0 -326
- hammad/data/collections/indexes/__init__.py +0 -37
- hammad/data/collections/indexes/qdrant/__init__.py +0 -1
- hammad/data/collections/indexes/qdrant/index.py +0 -723
- hammad/data/collections/indexes/qdrant/settings.py +0 -94
- hammad/data/collections/indexes/qdrant/utils.py +0 -210
- hammad/data/collections/indexes/tantivy/__init__.py +0 -1
- hammad/data/collections/indexes/tantivy/index.py +0 -426
- hammad/data/collections/indexes/tantivy/settings.py +0 -40
- hammad/data/collections/indexes/tantivy/utils.py +0 -176
- hammad/data/configurations/__init__.py +0 -35
- hammad/data/configurations/configuration.py +0 -564
- hammad/data/models/__init__.py +0 -50
- hammad/data/models/extensions/__init__.py +0 -4
- hammad/data/models/extensions/pydantic/__init__.py +0 -42
- hammad/data/models/extensions/pydantic/converters.py +0 -759
- hammad/data/models/fields.py +0 -546
- hammad/data/models/model.py +0 -1078
- hammad/data/models/utils.py +0 -280
- hammad/data/sql/__init__.py +0 -24
- hammad/data/sql/database.py +0 -576
- hammad/data/sql/types.py +0 -127
- hammad/data/types/__init__.py +0 -75
- hammad/data/types/file.py +0 -431
- hammad/data/types/multimodal/__init__.py +0 -36
- hammad/data/types/multimodal/audio.py +0 -200
- hammad/data/types/multimodal/image.py +0 -182
- hammad/data/types/text.py +0 -1308
- hammad/formatting/__init__.py +0 -33
- hammad/formatting/json/__init__.py +0 -27
- hammad/formatting/json/converters.py +0 -158
- hammad/formatting/text/__init__.py +0 -63
- hammad/formatting/text/converters.py +0 -723
- hammad/formatting/text/markdown.py +0 -131
- hammad/formatting/yaml/__init__.py +0 -26
- hammad/formatting/yaml/converters.py +0 -5
- hammad/genai/__init__.py +0 -217
- hammad/genai/a2a/__init__.py +0 -32
- hammad/genai/a2a/workers.py +0 -552
- hammad/genai/agents/__init__.py +0 -59
- hammad/genai/agents/agent.py +0 -1973
- hammad/genai/agents/run.py +0 -1024
- hammad/genai/agents/types/__init__.py +0 -42
- hammad/genai/agents/types/agent_context.py +0 -13
- hammad/genai/agents/types/agent_event.py +0 -128
- hammad/genai/agents/types/agent_hooks.py +0 -220
- hammad/genai/agents/types/agent_messages.py +0 -31
- hammad/genai/agents/types/agent_response.py +0 -125
- hammad/genai/agents/types/agent_stream.py +0 -327
- hammad/genai/graphs/__init__.py +0 -125
- hammad/genai/graphs/_utils.py +0 -190
- hammad/genai/graphs/base.py +0 -1828
- hammad/genai/graphs/plugins.py +0 -316
- hammad/genai/graphs/types.py +0 -638
- hammad/genai/models/__init__.py +0 -1
- hammad/genai/models/embeddings/__init__.py +0 -43
- hammad/genai/models/embeddings/model.py +0 -226
- hammad/genai/models/embeddings/run.py +0 -163
- hammad/genai/models/embeddings/types/__init__.py +0 -37
- hammad/genai/models/embeddings/types/embedding_model_name.py +0 -75
- hammad/genai/models/embeddings/types/embedding_model_response.py +0 -76
- hammad/genai/models/embeddings/types/embedding_model_run_params.py +0 -66
- hammad/genai/models/embeddings/types/embedding_model_settings.py +0 -47
- hammad/genai/models/language/__init__.py +0 -57
- hammad/genai/models/language/model.py +0 -1098
- hammad/genai/models/language/run.py +0 -878
- hammad/genai/models/language/types/__init__.py +0 -40
- hammad/genai/models/language/types/language_model_instructor_mode.py +0 -47
- hammad/genai/models/language/types/language_model_messages.py +0 -28
- hammad/genai/models/language/types/language_model_name.py +0 -239
- hammad/genai/models/language/types/language_model_request.py +0 -127
- hammad/genai/models/language/types/language_model_response.py +0 -217
- hammad/genai/models/language/types/language_model_response_chunk.py +0 -56
- hammad/genai/models/language/types/language_model_settings.py +0 -89
- hammad/genai/models/language/types/language_model_stream.py +0 -600
- hammad/genai/models/language/utils/__init__.py +0 -28
- hammad/genai/models/language/utils/requests.py +0 -421
- hammad/genai/models/language/utils/structured_outputs.py +0 -135
- hammad/genai/models/model_provider.py +0 -4
- hammad/genai/models/multimodal.py +0 -47
- hammad/genai/models/reranking.py +0 -26
- hammad/genai/types/__init__.py +0 -1
- hammad/genai/types/base.py +0 -215
- hammad/genai/types/history.py +0 -290
- hammad/genai/types/tools.py +0 -507
- hammad/logging/__init__.py +0 -35
- hammad/logging/decorators.py +0 -834
- hammad/logging/logger.py +0 -1018
- hammad/mcp/__init__.py +0 -53
- hammad/mcp/client/__init__.py +0 -35
- hammad/mcp/client/client.py +0 -624
- hammad/mcp/client/client_service.py +0 -400
- hammad/mcp/client/settings.py +0 -178
- hammad/mcp/servers/__init__.py +0 -26
- hammad/mcp/servers/launcher.py +0 -1161
- hammad/runtime/__init__.py +0 -32
- hammad/runtime/decorators.py +0 -142
- hammad/runtime/run.py +0 -299
- hammad/service/__init__.py +0 -49
- hammad/service/create.py +0 -527
- hammad/service/decorators.py +0 -283
- hammad/types.py +0 -288
- hammad/typing/__init__.py +0 -435
- hammad/web/__init__.py +0 -43
- hammad/web/http/__init__.py +0 -1
- hammad/web/http/client.py +0 -944
- hammad/web/models.py +0 -275
- hammad/web/openapi/__init__.py +0 -1
- hammad/web/openapi/client.py +0 -740
- hammad/web/search/__init__.py +0 -1
- hammad/web/search/client.py +0 -1023
- hammad/web/utils.py +0 -472
- hammad_python-0.0.30.dist-info/RECORD +0 -135
- {hammad → ham}/py.typed +0 -0
- {hammad_python-0.0.30.dist-info → hammad_python-0.0.32.dist-info}/WHEEL +0 -0
- {hammad_python-0.0.30.dist-info → hammad_python-0.0.32.dist-info}/licenses/LICENSE +0 -0
hammad/data/sql/database.py
DELETED
@@ -1,576 +0,0 @@
|
|
1
|
-
"""hammad.data.sql.database"""
|
2
|
-
|
3
|
-
from datetime import datetime, timezone, timedelta
|
4
|
-
from pathlib import Path
|
5
|
-
from typing import (
|
6
|
-
Any,
|
7
|
-
Dict,
|
8
|
-
Generic,
|
9
|
-
List,
|
10
|
-
Optional,
|
11
|
-
Type,
|
12
|
-
Union,
|
13
|
-
Literal,
|
14
|
-
final,
|
15
|
-
)
|
16
|
-
import uuid
|
17
|
-
import json
|
18
|
-
|
19
|
-
try:
|
20
|
-
from sqlalchemy import (
|
21
|
-
create_engine,
|
22
|
-
Column,
|
23
|
-
String,
|
24
|
-
Text,
|
25
|
-
DateTime,
|
26
|
-
Integer,
|
27
|
-
MetaData,
|
28
|
-
Table,
|
29
|
-
and_,
|
30
|
-
or_,
|
31
|
-
select,
|
32
|
-
insert,
|
33
|
-
update,
|
34
|
-
delete,
|
35
|
-
Engine,
|
36
|
-
)
|
37
|
-
from sqlalchemy.orm import sessionmaker, Session
|
38
|
-
from sqlalchemy.sql import Select
|
39
|
-
|
40
|
-
SQLALCHEMY_AVAILABLE = True
|
41
|
-
except ImportError:
|
42
|
-
# SQLAlchemy not available
|
43
|
-
SQLALCHEMY_AVAILABLE = False
|
44
|
-
create_engine = None
|
45
|
-
Engine = None
|
46
|
-
Session = None
|
47
|
-
|
48
|
-
from .types import (
|
49
|
-
DatabaseItemType,
|
50
|
-
DatabaseItemFilters,
|
51
|
-
DatabaseItem,
|
52
|
-
QueryOperator,
|
53
|
-
QueryCondition,
|
54
|
-
QueryFilter,
|
55
|
-
)
|
56
|
-
|
57
|
-
__all__ = [
|
58
|
-
"create_database",
|
59
|
-
"Database",
|
60
|
-
"DatabaseError",
|
61
|
-
]
|
62
|
-
|
63
|
-
|
64
|
-
class DatabaseError(Exception):
|
65
|
-
"""Exception raised when an error occurs in the Database."""
|
66
|
-
|
67
|
-
|
68
|
-
@final
|
69
|
-
class Database(Generic[DatabaseItemType]):
|
70
|
-
"""
|
71
|
-
A clean SQL-based database implementation using SQLAlchemy that provides
|
72
|
-
the lowest-level storage backend for collections.
|
73
|
-
|
74
|
-
Features:
|
75
|
-
- Optional schema validation
|
76
|
-
- Custom path format support (memory or file-based)
|
77
|
-
- Pythonic query interface with type-safe operators
|
78
|
-
- TTL support with automatic cleanup
|
79
|
-
- JSON serialization for complex objects
|
80
|
-
- Transaction support
|
81
|
-
"""
|
82
|
-
|
83
|
-
def __init__(
|
84
|
-
self,
|
85
|
-
*,
|
86
|
-
name: str = "default",
|
87
|
-
schema: Optional[Type[DatabaseItemType]] = None,
|
88
|
-
ttl: Optional[int] = None,
|
89
|
-
path: Optional[Union[Path, str]] = None,
|
90
|
-
table_name: str = "items",
|
91
|
-
auto_cleanup_expired: bool = True,
|
92
|
-
) -> None:
|
93
|
-
"""
|
94
|
-
Initialize a new Database instance.
|
95
|
-
|
96
|
-
Args:
|
97
|
-
name: The name of the database
|
98
|
-
schema: Optional schema type for validation
|
99
|
-
ttl: Default time-to-live in seconds
|
100
|
-
path: File path for persistent storage (None = in-memory)
|
101
|
-
table_name: Name of the primary table
|
102
|
-
auto_cleanup_expired: Whether to automatically clean up expired items
|
103
|
-
"""
|
104
|
-
if not SQLALCHEMY_AVAILABLE:
|
105
|
-
raise DatabaseError(
|
106
|
-
"SQLAlchemy is required for Database. "
|
107
|
-
"Install with: pip install sqlalchemy"
|
108
|
-
)
|
109
|
-
|
110
|
-
self.name = name
|
111
|
-
self.schema = schema
|
112
|
-
self.ttl = ttl
|
113
|
-
self.path = Path(path) if path else None
|
114
|
-
self.table_name = table_name
|
115
|
-
self.auto_cleanup_expired = auto_cleanup_expired
|
116
|
-
|
117
|
-
# Initialize SQLAlchemy components
|
118
|
-
self._engine: Optional[Engine] = None
|
119
|
-
self._session_factory = None
|
120
|
-
self._metadata: Optional[MetaData] = None
|
121
|
-
self._table: Optional[Table] = None
|
122
|
-
|
123
|
-
self._init_database()
|
124
|
-
|
125
|
-
def _init_database(self) -> None:
|
126
|
-
"""Initialize the database engine and create tables."""
|
127
|
-
# Determine connection string
|
128
|
-
if self.path is None:
|
129
|
-
# In-memory database
|
130
|
-
connection_string = "sqlite:///:memory:"
|
131
|
-
else:
|
132
|
-
# File-based database
|
133
|
-
# Create directory if it doesn't exist
|
134
|
-
if self.path.parent != Path("."):
|
135
|
-
self.path.parent.mkdir(parents=True, exist_ok=True)
|
136
|
-
connection_string = f"sqlite:///{self.path}"
|
137
|
-
|
138
|
-
# Create engine
|
139
|
-
self._engine = create_engine(
|
140
|
-
connection_string,
|
141
|
-
echo=False,
|
142
|
-
pool_pre_ping=True,
|
143
|
-
)
|
144
|
-
|
145
|
-
# Create session factory
|
146
|
-
self._session_factory = sessionmaker(bind=self._engine)
|
147
|
-
|
148
|
-
# Create metadata and table
|
149
|
-
self._metadata = MetaData()
|
150
|
-
self._create_table()
|
151
|
-
|
152
|
-
def _create_table(self) -> None:
|
153
|
-
"""Create the main table for storing items."""
|
154
|
-
self._table = Table(
|
155
|
-
self.table_name,
|
156
|
-
self._metadata,
|
157
|
-
Column("id", String, primary_key=True),
|
158
|
-
Column("item_data", Text, nullable=False), # JSON serialized item
|
159
|
-
Column("filters", Text), # JSON serialized filters
|
160
|
-
Column("created_at", DateTime, nullable=False),
|
161
|
-
Column("updated_at", DateTime, nullable=False),
|
162
|
-
Column("ttl", Integer), # TTL in seconds
|
163
|
-
Column("table_name", String, default=self.table_name),
|
164
|
-
)
|
165
|
-
|
166
|
-
# Create all tables
|
167
|
-
self._metadata.create_all(self._engine)
|
168
|
-
|
169
|
-
def _serialize_item(self, item: DatabaseItemType) -> str:
|
170
|
-
"""Serialize an item to JSON string."""
|
171
|
-
from dataclasses import is_dataclass, asdict
|
172
|
-
|
173
|
-
if isinstance(item, (str, int, float, bool, type(None))):
|
174
|
-
return json.dumps(item)
|
175
|
-
elif isinstance(item, (list, dict)):
|
176
|
-
return json.dumps(item)
|
177
|
-
elif is_dataclass(item):
|
178
|
-
return json.dumps(asdict(item))
|
179
|
-
elif hasattr(item, "__dict__"):
|
180
|
-
return json.dumps(item.__dict__)
|
181
|
-
else:
|
182
|
-
return json.dumps(str(item))
|
183
|
-
|
184
|
-
def _deserialize_item(self, data: str) -> DatabaseItemType:
|
185
|
-
"""Deserialize an item from JSON string."""
|
186
|
-
return json.loads(data)
|
187
|
-
|
188
|
-
def _validate_schema(self, item: DatabaseItemType) -> None:
|
189
|
-
"""Validate item against schema if one is set."""
|
190
|
-
if self.schema is not None:
|
191
|
-
if not isinstance(item, self.schema):
|
192
|
-
raise ValueError(f"Item is not of type {self.schema.__name__}")
|
193
|
-
|
194
|
-
def _build_query_conditions(
|
195
|
-
self,
|
196
|
-
query_filter: QueryFilter,
|
197
|
-
table: Table,
|
198
|
-
) -> Any:
|
199
|
-
"""Build SQLAlchemy query conditions from QueryFilter."""
|
200
|
-
conditions = []
|
201
|
-
|
202
|
-
for condition in query_filter.conditions:
|
203
|
-
column = getattr(table.c, condition.field, None)
|
204
|
-
if column is None:
|
205
|
-
continue
|
206
|
-
|
207
|
-
if condition.operator == "eq":
|
208
|
-
conditions.append(column == condition.value)
|
209
|
-
elif condition.operator == "ne":
|
210
|
-
conditions.append(column != condition.value)
|
211
|
-
elif condition.operator == "gt":
|
212
|
-
conditions.append(column > condition.value)
|
213
|
-
elif condition.operator == "gte":
|
214
|
-
conditions.append(column >= condition.value)
|
215
|
-
elif condition.operator == "lt":
|
216
|
-
conditions.append(column < condition.value)
|
217
|
-
elif condition.operator == "lte":
|
218
|
-
conditions.append(column <= condition.value)
|
219
|
-
elif condition.operator == "in":
|
220
|
-
conditions.append(column.in_(condition.value))
|
221
|
-
elif condition.operator == "not_in":
|
222
|
-
conditions.append(~column.in_(condition.value))
|
223
|
-
elif condition.operator == "like":
|
224
|
-
conditions.append(column.like(condition.value))
|
225
|
-
elif condition.operator == "ilike":
|
226
|
-
conditions.append(column.ilike(condition.value))
|
227
|
-
elif condition.operator == "is_null":
|
228
|
-
conditions.append(column.is_(None))
|
229
|
-
elif condition.operator == "is_not_null":
|
230
|
-
conditions.append(column.isnot(None))
|
231
|
-
elif condition.operator == "startswith":
|
232
|
-
conditions.append(column.like(f"{condition.value}%"))
|
233
|
-
elif condition.operator == "endswith":
|
234
|
-
conditions.append(column.like(f"%{condition.value}"))
|
235
|
-
elif condition.operator == "contains":
|
236
|
-
conditions.append(column.like(f"%{condition.value}%"))
|
237
|
-
|
238
|
-
if not conditions:
|
239
|
-
return None
|
240
|
-
|
241
|
-
if query_filter.logic == "and":
|
242
|
-
return and_(*conditions)
|
243
|
-
else: # or
|
244
|
-
return or_(*conditions)
|
245
|
-
|
246
|
-
def _cleanup_expired_items(self, session: Session) -> int:
|
247
|
-
"""Remove expired items from the database."""
|
248
|
-
if not self.auto_cleanup_expired:
|
249
|
-
return 0
|
250
|
-
|
251
|
-
now = datetime.now(timezone.utc)
|
252
|
-
|
253
|
-
# Find expired items by checking created_at + ttl < now
|
254
|
-
stmt = select(self._table).where(
|
255
|
-
and_(
|
256
|
-
self._table.c.ttl.isnot(None),
|
257
|
-
self._table.c.created_at + (self._table.c.ttl * timedelta(seconds=1))
|
258
|
-
< now,
|
259
|
-
)
|
260
|
-
)
|
261
|
-
|
262
|
-
expired_items = session.execute(stmt).fetchall()
|
263
|
-
expired_ids = [item.id for item in expired_items]
|
264
|
-
|
265
|
-
if expired_ids:
|
266
|
-
delete_stmt = delete(self._table).where(self._table.c.id.in_(expired_ids))
|
267
|
-
session.execute(delete_stmt)
|
268
|
-
|
269
|
-
return len(expired_ids)
|
270
|
-
|
271
|
-
def add(
|
272
|
-
self,
|
273
|
-
item: DatabaseItemType,
|
274
|
-
*,
|
275
|
-
id: Optional[str] = None,
|
276
|
-
filters: Optional[DatabaseItemFilters] = None,
|
277
|
-
ttl: Optional[int] = None,
|
278
|
-
) -> str:
|
279
|
-
"""
|
280
|
-
Add an item to the database.
|
281
|
-
|
282
|
-
Args:
|
283
|
-
item: The item to store
|
284
|
-
id: Optional ID (will generate UUID if not provided)
|
285
|
-
filters: Optional filters/metadata
|
286
|
-
ttl: Optional TTL in seconds
|
287
|
-
|
288
|
-
Returns:
|
289
|
-
The ID of the stored item
|
290
|
-
"""
|
291
|
-
self._validate_schema(item)
|
292
|
-
|
293
|
-
item_id = id or str(uuid.uuid4())
|
294
|
-
item_ttl = ttl or self.ttl
|
295
|
-
now = datetime.now(timezone.utc)
|
296
|
-
|
297
|
-
serialized_item = self._serialize_item(item)
|
298
|
-
serialized_filters = json.dumps(filters or {})
|
299
|
-
|
300
|
-
with self._session_factory() as session:
|
301
|
-
# Check if item already exists
|
302
|
-
existing = session.execute(
|
303
|
-
select(self._table).where(self._table.c.id == item_id)
|
304
|
-
).fetchone()
|
305
|
-
|
306
|
-
if existing:
|
307
|
-
# Update existing item
|
308
|
-
stmt = (
|
309
|
-
update(self._table)
|
310
|
-
.where(self._table.c.id == item_id)
|
311
|
-
.values(
|
312
|
-
item_data=serialized_item,
|
313
|
-
filters=serialized_filters,
|
314
|
-
updated_at=now,
|
315
|
-
ttl=item_ttl,
|
316
|
-
)
|
317
|
-
)
|
318
|
-
else:
|
319
|
-
# Insert new item
|
320
|
-
stmt = insert(self._table).values(
|
321
|
-
id=item_id,
|
322
|
-
item_data=serialized_item,
|
323
|
-
filters=serialized_filters,
|
324
|
-
created_at=now,
|
325
|
-
updated_at=now,
|
326
|
-
ttl=item_ttl,
|
327
|
-
table_name=self.table_name,
|
328
|
-
)
|
329
|
-
|
330
|
-
session.execute(stmt)
|
331
|
-
|
332
|
-
# Cleanup expired items
|
333
|
-
self._cleanup_expired_items(session)
|
334
|
-
|
335
|
-
session.commit()
|
336
|
-
|
337
|
-
return item_id
|
338
|
-
|
339
|
-
def get(
|
340
|
-
self,
|
341
|
-
id: str,
|
342
|
-
*,
|
343
|
-
filters: Optional[DatabaseItemFilters] = None,
|
344
|
-
) -> Optional[DatabaseItem[DatabaseItemType]]:
|
345
|
-
"""
|
346
|
-
Get an item by ID.
|
347
|
-
|
348
|
-
Args:
|
349
|
-
id: The item ID
|
350
|
-
filters: Optional filters to match
|
351
|
-
|
352
|
-
Returns:
|
353
|
-
The database item or None if not found
|
354
|
-
"""
|
355
|
-
with self._session_factory() as session:
|
356
|
-
stmt = select(self._table).where(self._table.c.id == id)
|
357
|
-
result = session.execute(stmt).fetchone()
|
358
|
-
|
359
|
-
if not result:
|
360
|
-
return None
|
361
|
-
|
362
|
-
# Check if expired
|
363
|
-
if result.ttl is not None:
|
364
|
-
expires_at = result.created_at + timedelta(seconds=result.ttl)
|
365
|
-
if datetime.now(timezone.utc) >= expires_at:
|
366
|
-
# Delete expired item
|
367
|
-
session.execute(delete(self._table).where(self._table.c.id == id))
|
368
|
-
session.commit()
|
369
|
-
return None
|
370
|
-
|
371
|
-
# Check filters if provided
|
372
|
-
if filters:
|
373
|
-
stored_filters = json.loads(result.filters or "{}")
|
374
|
-
if not all(stored_filters.get(k) == v for k, v in filters.items()):
|
375
|
-
return None
|
376
|
-
|
377
|
-
# Deserialize and return
|
378
|
-
item_data = self._deserialize_item(result.item_data)
|
379
|
-
stored_filters = json.loads(result.filters or "{}")
|
380
|
-
|
381
|
-
return DatabaseItem(
|
382
|
-
id=result.id,
|
383
|
-
item=item_data,
|
384
|
-
created_at=result.created_at,
|
385
|
-
updated_at=result.updated_at,
|
386
|
-
ttl=result.ttl,
|
387
|
-
filters=stored_filters,
|
388
|
-
table_name=result.table_name,
|
389
|
-
)
|
390
|
-
|
391
|
-
def query(
|
392
|
-
self,
|
393
|
-
query_filter: Optional[QueryFilter] = None,
|
394
|
-
*,
|
395
|
-
limit: Optional[int] = None,
|
396
|
-
offset: int = 0,
|
397
|
-
order_by: Optional[str] = None,
|
398
|
-
ascending: bool = True,
|
399
|
-
) -> List[DatabaseItem[DatabaseItemType]]:
|
400
|
-
"""
|
401
|
-
Query items from the database.
|
402
|
-
|
403
|
-
Args:
|
404
|
-
query_filter: Filter conditions to apply
|
405
|
-
limit: Maximum number of results
|
406
|
-
offset: Number of results to skip
|
407
|
-
order_by: Field to order by
|
408
|
-
ascending: Sort direction
|
409
|
-
|
410
|
-
Returns:
|
411
|
-
List of matching database items
|
412
|
-
"""
|
413
|
-
with self._session_factory() as session:
|
414
|
-
# Cleanup expired items first
|
415
|
-
self._cleanup_expired_items(session)
|
416
|
-
|
417
|
-
stmt = select(self._table)
|
418
|
-
|
419
|
-
# Apply filters
|
420
|
-
if query_filter:
|
421
|
-
conditions = self._build_query_conditions(query_filter, self._table)
|
422
|
-
if conditions is not None:
|
423
|
-
stmt = stmt.where(conditions)
|
424
|
-
|
425
|
-
# Apply ordering
|
426
|
-
if order_by:
|
427
|
-
column = getattr(self._table.c, order_by, None)
|
428
|
-
if column is not None:
|
429
|
-
if ascending:
|
430
|
-
stmt = stmt.order_by(column.asc())
|
431
|
-
else:
|
432
|
-
stmt = stmt.order_by(column.desc())
|
433
|
-
else:
|
434
|
-
# Default order by created_at desc
|
435
|
-
stmt = stmt.order_by(self._table.c.created_at.desc())
|
436
|
-
|
437
|
-
# Apply pagination
|
438
|
-
if offset > 0:
|
439
|
-
stmt = stmt.offset(offset)
|
440
|
-
if limit is not None:
|
441
|
-
stmt = stmt.limit(limit)
|
442
|
-
|
443
|
-
results = session.execute(stmt).fetchall()
|
444
|
-
|
445
|
-
items = []
|
446
|
-
for result in results:
|
447
|
-
# Double-check expiration (in case of race conditions)
|
448
|
-
if result.ttl is not None:
|
449
|
-
expires_at = result.created_at + timedelta(seconds=result.ttl)
|
450
|
-
if datetime.now(timezone.utc) >= expires_at:
|
451
|
-
continue
|
452
|
-
|
453
|
-
item_data = self._deserialize_item(result.item_data)
|
454
|
-
stored_filters = json.loads(result.filters or "{}")
|
455
|
-
|
456
|
-
items.append(
|
457
|
-
DatabaseItem(
|
458
|
-
id=result.id,
|
459
|
-
item=item_data,
|
460
|
-
created_at=result.created_at,
|
461
|
-
updated_at=result.updated_at,
|
462
|
-
ttl=result.ttl,
|
463
|
-
filters=stored_filters,
|
464
|
-
table_name=result.table_name,
|
465
|
-
)
|
466
|
-
)
|
467
|
-
|
468
|
-
return items
|
469
|
-
|
470
|
-
def delete(self, id: str) -> bool:
|
471
|
-
"""
|
472
|
-
Delete an item by ID.
|
473
|
-
|
474
|
-
Args:
|
475
|
-
id: The item ID
|
476
|
-
|
477
|
-
Returns:
|
478
|
-
True if item was deleted, False if not found
|
479
|
-
"""
|
480
|
-
with self._session_factory() as session:
|
481
|
-
stmt = delete(self._table).where(self._table.c.id == id)
|
482
|
-
result = session.execute(stmt)
|
483
|
-
session.commit()
|
484
|
-
return result.rowcount > 0
|
485
|
-
|
486
|
-
def count(
|
487
|
-
self,
|
488
|
-
query_filter: Optional[QueryFilter] = None,
|
489
|
-
) -> int:
|
490
|
-
"""
|
491
|
-
Count items matching the filter.
|
492
|
-
|
493
|
-
Args:
|
494
|
-
query_filter: Filter conditions to apply
|
495
|
-
|
496
|
-
Returns:
|
497
|
-
Number of matching items
|
498
|
-
"""
|
499
|
-
with self._session_factory() as session:
|
500
|
-
# Cleanup expired items first
|
501
|
-
self._cleanup_expired_items(session)
|
502
|
-
|
503
|
-
from sqlalchemy import func
|
504
|
-
|
505
|
-
stmt = select(func.count(self._table.c.id))
|
506
|
-
|
507
|
-
if query_filter:
|
508
|
-
conditions = self._build_query_conditions(query_filter, self._table)
|
509
|
-
if conditions is not None:
|
510
|
-
stmt = stmt.where(conditions)
|
511
|
-
|
512
|
-
result = session.execute(stmt).fetchone()
|
513
|
-
return result[0] if result else 0
|
514
|
-
|
515
|
-
def clear(self) -> int:
|
516
|
-
"""
|
517
|
-
Clear all items from the database.
|
518
|
-
|
519
|
-
Returns:
|
520
|
-
Number of items deleted
|
521
|
-
"""
|
522
|
-
with self._session_factory() as session:
|
523
|
-
stmt = delete(self._table)
|
524
|
-
result = session.execute(stmt)
|
525
|
-
session.commit()
|
526
|
-
return result.rowcount
|
527
|
-
|
528
|
-
def cleanup_expired(self) -> int:
|
529
|
-
"""
|
530
|
-
Manually cleanup expired items.
|
531
|
-
|
532
|
-
Returns:
|
533
|
-
Number of items cleaned up
|
534
|
-
"""
|
535
|
-
with self._session_factory() as session:
|
536
|
-
count = self._cleanup_expired_items(session)
|
537
|
-
session.commit()
|
538
|
-
return count
|
539
|
-
|
540
|
-
def __repr__(self) -> str:
|
541
|
-
"""String representation of the database."""
|
542
|
-
location = str(self.path) if self.path else "memory"
|
543
|
-
return f"<Database name='{self.name}' location='{location}' table='{self.table_name}'>"
|
544
|
-
|
545
|
-
|
546
|
-
def create_database(
|
547
|
-
name: str,
|
548
|
-
*,
|
549
|
-
schema: Optional[Type[DatabaseItemType]] = None,
|
550
|
-
ttl: Optional[int] = None,
|
551
|
-
path: Optional[Union[Path, str]] = None,
|
552
|
-
table_name: str = "items",
|
553
|
-
auto_cleanup_expired: bool = True,
|
554
|
-
) -> Database[DatabaseItemType]:
|
555
|
-
"""
|
556
|
-
Create a new database instance.
|
557
|
-
|
558
|
-
Args:
|
559
|
-
name: The name of the database
|
560
|
-
schema: Optional schema type for validation
|
561
|
-
ttl: Default time-to-live in seconds
|
562
|
-
path: File path for storage (None = in-memory)
|
563
|
-
table_name: Name of the primary table
|
564
|
-
auto_cleanup_expired: Whether to automatically clean up expired items
|
565
|
-
|
566
|
-
Returns:
|
567
|
-
A Database instance
|
568
|
-
"""
|
569
|
-
return Database(
|
570
|
-
name=name,
|
571
|
-
schema=schema,
|
572
|
-
ttl=ttl,
|
573
|
-
path=path,
|
574
|
-
table_name=table_name,
|
575
|
-
auto_cleanup_expired=auto_cleanup_expired,
|
576
|
-
)
|
hammad/data/sql/types.py
DELETED
@@ -1,127 +0,0 @@
|
|
1
|
-
"""hammad.data.sql.types"""
|
2
|
-
|
3
|
-
from datetime import datetime, timezone
|
4
|
-
from dataclasses import dataclass, field
|
5
|
-
from typing import (
|
6
|
-
Any,
|
7
|
-
Dict,
|
8
|
-
Generic,
|
9
|
-
Optional,
|
10
|
-
Type,
|
11
|
-
TypeVar,
|
12
|
-
TypeAlias,
|
13
|
-
Literal,
|
14
|
-
Union,
|
15
|
-
)
|
16
|
-
import uuid
|
17
|
-
|
18
|
-
__all__ = (
|
19
|
-
"DatabaseItemType",
|
20
|
-
"DatabaseItemFilters",
|
21
|
-
"DatabaseItem",
|
22
|
-
"QueryOperator",
|
23
|
-
"QueryCondition",
|
24
|
-
"QueryFilter",
|
25
|
-
)
|
26
|
-
|
27
|
-
|
28
|
-
DatabaseItemType = TypeVar("DatabaseItemType")
|
29
|
-
"""Generic type variable for any valid item type that can be stored
|
30
|
-
within a database."""
|
31
|
-
|
32
|
-
|
33
|
-
DatabaseItemFilters: TypeAlias = Dict[str, object]
|
34
|
-
"""A dictionary of filters that can be used to query the database."""
|
35
|
-
|
36
|
-
|
37
|
-
QueryOperator = Literal[
|
38
|
-
"eq", # equal
|
39
|
-
"ne", # not equal
|
40
|
-
"gt", # greater than
|
41
|
-
"gte", # greater than or equal
|
42
|
-
"lt", # less than
|
43
|
-
"lte", # less than or equal
|
44
|
-
"in", # in list
|
45
|
-
"not_in", # not in list
|
46
|
-
"like", # SQL LIKE
|
47
|
-
"ilike", # case insensitive LIKE
|
48
|
-
"is_null", # IS NULL
|
49
|
-
"is_not_null", # IS NOT NULL
|
50
|
-
"contains", # for JSON contains
|
51
|
-
"startswith", # string starts with
|
52
|
-
"endswith", # string ends with
|
53
|
-
]
|
54
|
-
"""Supported query operators for database queries."""
|
55
|
-
|
56
|
-
|
57
|
-
@dataclass
|
58
|
-
class QueryCondition:
|
59
|
-
"""Represents a single query condition for database filtering."""
|
60
|
-
|
61
|
-
field: str
|
62
|
-
"""The field name to filter on."""
|
63
|
-
|
64
|
-
operator: QueryOperator
|
65
|
-
"""The operator to use for comparison."""
|
66
|
-
|
67
|
-
value: Any = None
|
68
|
-
"""The value to compare against (not needed for is_null/is_not_null)."""
|
69
|
-
|
70
|
-
|
71
|
-
@dataclass
|
72
|
-
class QueryFilter:
|
73
|
-
"""Represents a collection of query conditions with logical operators."""
|
74
|
-
|
75
|
-
conditions: list[QueryCondition] = field(default_factory=list)
|
76
|
-
"""List of individual query conditions."""
|
77
|
-
|
78
|
-
logic: Literal["and", "or"] = "and"
|
79
|
-
"""Logical operator to combine conditions."""
|
80
|
-
|
81
|
-
|
82
|
-
@dataclass
|
83
|
-
class DatabaseItem(Generic[DatabaseItemType]):
|
84
|
-
"""Base class for all items that can be stored within a database."""
|
85
|
-
|
86
|
-
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
87
|
-
"""The unique identifier for this item."""
|
88
|
-
|
89
|
-
item: DatabaseItemType = field(default_factory=lambda: None)
|
90
|
-
"""The item that is stored within this database item."""
|
91
|
-
|
92
|
-
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
93
|
-
"""The timestamp when this item was created."""
|
94
|
-
|
95
|
-
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
96
|
-
"""The timestamp when this item was last updated."""
|
97
|
-
|
98
|
-
ttl: Optional[int] = field(default=None)
|
99
|
-
"""The time to live for this item in seconds."""
|
100
|
-
|
101
|
-
filters: DatabaseItemFilters = field(default_factory=dict)
|
102
|
-
"""The filters that are associated with this item."""
|
103
|
-
|
104
|
-
table_name: str = field(default="default")
|
105
|
-
"""The table/collection name where this item is stored."""
|
106
|
-
|
107
|
-
score: Optional[float] = field(default=None)
|
108
|
-
"""The similarity score for this item (used in vector search results)."""
|
109
|
-
|
110
|
-
def is_expired(self) -> bool:
|
111
|
-
"""Check if this item has expired based on its TTL."""
|
112
|
-
if self.ttl is None:
|
113
|
-
return False
|
114
|
-
|
115
|
-
from datetime import timedelta
|
116
|
-
|
117
|
-
expires_at = self.created_at + timedelta(seconds=self.ttl)
|
118
|
-
return datetime.now(timezone.utc) >= expires_at
|
119
|
-
|
120
|
-
def expires_at(self) -> Optional[datetime]:
|
121
|
-
"""Calculate when this item will expire."""
|
122
|
-
if self.ttl is None:
|
123
|
-
return None
|
124
|
-
|
125
|
-
from datetime import timedelta
|
126
|
-
|
127
|
-
return self.created_at + timedelta(seconds=self.ttl)
|