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.
- hammad/__init__.py +64 -10
- hammad/based/__init__.py +52 -0
- hammad/based/fields.py +546 -0
- hammad/based/model.py +968 -0
- hammad/based/utils.py +455 -0
- hammad/cache/__init__.py +30 -0
- hammad/{cache.py → cache/_cache.py} +83 -12
- hammad/cli/__init__.py +25 -0
- hammad/cli/plugins/__init__.py +786 -0
- hammad/cli/styles/__init__.py +5 -0
- hammad/cli/styles/animations.py +548 -0
- hammad/cli/styles/settings.py +135 -0
- hammad/cli/styles/types.py +358 -0
- hammad/cli/styles/utils.py +480 -0
- hammad/data/__init__.py +51 -0
- hammad/data/collections/__init__.py +32 -0
- hammad/data/collections/base_collection.py +58 -0
- hammad/data/collections/collection.py +227 -0
- hammad/data/collections/searchable_collection.py +556 -0
- hammad/data/collections/vector_collection.py +497 -0
- hammad/data/databases/__init__.py +21 -0
- hammad/data/databases/database.py +551 -0
- hammad/data/types/__init__.py +33 -0
- hammad/data/types/files/__init__.py +1 -0
- hammad/data/types/files/audio.py +81 -0
- hammad/data/types/files/configuration.py +475 -0
- hammad/data/types/files/document.py +195 -0
- hammad/data/types/files/file.py +358 -0
- hammad/data/types/files/image.py +80 -0
- hammad/json/__init__.py +21 -0
- hammad/{utils/json → json}/converters.py +4 -1
- hammad/logging/__init__.py +27 -0
- hammad/logging/decorators.py +432 -0
- hammad/logging/logger.py +534 -0
- hammad/pydantic/__init__.py +43 -0
- hammad/{utils/pydantic → pydantic}/converters.py +2 -1
- hammad/pydantic/models/__init__.py +28 -0
- hammad/pydantic/models/arbitrary_model.py +46 -0
- hammad/pydantic/models/cacheable_model.py +79 -0
- hammad/pydantic/models/fast_model.py +318 -0
- hammad/pydantic/models/function_model.py +176 -0
- hammad/pydantic/models/subscriptable_model.py +63 -0
- hammad/text/__init__.py +37 -0
- hammad/text/text.py +1068 -0
- hammad/text/utils/__init__.py +1 -0
- hammad/{utils/text → text/utils}/converters.py +2 -2
- hammad/text/utils/markdown/__init__.py +1 -0
- hammad/{utils → text/utils}/markdown/converters.py +3 -3
- hammad/{utils → text/utils}/markdown/formatting.py +1 -1
- hammad/{utils/typing/utils.py → typing/__init__.py} +75 -2
- hammad/web/__init__.py +42 -0
- hammad/web/http/__init__.py +1 -0
- hammad/web/http/client.py +944 -0
- hammad/web/openapi/client.py +740 -0
- hammad/web/search/__init__.py +1 -0
- hammad/web/search/client.py +936 -0
- hammad/web/utils.py +463 -0
- hammad/yaml/__init__.py +30 -0
- hammad/yaml/converters.py +19 -0
- {hammad_python-0.0.10.dist-info → hammad_python-0.0.11.dist-info}/METADATA +14 -8
- hammad_python-0.0.11.dist-info/RECORD +65 -0
- hammad/database.py +0 -447
- hammad/logger.py +0 -273
- hammad/types/color.py +0 -951
- hammad/utils/json/__init__.py +0 -0
- hammad/utils/markdown/__init__.py +0 -0
- hammad/utils/pydantic/__init__.py +0 -0
- hammad/utils/text/__init__.py +0 -0
- hammad/utils/typing/__init__.py +0 -0
- hammad_python-0.0.10.dist-info/RECORD +0 -22
- /hammad/{types/__init__.py → py.typed} +0 -0
- /hammad/{utils → web/openapi}/__init__.py +0 -0
- {hammad_python-0.0.10.dist-info → hammad_python-0.0.11.dist-info}/WHEEL +0 -0
- {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)
|