hammad-python 0.0.10__py3-none-any.whl → 0.0.11__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 (74) hide show
  1. hammad/__init__.py +64 -10
  2. hammad/based/__init__.py +52 -0
  3. hammad/based/fields.py +546 -0
  4. hammad/based/model.py +968 -0
  5. hammad/based/utils.py +455 -0
  6. hammad/cache/__init__.py +30 -0
  7. hammad/{cache.py → cache/_cache.py} +83 -12
  8. hammad/cli/__init__.py +25 -0
  9. hammad/cli/plugins/__init__.py +786 -0
  10. hammad/cli/styles/__init__.py +5 -0
  11. hammad/cli/styles/animations.py +548 -0
  12. hammad/cli/styles/settings.py +135 -0
  13. hammad/cli/styles/types.py +358 -0
  14. hammad/cli/styles/utils.py +480 -0
  15. hammad/data/__init__.py +51 -0
  16. hammad/data/collections/__init__.py +32 -0
  17. hammad/data/collections/base_collection.py +58 -0
  18. hammad/data/collections/collection.py +227 -0
  19. hammad/data/collections/searchable_collection.py +556 -0
  20. hammad/data/collections/vector_collection.py +497 -0
  21. hammad/data/databases/__init__.py +21 -0
  22. hammad/data/databases/database.py +551 -0
  23. hammad/data/types/__init__.py +33 -0
  24. hammad/data/types/files/__init__.py +1 -0
  25. hammad/data/types/files/audio.py +81 -0
  26. hammad/data/types/files/configuration.py +475 -0
  27. hammad/data/types/files/document.py +195 -0
  28. hammad/data/types/files/file.py +358 -0
  29. hammad/data/types/files/image.py +80 -0
  30. hammad/json/__init__.py +21 -0
  31. hammad/{utils/json → json}/converters.py +4 -1
  32. hammad/logging/__init__.py +27 -0
  33. hammad/logging/decorators.py +432 -0
  34. hammad/logging/logger.py +534 -0
  35. hammad/pydantic/__init__.py +43 -0
  36. hammad/{utils/pydantic → pydantic}/converters.py +2 -1
  37. hammad/pydantic/models/__init__.py +28 -0
  38. hammad/pydantic/models/arbitrary_model.py +46 -0
  39. hammad/pydantic/models/cacheable_model.py +79 -0
  40. hammad/pydantic/models/fast_model.py +318 -0
  41. hammad/pydantic/models/function_model.py +176 -0
  42. hammad/pydantic/models/subscriptable_model.py +63 -0
  43. hammad/text/__init__.py +37 -0
  44. hammad/text/text.py +1068 -0
  45. hammad/text/utils/__init__.py +1 -0
  46. hammad/{utils/text → text/utils}/converters.py +2 -2
  47. hammad/text/utils/markdown/__init__.py +1 -0
  48. hammad/{utils → text/utils}/markdown/converters.py +3 -3
  49. hammad/{utils → text/utils}/markdown/formatting.py +1 -1
  50. hammad/{utils/typing/utils.py → typing/__init__.py} +75 -2
  51. hammad/web/__init__.py +42 -0
  52. hammad/web/http/__init__.py +1 -0
  53. hammad/web/http/client.py +944 -0
  54. hammad/web/openapi/client.py +740 -0
  55. hammad/web/search/__init__.py +1 -0
  56. hammad/web/search/client.py +936 -0
  57. hammad/web/utils.py +463 -0
  58. hammad/yaml/__init__.py +30 -0
  59. hammad/yaml/converters.py +19 -0
  60. {hammad_python-0.0.10.dist-info → hammad_python-0.0.11.dist-info}/METADATA +14 -8
  61. hammad_python-0.0.11.dist-info/RECORD +65 -0
  62. hammad/database.py +0 -447
  63. hammad/logger.py +0 -273
  64. hammad/types/color.py +0 -951
  65. hammad/utils/json/__init__.py +0 -0
  66. hammad/utils/markdown/__init__.py +0 -0
  67. hammad/utils/pydantic/__init__.py +0 -0
  68. hammad/utils/text/__init__.py +0 -0
  69. hammad/utils/typing/__init__.py +0 -0
  70. hammad_python-0.0.10.dist-info/RECORD +0 -22
  71. /hammad/{types/__init__.py → py.typed} +0 -0
  72. /hammad/{utils → web/openapi}/__init__.py +0 -0
  73. {hammad_python-0.0.10.dist-info → hammad_python-0.0.11.dist-info}/WHEEL +0 -0
  74. {hammad_python-0.0.10.dist-info → hammad_python-0.0.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,497 @@
1
+ """hammad.data.collections.vector_collection"""
2
+
3
+ import uuid
4
+ from typing import Any, Dict, Optional, List, Generic, Union, Callable
5
+ from datetime import datetime, timezone, timedelta
6
+
7
+ try:
8
+ from qdrant_client import QdrantClient
9
+ from qdrant_client.models import (
10
+ Distance,
11
+ VectorParams,
12
+ PointStruct,
13
+ Filter,
14
+ FieldCondition,
15
+ MatchValue,
16
+ SearchRequest,
17
+ QueryResponse,
18
+ )
19
+ import numpy as np
20
+ except ImportError as e:
21
+ raise ImportError(
22
+ "qdrant-client is required for VectorCollection. "
23
+ "Install with: pip install qdrant-client"
24
+ "Or install the the `ai` extra: `pip install hammad-python[ai]`"
25
+ ) from e
26
+
27
+ from .base_collection import BaseCollection, Object, Filters, Schema
28
+
29
+ __all__ = ("VectorCollection",)
30
+
31
+
32
+ class VectorCollection(BaseCollection, Generic[Object]):
33
+ """
34
+ Vector collection class that uses Qdrant for vector storage and similarity search.
35
+
36
+ This provides vector-based functionality for storing embeddings and performing
37
+ semantic similarity searches.
38
+ """
39
+
40
+ # Namespace UUID for generating deterministic UUIDs from string IDs
41
+ _NAMESPACE_UUID = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
42
+
43
+ def __init__(
44
+ self,
45
+ name: str,
46
+ vector_size: int,
47
+ schema: Optional[Schema] = None,
48
+ default_ttl: Optional[int] = None,
49
+ storage_backend: Optional[Any] = None,
50
+ distance_metric: Distance = Distance.DOT,
51
+ qdrant_config: Optional[Dict[str, Any]] = None,
52
+ embedding_function: Optional[Callable[[Any], List[float]]] = None,
53
+ ):
54
+ """
55
+ Initialize a vector collection.
56
+
57
+ Args:
58
+ name: The name of the collection
59
+ vector_size: Size/dimension of the vectors to store
60
+ schema: Optional schema for type validation
61
+ default_ttl: Default TTL for items in seconds
62
+ storage_backend: Optional storage backend (Database instance or custom)
63
+ distance_metric: Distance metric for similarity search (COSINE, DOT, EUCLID, MANHATTAN)
64
+ qdrant_config: Optional Qdrant configuration
65
+ Example: {
66
+ "path": "/path/to/db", # For persistent storage
67
+ "host": "localhost", # For remote Qdrant
68
+ "port": 6333,
69
+ "grpc_port": 6334,
70
+ "prefer_grpc": True,
71
+ "api_key": "your-api-key"
72
+ }
73
+ embedding_function: Optional function to convert objects to vectors
74
+ """
75
+ self.name = name
76
+ self.vector_size = vector_size
77
+ self.schema = schema
78
+ self.default_ttl = default_ttl
79
+ self.distance_metric = distance_metric
80
+ self._storage_backend = storage_backend
81
+ self._embedding_function = embedding_function
82
+
83
+ # Store qdrant configuration
84
+ self._qdrant_config = qdrant_config or {}
85
+
86
+ # In-memory storage when used independently
87
+ self._items: Dict[str, Dict[str, Any]] = {}
88
+
89
+ # Mapping from original IDs to UUIDs
90
+ self._id_mapping: Dict[str, str] = {}
91
+
92
+ # Initialize Qdrant client
93
+ self._init_qdrant_client()
94
+
95
+ def _init_qdrant_client(self):
96
+ """Initialize the Qdrant client and collection."""
97
+ config = self._qdrant_config
98
+
99
+ if "path" in config:
100
+ # Persistent local storage
101
+ self._client = QdrantClient(path=config["path"])
102
+ elif "host" in config:
103
+ # Remote Qdrant server
104
+ self._client = QdrantClient(
105
+ host=config.get("host", "localhost"),
106
+ port=config.get("port", 6333),
107
+ grpc_port=config.get("grpc_port", 6334),
108
+ prefer_grpc=config.get("prefer_grpc", False),
109
+ api_key=config.get("api_key"),
110
+ timeout=config.get("timeout"),
111
+ )
112
+ else:
113
+ # In-memory database (default)
114
+ self._client = QdrantClient(":memory:")
115
+
116
+ # Create collection if it doesn't exist
117
+ try:
118
+ collections = self._client.get_collections()
119
+ collection_names = [col.name for col in collections.collections]
120
+
121
+ if self.name not in collection_names:
122
+ self._client.create_collection(
123
+ collection_name=self.name,
124
+ vectors_config=VectorParams(
125
+ size=self.vector_size, distance=self.distance_metric
126
+ ),
127
+ )
128
+ except Exception as e:
129
+ # Collection might already exist or other issue
130
+ pass
131
+
132
+ def _ensure_uuid(self, id_str: str) -> str:
133
+ """Convert a string ID to a UUID string, or validate if already a UUID."""
134
+ # Check if it's already a valid UUID
135
+ try:
136
+ uuid.UUID(id_str)
137
+ return id_str
138
+ except ValueError:
139
+ # Not a valid UUID, create a deterministic one
140
+ new_uuid = str(uuid.uuid5(self._NAMESPACE_UUID, id_str))
141
+ self._id_mapping[id_str] = new_uuid
142
+ return new_uuid
143
+
144
+ def __repr__(self) -> str:
145
+ item_count = len(self._items) if self._storage_backend is None else "managed"
146
+ return f"<{self.__class__.__name__} name='{self.name}' vector_size={self.vector_size} items={item_count}>"
147
+
148
+ def _calculate_expires_at(self, ttl: Optional[int]) -> Optional[datetime]:
149
+ """Calculate expiry time based on TTL."""
150
+ if ttl is None:
151
+ ttl = self.default_ttl
152
+ if ttl and ttl > 0:
153
+ return datetime.now(timezone.utc) + timedelta(seconds=ttl)
154
+ return None
155
+
156
+ def _is_expired(self, expires_at: Optional[datetime]) -> bool:
157
+ """Check if an item has expired."""
158
+ if expires_at is None:
159
+ return False
160
+ now = datetime.now(timezone.utc)
161
+ if expires_at.tzinfo is None:
162
+ expires_at = expires_at.replace(tzinfo=timezone.utc)
163
+ return now >= expires_at
164
+
165
+ def _match_filters(
166
+ self, stored: Optional[Filters], query: Optional[Filters]
167
+ ) -> bool:
168
+ """Check if stored filters match query filters."""
169
+ if query is None:
170
+ return True
171
+ if stored is None:
172
+ return False
173
+ return all(stored.get(k) == v for k, v in query.items())
174
+
175
+ def _prepare_vector(self, entry: Any) -> List[float]:
176
+ """Prepare vector from entry using embedding function or direct vector."""
177
+ if self._embedding_function:
178
+ return self._embedding_function(entry)
179
+ elif isinstance(entry, dict) and "vector" in entry:
180
+ vector = entry["vector"]
181
+ if isinstance(vector, np.ndarray):
182
+ return vector.tolist()
183
+ elif isinstance(vector, list):
184
+ return vector
185
+ else:
186
+ raise ValueError("Vector must be a list or numpy array")
187
+ elif isinstance(entry, (list, np.ndarray)):
188
+ if isinstance(entry, np.ndarray):
189
+ return entry.tolist()
190
+ return entry
191
+ else:
192
+ raise ValueError(
193
+ "Entry must contain 'vector' key, be a vector itself, "
194
+ "or embedding_function must be provided"
195
+ )
196
+
197
+ def _build_qdrant_filter(self, filters: Optional[Filters]) -> Optional[Filter]:
198
+ """Build Qdrant filter from filters dict."""
199
+ if not filters:
200
+ return None
201
+
202
+ conditions = []
203
+ for key, value in filters.items():
204
+ conditions.append(FieldCondition(key=key, match=MatchValue(value=value)))
205
+
206
+ if len(conditions) == 1:
207
+ return Filter(must=[conditions[0]])
208
+ else:
209
+ return Filter(must=conditions)
210
+
211
+ def get(self, id: str, *, filters: Optional[Filters] = None) -> Optional[Object]:
212
+ """Get an item by ID."""
213
+ if self._storage_backend is not None:
214
+ # Delegate to storage backend (Database instance)
215
+ return self._storage_backend.get(id, collection=self.name, filters=filters)
216
+
217
+ # Convert ID to UUID if needed
218
+ uuid_id = self._ensure_uuid(id)
219
+
220
+ # Independent operation
221
+ try:
222
+ points = self._client.retrieve(
223
+ collection_name=self.name,
224
+ ids=[uuid_id],
225
+ with_payload=True,
226
+ with_vectors=False,
227
+ )
228
+
229
+ if not points:
230
+ return None
231
+
232
+ point = points[0]
233
+ payload = point.payload or {}
234
+
235
+ # Check expiration
236
+ expires_at_str = payload.get("expires_at")
237
+ if expires_at_str:
238
+ expires_at = datetime.fromisoformat(expires_at_str)
239
+ if self._is_expired(expires_at):
240
+ # Delete expired item
241
+ self._client.delete(
242
+ collection_name=self.name, points_selector=[uuid_id]
243
+ )
244
+ return None
245
+
246
+ # Check filters - they are stored as top-level fields in payload
247
+ if filters:
248
+ for key, value in filters.items():
249
+ if payload.get(key) != value:
250
+ return None
251
+
252
+ return payload.get("value")
253
+
254
+ except Exception:
255
+ return None
256
+
257
+ def add(
258
+ self,
259
+ entry: Object,
260
+ *,
261
+ id: Optional[str] = None,
262
+ filters: Optional[Filters] = None,
263
+ ttl: Optional[int] = None,
264
+ ) -> None:
265
+ """Add an item to the collection."""
266
+ if self._storage_backend is not None:
267
+ # Delegate to storage backend
268
+ self._storage_backend.add(
269
+ entry, id=id, collection=self.name, filters=filters, ttl=ttl
270
+ )
271
+ return
272
+
273
+ # Independent operation
274
+ item_id = id or str(uuid.uuid4())
275
+ # Convert to UUID if needed
276
+ uuid_id = self._ensure_uuid(item_id)
277
+
278
+ expires_at = self._calculate_expires_at(ttl)
279
+ created_at = datetime.now(timezone.utc)
280
+
281
+ # Prepare vector
282
+ vector = self._prepare_vector(entry)
283
+
284
+ if len(vector) != self.vector_size:
285
+ raise ValueError(
286
+ f"Vector size {len(vector)} doesn't match collection size {self.vector_size}"
287
+ )
288
+
289
+ # Prepare payload - store original ID if converted
290
+ payload = {
291
+ "value": entry,
292
+ "created_at": created_at.isoformat(),
293
+ "updated_at": created_at.isoformat(),
294
+ }
295
+
296
+ # Add filter fields as top-level payload fields
297
+ if filters:
298
+ for key, value in filters.items():
299
+ payload[key] = value
300
+
301
+ # Store original ID if it was converted
302
+ if item_id != uuid_id:
303
+ payload["original_id"] = item_id
304
+
305
+ if expires_at:
306
+ payload["expires_at"] = expires_at.isoformat()
307
+
308
+ # Store in memory with UUID
309
+ self._items[uuid_id] = payload
310
+
311
+ # Create point and upsert to Qdrant
312
+ point = PointStruct(id=uuid_id, vector=vector, payload=payload)
313
+
314
+ self._client.upsert(collection_name=self.name, points=[point])
315
+
316
+ def query(
317
+ self,
318
+ *,
319
+ filters: Optional[Filters] = None,
320
+ search: Optional[str] = None,
321
+ limit: Optional[int] = None,
322
+ ) -> List[Object]:
323
+ """Query items from the collection."""
324
+ if self._storage_backend is not None:
325
+ return self._storage_backend.query(
326
+ collection=self.name,
327
+ filters=filters,
328
+ search=search,
329
+ limit=limit,
330
+ )
331
+
332
+ # For basic query without vector search, just return all items with filters
333
+ if search is None:
334
+ return self._query_all(filters=filters, limit=limit)
335
+
336
+ # If search is provided but no embedding function, treat as error
337
+ if self._embedding_function is None:
338
+ raise ValueError(
339
+ "Search query provided but no embedding_function configured. "
340
+ "Use vector_search() for direct vector similarity search."
341
+ )
342
+
343
+ # Convert search to vector and perform similarity search
344
+ query_vector = self._embedding_function(search)
345
+ return self.vector_search(
346
+ query_vector=query_vector, filters=filters, limit=limit
347
+ )
348
+
349
+ def _query_all(
350
+ self,
351
+ *,
352
+ filters: Optional[Filters] = None,
353
+ limit: Optional[int] = None,
354
+ ) -> List[Object]:
355
+ """Query all items with optional filters (no vector search)."""
356
+ try:
357
+ # Scroll through all points
358
+ points, _ = self._client.scroll(
359
+ collection_name=self.name,
360
+ scroll_filter=self._build_qdrant_filter(filters),
361
+ limit=limit or 100,
362
+ with_payload=True,
363
+ with_vectors=False,
364
+ )
365
+
366
+ results = []
367
+ for point in points:
368
+ payload = point.payload or {}
369
+
370
+ # Check expiration
371
+ expires_at_str = payload.get("expires_at")
372
+ if expires_at_str:
373
+ expires_at = datetime.fromisoformat(expires_at_str)
374
+ if self._is_expired(expires_at):
375
+ continue
376
+
377
+ results.append(payload.get("value"))
378
+
379
+ return results
380
+
381
+ except Exception:
382
+ return []
383
+
384
+ def vector_search(
385
+ self,
386
+ query_vector: Union[List[float], np.ndarray],
387
+ *,
388
+ filters: Optional[Filters] = None,
389
+ limit: Optional[int] = None,
390
+ score_threshold: Optional[float] = None,
391
+ ) -> List[Object]:
392
+ """
393
+ Perform vector similarity search.
394
+
395
+ Args:
396
+ query_vector: Query vector for similarity search
397
+ filters: Optional filters to apply
398
+ limit: Maximum number of results to return
399
+ score_threshold: Minimum similarity score threshold
400
+
401
+ Returns:
402
+ List of matching objects sorted by similarity score
403
+ """
404
+ if isinstance(query_vector, np.ndarray):
405
+ query_vector = query_vector.tolist()
406
+
407
+ if len(query_vector) != self.vector_size:
408
+ raise ValueError(
409
+ f"Query vector size {len(query_vector)} doesn't match collection size {self.vector_size}"
410
+ )
411
+
412
+ try:
413
+ results = self._client.query_points(
414
+ collection_name=self.name,
415
+ query=query_vector,
416
+ query_filter=self._build_qdrant_filter(filters),
417
+ limit=limit or 10,
418
+ score_threshold=score_threshold,
419
+ with_payload=True,
420
+ with_vectors=False,
421
+ )
422
+
423
+ matches = []
424
+ for result in results.points:
425
+ payload = result.payload or {}
426
+
427
+ # Check expiration
428
+ expires_at_str = payload.get("expires_at")
429
+ if expires_at_str:
430
+ expires_at = datetime.fromisoformat(expires_at_str)
431
+ if self._is_expired(expires_at):
432
+ continue
433
+
434
+ matches.append(payload.get("value"))
435
+
436
+ return matches
437
+
438
+ except Exception:
439
+ return []
440
+
441
+ def get_vector(self, id: str) -> Optional[List[float]]:
442
+ """Get the vector for a specific item by ID."""
443
+ # Convert ID to UUID if needed
444
+ uuid_id = self._ensure_uuid(id)
445
+
446
+ try:
447
+ points = self._client.retrieve(
448
+ collection_name=self.name,
449
+ ids=[uuid_id],
450
+ with_payload=False,
451
+ with_vectors=True,
452
+ )
453
+
454
+ if not points:
455
+ return None
456
+
457
+ vector = points[0].vector
458
+ if isinstance(vector, dict):
459
+ # Handle named vectors if used
460
+ return list(vector.values())[0] if vector else None
461
+ return vector
462
+
463
+ except Exception:
464
+ return None
465
+
466
+ def delete(self, id: str) -> bool:
467
+ """Delete an item by ID."""
468
+ # Convert ID to UUID if needed
469
+ uuid_id = self._ensure_uuid(id)
470
+
471
+ try:
472
+ self._client.delete(collection_name=self.name, points_selector=[uuid_id])
473
+ # Remove from in-memory storage if exists
474
+ self._items.pop(uuid_id, None)
475
+ return True
476
+ except Exception:
477
+ return False
478
+
479
+ def count(self, *, filters: Optional[Filters] = None) -> int:
480
+ """Count items in the collection."""
481
+ try:
482
+ info = self._client.count(
483
+ collection_name=self.name,
484
+ count_filter=self._build_qdrant_filter(filters),
485
+ exact=True,
486
+ )
487
+ return info.count
488
+ except Exception:
489
+ return 0
490
+
491
+ def attach_to_database(self, database: Any) -> None:
492
+ """Attach this collection to a database instance."""
493
+ self._storage_backend = database
494
+ # Ensure the collection exists in the database
495
+ database.create_collection(
496
+ self.name, schema=self.schema, default_ttl=self.default_ttl
497
+ )
@@ -0,0 +1,21 @@
1
+ """hammad.data.databases"""
2
+
3
+ from typing import TYPE_CHECKING
4
+ from ...based.utils import auto_create_lazy_loader
5
+
6
+ if TYPE_CHECKING:
7
+ from .database import Database, create_database
8
+
9
+
10
+ __all__ = (
11
+ "Database",
12
+ "create_database",
13
+ )
14
+
15
+
16
+ __getattr__ = auto_create_lazy_loader(__all__)
17
+
18
+
19
+ def __dir__() -> list[str]:
20
+ """Get the attributes of the data.databases module."""
21
+ return list(__all__)