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
hammad/database.py DELETED
@@ -1,447 +0,0 @@
1
- """hammad.database
2
-
3
- Contains the `Database` class, which is an incredibly simple to use
4
- wrapper over various modules from the `sqlalchemy` library, which allows
5
- for the creation of a simple, yet powerful database interface.
6
- """
7
-
8
- import uuid
9
- import json
10
- from contextlib import contextmanager
11
- from datetime import datetime, timezone, timedelta
12
- from dataclasses import dataclass, field, asdict
13
- from typing import (
14
- Any,
15
- Dict,
16
- Optional,
17
- List,
18
- Iterator,
19
- Literal,
20
- Tuple,
21
- Type,
22
- TypeVar,
23
- Generic,
24
- TypeAlias,
25
- Union,
26
- cast,
27
- )
28
- import threading
29
-
30
- from sqlalchemy import (
31
- create_engine,
32
- Engine,
33
- event,
34
- pool,
35
- Column,
36
- String,
37
- DateTime,
38
- Integer,
39
- JSON as SQLAlchemyJSON,
40
- MetaData,
41
- and_,
42
- )
43
- from sqlalchemy.orm import Session as SQLAlchemySession, declarative_base
44
- from sqlalchemy.exc import SQLAlchemyError
45
- from pydantic import BaseModel
46
-
47
- from .logger import get_logger
48
-
49
-
50
- logger = get_logger(__name__)
51
-
52
-
53
- # -----------------------------------------------------------------------------
54
- # Types
55
- # -----------------------------------------------------------------------------
56
-
57
-
58
- DatabaseLocation: TypeAlias = Literal[
59
- "memory",
60
- "disk",
61
- ]
62
- """The location to initialize or load a database from. The `hammad` definition
63
- of a database is quite literal, and does not mean anything more than a persistable
64
- collection of data items. Concepts like queries, transactions, and other
65
- functional database concepts are not supported, as they are not the focus of this
66
- library."""
67
-
68
-
69
- DatabaseEntryType = TypeVar("DatabaseEntryType", bound=BaseModel | Dict | Any)
70
- """Helper type variable for input objects added to a database."""
71
-
72
-
73
- DatabaseSchema: TypeAlias = Union[
74
- Type[DatabaseEntryType], Type[BaseModel], Dict[str, Any], None
75
- ]
76
- """An optional schema type that a database must follow for a specific collection
77
- of data items. Databases are built around `collections`, and a single collection can
78
- adhere to a schema, or not."""
79
-
80
-
81
- DatabaseCollection: TypeAlias = Union[Literal["default"], str]
82
- """Helper type alias that provides common names for a collection within
83
- a database. Only the `default` collection is created automatically, if no
84
- section name is provided when adding objects."""
85
-
86
-
87
- DatabaseFilters: TypeAlias = Dict[str, str | int | float | bool | None]
88
- """Alias for a filter that can be used to create even more easy 'complexity' within a
89
- database. When adding items to a database, you can specify something like
90
-
91
- ```python
92
- database.add(
93
- ...,
94
- filter = {
95
- "some_key" : "some_value",
96
- "some_other_key" : 123,
97
- "some_third_key" : True,
98
- "some_fourth_key" : None,
99
- }
100
- )
101
- """
102
-
103
-
104
- _DatabaseEntry = declarative_base()
105
-
106
-
107
- class DatabaseEntry(_DatabaseEntry):
108
- """
109
- Base class definition for an data item within a database or
110
- a collection. This class is used to define both the strictly
111
- schema defined, and standard key-value data item types used within
112
- hammad.
113
- """
114
-
115
- __tablename__: str
116
- __tablename__ = "database_entries"
117
- __table_args__ = {"extend_existing": True}
118
-
119
- id: Column[str] = Column(String(255), primary_key=True, index=True)
120
- """
121
- The ID of the data item.
122
- """
123
- value: Column[DatabaseEntryType] = Column(SQLAlchemyJSON, nullable=False)
124
- """
125
- The value of an data item.
126
- """
127
- collection: Column[str] = Column(String(255), default="default", index=True)
128
- """
129
- The collection that the data item belongs to.
130
- """
131
- filters: Column[DatabaseFilters] = Column(SQLAlchemyJSON, nullable=True)
132
- """
133
- Any additional filters that belong to this data item.
134
- """
135
- created_at: Column[datetime] = Column(
136
- DateTime, default=lambda: datetime.now(timezone.utc)
137
- )
138
- """
139
- The timestamp of when the data item was created.
140
- """
141
- updated_at: Column[datetime] = Column(
142
- DateTime,
143
- default=lambda: datetime.now(timezone.utc),
144
- onupdate=lambda: datetime.now(timezone.utc),
145
- )
146
- """
147
- The timestamp of when the data item was last updated.
148
- """
149
- expires_at: Column[datetime] = Column(DateTime, nullable=True, index=True)
150
- """
151
- The timestamp of when the data item will expire.
152
- """
153
-
154
- def __repr__(self) -> str:
155
- return f"<DatabaseEntry id={self.id} collection={self.collection}>"
156
-
157
-
158
- # -----------------------------------------------------------------------------
159
- # Database
160
- # -----------------------------------------------------------------------------
161
-
162
-
163
- class Database(Generic[DatabaseEntryType]):
164
- """
165
- A simple, yet powerful database interface that allows for the creation of
166
- a database that can be used to store and retrieve data items.
167
- """
168
-
169
- def __init__(
170
- self,
171
- location: DatabaseLocation = "memory",
172
- *,
173
- default_ttl: Optional[int] = None,
174
- verbose: bool = False,
175
- ):
176
- """
177
- Creates a new database.
178
-
179
- Args:
180
- location (DatabaseLocation, optional): The location to initialize or load a database from. Defaults to "memory".
181
- default_ttl (Optional[int], optional): The default TTL for items in the database. Defaults to None.
182
- verbose (bool, optional): Whether to log verbose information. Defaults to False.
183
- """
184
- self.location = location
185
- self.default_ttl = default_ttl
186
- self.verbose = verbose
187
-
188
- # Thread-safety for in-memory operations
189
- self._lock = threading.Lock()
190
-
191
- # Per-collection metadata
192
- self._schemas: Dict[str, Optional[DatabaseSchema]] = {}
193
- self._collection_ttls: Dict[str, Optional[int]] = {}
194
-
195
- # In-memory store → {collection: {id: item_dict}}
196
- self._storage: Dict[str, Dict[str, Dict[str, Any]]] = {"default": {}}
197
-
198
- # Engine for on-disk mode
199
- self._engine: Optional[Engine] = None
200
- if self.location == "disk":
201
- db_url = "sqlite:///./hammad_database.db"
202
- self._engine = create_engine(
203
- db_url,
204
- echo=self.verbose,
205
- future=True,
206
- connect_args={"check_same_thread": False},
207
- poolclass=pool.StaticPool,
208
- )
209
- _DatabaseEntry.metadata.create_all(self._engine, checkfirst=True)
210
-
211
- if self.verbose:
212
- logger.info(f"Database initialised ({self.location}).")
213
-
214
- # ------------------------------------------------------------------
215
- # Private helpers
216
- # ------------------------------------------------------------------
217
-
218
- def _calculate_expires_at(
219
- self, ttl: Optional[int], collection: str
220
- ) -> Optional[datetime]:
221
- """Return an absolute expiry time for the given TTL (seconds)."""
222
- if ttl is None:
223
- ttl = self._collection_ttls.get(collection) or self.default_ttl
224
- if ttl and ttl > 0:
225
- return datetime.now(timezone.utc) + timedelta(seconds=ttl)
226
- return None
227
-
228
- def _is_expired(self, expires_at: Optional[datetime]) -> bool:
229
- if expires_at is None:
230
- return False
231
-
232
- now = datetime.now(timezone.utc)
233
- # Handle both timezone-aware and naive datetimes
234
- if expires_at.tzinfo is None:
235
- expires_at = expires_at.replace(tzinfo=timezone.utc)
236
-
237
- return now >= expires_at
238
-
239
- def _match_filters(
240
- self, stored: Optional[DatabaseFilters], query: Optional[DatabaseFilters]
241
- ) -> bool:
242
- if query is None:
243
- return True
244
- if stored is None:
245
- return False
246
- return all(stored.get(k) == v for k, v in query.items())
247
-
248
- def _serialize_item(self, item: Any, schema: Optional[DatabaseSchema]) -> Any:
249
- if schema is None:
250
- return item
251
- if hasattr(item, "__dataclass_fields__"):
252
- return asdict(item)
253
- if hasattr(item, "model_dump"):
254
- return item.model_dump()
255
- if isinstance(item, dict):
256
- return item
257
- raise TypeError(
258
- f"Cannot serialize item of type {type(item)} for schema storage"
259
- )
260
-
261
- def _deserialize_item(self, data: Any, schema: Optional[DatabaseSchema]) -> Any:
262
- if schema is None or isinstance(schema, dict):
263
- return data
264
- try:
265
- if hasattr(schema, "model_validate"):
266
- return schema.model_validate(data)
267
- return schema(**data) # type: ignore[arg-type]
268
- except Exception:
269
- return data
270
-
271
- @contextmanager
272
- def _get_session(self) -> Iterator[SQLAlchemySession]:
273
- if self.location != "disk" or self._engine is None:
274
- raise RuntimeError(
275
- "A SQLAlchemy session is only available when using disk storage."
276
- )
277
- session = SQLAlchemySession(self._engine)
278
- try:
279
- yield session
280
- session.commit()
281
- except SQLAlchemyError:
282
- session.rollback()
283
- raise
284
- finally:
285
- session.close()
286
-
287
- def _ensure_collection(
288
- self,
289
- name: str,
290
- schema: Optional[DatabaseSchema] = None,
291
- default_ttl: Optional[int] = None,
292
- ) -> None:
293
- """Create a collection if it doesn't yet exist, honouring schema consistency."""
294
- with self._lock:
295
- if name in self._schemas:
296
- if schema is not None and self._schemas[name] != schema:
297
- raise TypeError(
298
- f"Collection '{name}' already exists with a different schema."
299
- )
300
- return
301
- self._schemas[name] = schema
302
- self._collection_ttls[name] = default_ttl
303
- self._storage.setdefault(name, {})
304
- if self.verbose:
305
- logger.debug(f"Created collection '{name}' (ttl={default_ttl})")
306
-
307
- def create_collection(
308
- self,
309
- name: str,
310
- schema: Optional[DatabaseSchema] = None,
311
- default_ttl: Optional[int] = None,
312
- ) -> None:
313
- """
314
- Creates a new collection within the database.
315
- """
316
- self._ensure_collection(name, schema=schema, default_ttl=default_ttl)
317
-
318
- def add(
319
- self,
320
- entry: DatabaseEntryType | Any,
321
- *,
322
- id: Optional[str] = None,
323
- collection: DatabaseCollection = "default",
324
- filters: Optional[DatabaseFilters] = None,
325
- ttl: Optional[int] = None,
326
- ) -> None:
327
- """Add an item to the database using an optional ID (key) to a specified
328
- collection.
329
-
330
- Example:
331
- ```python
332
- database.add(
333
- entry = {
334
- "name" : "John Doe",
335
- },
336
- filters = {
337
- "user_group" : "admin",
338
- "is_active" : True,
339
- }
340
- )
341
- ```
342
-
343
- Args:
344
- entry (DatabaseEntryType | Any): The item to add to the database.
345
- id (Optional[str], optional): The ID of the item. Defaults to None.
346
- collection (DatabaseCollection, optional): The collection to add the item to. Defaults to "default".
347
- filters (Optional[DatabaseFilters], optional): Any additional filters to apply to the item. Defaults to None.
348
- ttl (Optional[int], optional): The TTL for the item. Defaults to None.
349
- """
350
- self._ensure_collection(collection)
351
- schema = self._schemas.get(collection)
352
-
353
- item_id = id or str(uuid.uuid4())
354
- expires_at = self._calculate_expires_at(ttl, collection)
355
- serialized_value = self._serialize_item(entry, schema)
356
-
357
- if self.location == "memory":
358
- with self._lock:
359
- coll_store = self._storage.setdefault(collection, {})
360
- coll_store[item_id] = {
361
- "value": entry,
362
- "serialized": serialized_value,
363
- "filters": filters or {},
364
- "created_at": datetime.now(timezone.utc),
365
- "updated_at": datetime.now(timezone.utc),
366
- "expires_at": expires_at,
367
- }
368
- else: # disk
369
- with self._get_session() as session:
370
- row = (
371
- session.query(DatabaseEntry)
372
- .filter_by(id=item_id, collection=collection)
373
- .first()
374
- )
375
- if row:
376
- row.value = serialized_value
377
- row.filters = filters or {}
378
- row.updated_at = datetime.now(timezone.utc)
379
- row.expires_at = expires_at
380
- else:
381
- session.add(
382
- DatabaseEntry(
383
- id=item_id,
384
- collection=collection,
385
- value=serialized_value,
386
- filters=filters or {},
387
- created_at=datetime.now(timezone.utc),
388
- updated_at=datetime.now(timezone.utc),
389
- expires_at=expires_at,
390
- )
391
- )
392
-
393
- def get(
394
- self,
395
- id: str,
396
- *,
397
- collection: DatabaseCollection = "default",
398
- filters: Optional[DatabaseFilters] = None,
399
- ) -> Optional[DatabaseEntryType]:
400
- """Get an item from the database using an optional ID (key) to a specified
401
- collection.
402
-
403
- """
404
- # For memory databases, collection must exist in schemas
405
- if self.location == "memory" and collection not in self._schemas:
406
- return None
407
-
408
- schema = self._schemas.get(collection)
409
-
410
- if self.location == "memory":
411
- coll_store = self._storage.get(collection, {})
412
- item = coll_store.get(id)
413
- if not item:
414
- return None
415
- if self._is_expired(item["expires_at"]):
416
- with self._lock:
417
- del coll_store[id]
418
- return None
419
- if not self._match_filters(item.get("filters"), filters):
420
- return None
421
- return (
422
- item["value"]
423
- if schema is None
424
- else self._deserialize_item(item["serialized"], schema)
425
- )
426
- else:
427
- # For disk databases, ensure collection is tracked if we find data
428
- with self._get_session() as session:
429
- row = (
430
- session.query(DatabaseEntry)
431
- .filter_by(id=id, collection=collection)
432
- .first()
433
- )
434
- if not row:
435
- return None
436
-
437
- # Auto-register collection if found on disk but not in memory
438
- if collection not in self._schemas:
439
- self._ensure_collection(collection)
440
- schema = self._schemas.get(collection)
441
-
442
- if self._is_expired(row.expires_at):
443
- session.delete(row)
444
- return None
445
- if not self._match_filters(row.filters, filters):
446
- return None
447
- return self._deserialize_item(row.value, schema)