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