swiftpy-framework 0.1.0__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.
- swiftpy/.gitignore +0 -0
- swiftpy/__init__.py +572 -0
- swiftpy/admin/__init__.py +50 -0
- swiftpy/admin/auth.py +522 -0
- swiftpy/admin/registry.py +567 -0
- swiftpy/admin/views.py +1583 -0
- swiftpy/ai/__init__.py +199 -0
- swiftpy/ai/_deterministic.py +40 -0
- swiftpy/ai/_fake.py +604 -0
- swiftpy/ai/agents/__init__.py +30 -0
- swiftpy/ai/agents/base_agent.py +506 -0
- swiftpy/ai/agents/memory.py +864 -0
- swiftpy/ai/client.py +1011 -0
- swiftpy/ai/conversation.py +171 -0
- swiftpy/ai/embeddings.py +321 -0
- swiftpy/ai/exceptions.py +75 -0
- swiftpy/ai/history_strategies.py +157 -0
- swiftpy/ai/middleware.py +918 -0
- swiftpy/ai/observability/__init__.py +28 -0
- swiftpy/ai/observability/drivers.py +763 -0
- swiftpy/ai/observability/observer.py +364 -0
- swiftpy/ai/pool.py +102 -0
- swiftpy/ai/pricing.py +101 -0
- swiftpy/ai/prompts/__init__.py +6 -0
- swiftpy/ai/prompts/loader.py +343 -0
- swiftpy/ai/providers/__init__.py +22 -0
- swiftpy/ai/providers/anthropic_provider.py +353 -0
- swiftpy/ai/providers/base.py +273 -0
- swiftpy/ai/providers/openai_provider.py +275 -0
- swiftpy/ai/rag/__init__.py +53 -0
- swiftpy/ai/rag/context_formatter.py +263 -0
- swiftpy/ai/rag/pipeline_builder.py +544 -0
- swiftpy/ai/rag/reranker.py +484 -0
- swiftpy/ai/rag/result.py +78 -0
- swiftpy/ai/rag/retriever.py +315 -0
- swiftpy/ai/redis_store.py +227 -0
- swiftpy/ai/streaming.py +115 -0
- swiftpy/ai/tools/__init__.py +22 -0
- swiftpy/ai/tools/base_tool.py +489 -0
- swiftpy/ai/tools/first_party.py +384 -0
- swiftpy/ai/tools/tool_loop.py +417 -0
- swiftpy/ai/vectorstore/__init__.py +46 -0
- swiftpy/ai/vectorstore/base.py +542 -0
- swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
- swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
- swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
- swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
- swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
- swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
- swiftpy/auth/__init__.py +50 -0
- swiftpy/auth/api_key_guard.py +544 -0
- swiftpy/auth/auth_manager.py +259 -0
- swiftpy/auth/exceptions.py +71 -0
- swiftpy/auth/jwt_guard.py +230 -0
- swiftpy/auth/jwt_issuer.py +658 -0
- swiftpy/auth/token_blacklist.py +300 -0
- swiftpy/cache/__init__.py +14 -0
- swiftpy/cache/cache.py +251 -0
- swiftpy/cache/driver.py +216 -0
- swiftpy/cache/drivers/memory.py +199 -0
- swiftpy/cache/drivers/redis.py +421 -0
- swiftpy/console/__init__.py +0 -0
- swiftpy/console/cli.py +68 -0
- swiftpy/console/commands/__init__.py +0 -0
- swiftpy/console/commands/cache.py +164 -0
- swiftpy/console/commands/make.py +935 -0
- swiftpy/console/commands/migrate.py +280 -0
- swiftpy/console/commands/new.py +1806 -0
- swiftpy/console/commands/queue.py +306 -0
- swiftpy/console/commands/seed.py +252 -0
- swiftpy/console/commands/start.py +171 -0
- swiftpy/console/commands/storage.py +104 -0
- swiftpy/console/loader.py +21 -0
- swiftpy/console/stubs/__init__.py +26 -0
- swiftpy/console/stubs/controller.stub +14 -0
- swiftpy/console/stubs/event.stub +14 -0
- swiftpy/console/stubs/job.stub +22 -0
- swiftpy/console/stubs/listener.stub +14 -0
- swiftpy/console/stubs/mail.stub +20 -0
- swiftpy/console/stubs/middleware.stub +16 -0
- swiftpy/console/stubs/migration.alter.stub +17 -0
- swiftpy/console/stubs/migration.api_keys.stub +22 -0
- swiftpy/console/stubs/migration.blank.stub +11 -0
- swiftpy/console/stubs/migration.create.stub +15 -0
- swiftpy/console/stubs/migration.stub +11 -0
- swiftpy/console/stubs/migration.update.stub +17 -0
- swiftpy/console/stubs/model.stub +17 -0
- swiftpy/console/stubs/provider.stub +18 -0
- swiftpy/console/stubs/repository.stub +10 -0
- swiftpy/console/stubs/request.stub +16 -0
- swiftpy/console/stubs/resource.stub +21 -0
- swiftpy/console/stubs/schema.stub +12 -0
- swiftpy/console/stubs/seeder.stub +14 -0
- swiftpy/console/stubs/service.stub +10 -0
- swiftpy/core/__init__.py +0 -0
- swiftpy/core/application.py +1306 -0
- swiftpy/core/bootstrap.py +30 -0
- swiftpy/core/config.py +432 -0
- swiftpy/core/container.py +596 -0
- swiftpy/core/container_exceptions.py +18 -0
- swiftpy/core/context.py +148 -0
- swiftpy/core/exceptions.py +352 -0
- swiftpy/core/providers/base.py +35 -0
- swiftpy/database/__init__.py +41 -0
- swiftpy/database/exceptions.py +42 -0
- swiftpy/database/identifiers.py +105 -0
- swiftpy/database/migrations/__init__.py +36 -0
- swiftpy/database/migrations/blueprint.py +431 -0
- swiftpy/database/migrations/lock.py +104 -0
- swiftpy/database/migrations/migration.py +22 -0
- swiftpy/database/migrations/migrator.py +507 -0
- swiftpy/database/migrations/schema.py +435 -0
- swiftpy/database/orm/__init__.py +33 -0
- swiftpy/database/orm/fields.py +92 -0
- swiftpy/database/orm/model.py +2034 -0
- swiftpy/database/orm/registry.py +33 -0
- swiftpy/database/orm/relationships.py +836 -0
- swiftpy/database/pagination/__init__.py +16 -0
- swiftpy/database/pagination/contracts.py +60 -0
- swiftpy/database/pagination/paginator.py +112 -0
- swiftpy/database/pagination/simple_paginator.py +97 -0
- swiftpy/database/pool.py +218 -0
- swiftpy/database/query_builder.py +1803 -0
- swiftpy/database/seeder.py +39 -0
- swiftpy/database/transaction.py +276 -0
- swiftpy/events/__init__.py +10 -0
- swiftpy/events/dispatcher.py +308 -0
- swiftpy/events/redis_bus.py +552 -0
- swiftpy/filesystem/__init__.py +227 -0
- swiftpy/filesystem/disk.py +378 -0
- swiftpy/filesystem/drivers/__init__.py +11 -0
- swiftpy/filesystem/drivers/base.py +156 -0
- swiftpy/filesystem/drivers/local.py +559 -0
- swiftpy/filesystem/drivers/s3.py +655 -0
- swiftpy/filesystem/events.py +37 -0
- swiftpy/filesystem/exceptions.py +127 -0
- swiftpy/filesystem/manager.py +138 -0
- swiftpy/filesystem/metadata.py +22 -0
- swiftpy/filesystem/paths.py +99 -0
- swiftpy/filesystem/service_provider.py +17 -0
- swiftpy/filesystem/uploads/__init__.py +13 -0
- swiftpy/filesystem/uploads/handlers.py +362 -0
- swiftpy/filesystem/uploads/uploaded_file.py +222 -0
- swiftpy/filesystem/uploads/validators.py +186 -0
- swiftpy/filesystem/visibility.py +22 -0
- swiftpy/helpers/__init__.py +42 -0
- swiftpy/helpers/helper.py +180 -0
- swiftpy/http/exceptions.py +41 -0
- swiftpy/http/middleware.py +307 -0
- swiftpy/http/request.py +375 -0
- swiftpy/http/response.py +442 -0
- swiftpy/http/router.py +1050 -0
- swiftpy/mail/__init__.py +58 -0
- swiftpy/mail/attachments.py +255 -0
- swiftpy/mail/config.py +71 -0
- swiftpy/mail/envelope.py +183 -0
- swiftpy/mail/exceptions.py +90 -0
- swiftpy/mail/facade.py +312 -0
- swiftpy/mail/mailable.py +53 -0
- swiftpy/mail/manager.py +240 -0
- swiftpy/mail/message.py +186 -0
- swiftpy/mail/models.py +19 -0
- swiftpy/mail/queue/__init__.py +5 -0
- swiftpy/mail/queue/job.py +279 -0
- swiftpy/mail/service_provider.py +19 -0
- swiftpy/mail/templates/__init__.py +5 -0
- swiftpy/mail/templates/renderer.py +173 -0
- swiftpy/mail/transports/__init__.py +21 -0
- swiftpy/mail/transports/api/__init__.py +13 -0
- swiftpy/mail/transports/api/base.py +194 -0
- swiftpy/mail/transports/api/mailgun.py +158 -0
- swiftpy/mail/transports/api/resend.py +156 -0
- swiftpy/mail/transports/api/sendgrid.py +172 -0
- swiftpy/mail/transports/base.py +24 -0
- swiftpy/mail/transports/console.py +91 -0
- swiftpy/mail/transports/sendmail.py +128 -0
- swiftpy/mail/transports/smtp.py +466 -0
- swiftpy/queue/__init__.py +13 -0
- swiftpy/queue/dispatcher.py +813 -0
- swiftpy/queue/job.py +541 -0
- swiftpy/queue/worker.py +556 -0
- swiftpy/support/__init__.py +12 -0
- swiftpy/support/keys.py +108 -0
- swiftpy/support/redis_config.py +223 -0
- swiftpy/support/strings.py +114 -0
- swiftpy/testing/__init__.py +20 -0
- swiftpy/testing/fakes/__init__.py +11 -0
- swiftpy/testing/fakes/ai_fake.py +21 -0
- swiftpy/testing/fakes/vectorstore_fake.py +194 -0
- swiftpy/testing/fixtures.py +124 -0
- swiftpy/testing/http_client.py +730 -0
- swiftpy/testing/test_case.py +126 -0
- swiftpy/validation/__init__.py +97 -0
- swiftpy/validation/exceptions.py +26 -0
- swiftpy/validation/form_request.py +302 -0
- swiftpy/validation/protocols.py +33 -0
- swiftpy/validation/rules/__init__.py +89 -0
- swiftpy/validation/rules/builtin.py +391 -0
- swiftpy/validation/rules/exists.py +86 -0
- swiftpy/validation/rules/registry.py +290 -0
- swiftpy/validation/rules/required.py +55 -0
- swiftpy/validation/rules/unique.py +138 -0
- swiftpy/validation/validator.py +612 -0
- swiftpy/websockets/__init__.py +19 -0
- swiftpy/websockets/connection.py +317 -0
- swiftpy/websockets/manager.py +271 -0
- swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
- swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
- swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
- swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
- swiftpy_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
swiftpy/cache/driver.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("swiftpy.cache")
|
|
12
|
+
|
|
13
|
+
_SENTINEL = object()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _LockWithRef:
|
|
17
|
+
__slots__ = ("lock", "refs")
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self.lock = asyncio.Lock()
|
|
21
|
+
self.refs = 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CacheDriver(ABC):
|
|
25
|
+
"""
|
|
26
|
+
Abstract base class defining the contract for all SwiftPY cache drivers.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._remember_locks: dict[str, _LockWithRef] = {}
|
|
31
|
+
self._meta_lock = asyncio.Lock()
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
async def get(self, key: str, default: Any = None) -> Any:
|
|
35
|
+
"""
|
|
36
|
+
Retrieve an item from the cache by key.
|
|
37
|
+
|
|
38
|
+
If the item does not exist or has expired, return the default value.
|
|
39
|
+
"""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def set(
|
|
44
|
+
self,
|
|
45
|
+
key: str,
|
|
46
|
+
value: Any,
|
|
47
|
+
ttl: float | None = None,
|
|
48
|
+
) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Store an item in the cache with an optional TTL in seconds.
|
|
51
|
+
"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def forget(self, key: str) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Remove an item from the cache.
|
|
58
|
+
|
|
59
|
+
Returns True if the item was removed, False if it did not exist.
|
|
60
|
+
"""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
async def has(self, key: str) -> bool:
|
|
65
|
+
"""
|
|
66
|
+
Determine if an item exists in the cache and has not expired.
|
|
67
|
+
"""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
async def flush(self) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Remove all items from the cache driver.
|
|
74
|
+
"""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
async def pull(self, key: str, default: Any = None) -> Any:
|
|
78
|
+
"""
|
|
79
|
+
Retrieve an item from the cache and delete it.
|
|
80
|
+
"""
|
|
81
|
+
value = await self.get(key, _SENTINEL)
|
|
82
|
+
if value is _SENTINEL:
|
|
83
|
+
return default
|
|
84
|
+
await self.forget(key)
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
async def add(
|
|
88
|
+
self,
|
|
89
|
+
key: str,
|
|
90
|
+
value: Any,
|
|
91
|
+
ttl: float | None = None,
|
|
92
|
+
) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
Store an item in the cache if the key does not already exist.
|
|
95
|
+
|
|
96
|
+
Returns True if the item was added, False otherwise.
|
|
97
|
+
"""
|
|
98
|
+
if await self.has(key):
|
|
99
|
+
return False
|
|
100
|
+
return await self.set(key, value, ttl)
|
|
101
|
+
|
|
102
|
+
async def increment(
|
|
103
|
+
self, key: str, value: int = 1, ttl: float | None = None
|
|
104
|
+
) -> int:
|
|
105
|
+
"""
|
|
106
|
+
Increment the value of an item in the cache.
|
|
107
|
+
|
|
108
|
+
If the key does not exist, it is initialized to the given value with optional TTL.
|
|
109
|
+
"""
|
|
110
|
+
current = await self.get(key, _SENTINEL)
|
|
111
|
+
is_new = current is _SENTINEL
|
|
112
|
+
base = 0 if is_new else current
|
|
113
|
+
try:
|
|
114
|
+
new_value = int(base) + value
|
|
115
|
+
except (ValueError, TypeError):
|
|
116
|
+
raise TypeError(
|
|
117
|
+
f"Cannot increment non-numeric cached value of type {type(base).__name__}"
|
|
118
|
+
)
|
|
119
|
+
await self.set(key, new_value, ttl if is_new else None)
|
|
120
|
+
return new_value
|
|
121
|
+
|
|
122
|
+
async def decrement(
|
|
123
|
+
self, key: str, value: int = 1, ttl: float | None = None
|
|
124
|
+
) -> int:
|
|
125
|
+
"""
|
|
126
|
+
Decrement the value of an item in the cache.
|
|
127
|
+
"""
|
|
128
|
+
return await self.increment(key, -value, ttl)
|
|
129
|
+
|
|
130
|
+
@asynccontextmanager
|
|
131
|
+
async def _acquire_key_lock(
|
|
132
|
+
self, key: str, lock_timeout: float = 10.0
|
|
133
|
+
) -> AsyncIterator[bool]:
|
|
134
|
+
"""Acquire an in-process per-key lock with reference-counted cleanup."""
|
|
135
|
+
async with self._meta_lock:
|
|
136
|
+
if key not in self._remember_locks:
|
|
137
|
+
self._remember_locks[key] = _LockWithRef()
|
|
138
|
+
entry = self._remember_locks[key]
|
|
139
|
+
entry.refs += 1
|
|
140
|
+
|
|
141
|
+
acquired = False
|
|
142
|
+
try:
|
|
143
|
+
if lock_timeout <= 0:
|
|
144
|
+
if not entry.lock.locked():
|
|
145
|
+
await entry.lock.acquire()
|
|
146
|
+
acquired = True
|
|
147
|
+
else:
|
|
148
|
+
try:
|
|
149
|
+
await asyncio.wait_for(
|
|
150
|
+
entry.lock.acquire(), timeout=float(lock_timeout)
|
|
151
|
+
)
|
|
152
|
+
acquired = True
|
|
153
|
+
except TimeoutError:
|
|
154
|
+
acquired = False
|
|
155
|
+
|
|
156
|
+
yield acquired
|
|
157
|
+
finally:
|
|
158
|
+
if acquired:
|
|
159
|
+
entry.lock.release()
|
|
160
|
+
async with self._meta_lock:
|
|
161
|
+
entry.refs -= 1
|
|
162
|
+
if entry.refs <= 0:
|
|
163
|
+
self._remember_locks.pop(key, None)
|
|
164
|
+
|
|
165
|
+
async def remember(
|
|
166
|
+
self,
|
|
167
|
+
key: str,
|
|
168
|
+
ttl: float | None,
|
|
169
|
+
callback: Callable[[], Any] | Callable[[], Awaitable[Any]],
|
|
170
|
+
lock_timeout: float = 10.0,
|
|
171
|
+
) -> Any:
|
|
172
|
+
"""
|
|
173
|
+
Get an item from the cache, or execute the given callback and store the result.
|
|
174
|
+
|
|
175
|
+
Includes stampede protection via per-key locking so that concurrent
|
|
176
|
+
requests for a missing key execute the callback exactly once.
|
|
177
|
+
If lock acquisition times out, computes the value without caching and logs a warning.
|
|
178
|
+
"""
|
|
179
|
+
# Fast path check
|
|
180
|
+
cached = await self.get(key, _SENTINEL)
|
|
181
|
+
if cached is not _SENTINEL:
|
|
182
|
+
return cached
|
|
183
|
+
|
|
184
|
+
async with self._acquire_key_lock(key, lock_timeout=lock_timeout) as acquired:
|
|
185
|
+
if not acquired:
|
|
186
|
+
logger.warning(
|
|
187
|
+
"Cache lock timed out waiting for key '%s'; computing value directly without caching",
|
|
188
|
+
key,
|
|
189
|
+
)
|
|
190
|
+
if inspect.iscoroutinefunction(callback):
|
|
191
|
+
return await callback()
|
|
192
|
+
res = callback()
|
|
193
|
+
return await res if inspect.isawaitable(res) else res
|
|
194
|
+
|
|
195
|
+
# Double-checked locking
|
|
196
|
+
cached = await self.get(key, _SENTINEL)
|
|
197
|
+
if cached is not _SENTINEL:
|
|
198
|
+
return cached
|
|
199
|
+
|
|
200
|
+
# Execute callback (supporting both synchronous and asynchronous callables)
|
|
201
|
+
if inspect.iscoroutinefunction(callback):
|
|
202
|
+
result = await callback()
|
|
203
|
+
else:
|
|
204
|
+
res = callback()
|
|
205
|
+
if inspect.isawaitable(res):
|
|
206
|
+
result = await res
|
|
207
|
+
else:
|
|
208
|
+
result = res
|
|
209
|
+
|
|
210
|
+
await self.set(key, result, ttl)
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
async def close(self) -> None:
|
|
214
|
+
"""
|
|
215
|
+
Close any underlying resources, connections, or pools held by the driver.
|
|
216
|
+
"""
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import copy
|
|
5
|
+
import itertools
|
|
6
|
+
import time
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from swiftpy.cache.driver import _SENTINEL, CacheDriver
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MemoryDriver(CacheDriver):
|
|
15
|
+
"""
|
|
16
|
+
In-memory cache driver suitable for unit tests and local execution
|
|
17
|
+
without requiring an external Redis instance.
|
|
18
|
+
Includes LRU eviction when max_items is set and lazy expiry sweeping.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
prefix: str = "swiftpy_cache:",
|
|
24
|
+
default_ttl: float | None = None,
|
|
25
|
+
clock: Callable[[], float] = time.monotonic,
|
|
26
|
+
max_items: int | None = 1000,
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.prefix = prefix
|
|
30
|
+
self.default_ttl = default_ttl
|
|
31
|
+
self._clock = clock
|
|
32
|
+
self.max_items = max_items
|
|
33
|
+
self._storage: OrderedDict[str, tuple[Any, float | None]] = OrderedDict()
|
|
34
|
+
self._lock = asyncio.Lock()
|
|
35
|
+
self._sweep_cursor: int = 0
|
|
36
|
+
|
|
37
|
+
def _sweep_expired(self, max_keys: int = 20) -> None:
|
|
38
|
+
"""Lazily sweep up to max_keys expired entries using a rolling cursor."""
|
|
39
|
+
total = len(self._storage)
|
|
40
|
+
if total == 0 or max_keys <= 0:
|
|
41
|
+
if total == 0:
|
|
42
|
+
self._sweep_cursor = 0
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
start = self._sweep_cursor % total
|
|
46
|
+
batch = list(itertools.islice(self._storage.items(), start, start + max_keys))
|
|
47
|
+
now = self._clock()
|
|
48
|
+
deleted = 0
|
|
49
|
+
for k, (_, exp) in batch:
|
|
50
|
+
if exp is not None and now >= exp:
|
|
51
|
+
self._storage.pop(k, None)
|
|
52
|
+
deleted += 1
|
|
53
|
+
|
|
54
|
+
new_total = len(self._storage)
|
|
55
|
+
if new_total == 0:
|
|
56
|
+
self._sweep_cursor = 0
|
|
57
|
+
else:
|
|
58
|
+
scanned_surviving = len(batch) - deleted
|
|
59
|
+
self._sweep_cursor = (start + scanned_surviving) % new_total
|
|
60
|
+
|
|
61
|
+
def _prefixed_key(self, key: str) -> str:
|
|
62
|
+
return f"{self.prefix}{key}"
|
|
63
|
+
|
|
64
|
+
async def get(self, key: str, default: Any = None) -> Any:
|
|
65
|
+
full_key = self._prefixed_key(key)
|
|
66
|
+
raw_val: Any = _SENTINEL
|
|
67
|
+
async with self._lock:
|
|
68
|
+
if full_key in self._storage:
|
|
69
|
+
value, expires_at = self._storage[full_key]
|
|
70
|
+
if expires_at is not None and self._clock() >= expires_at:
|
|
71
|
+
del self._storage[full_key]
|
|
72
|
+
else:
|
|
73
|
+
self._storage.move_to_end(full_key)
|
|
74
|
+
raw_val = value
|
|
75
|
+
|
|
76
|
+
if raw_val is _SENTINEL:
|
|
77
|
+
return default
|
|
78
|
+
return copy.deepcopy(raw_val)
|
|
79
|
+
|
|
80
|
+
async def set(
|
|
81
|
+
self,
|
|
82
|
+
key: str,
|
|
83
|
+
value: Any,
|
|
84
|
+
ttl: float | None = None,
|
|
85
|
+
) -> bool:
|
|
86
|
+
full_key = self._prefixed_key(key)
|
|
87
|
+
effective_ttl = ttl if ttl is not None else self.default_ttl
|
|
88
|
+
val_to_store = copy.deepcopy(value)
|
|
89
|
+
|
|
90
|
+
async with self._lock:
|
|
91
|
+
self._sweep_expired()
|
|
92
|
+
if effective_ttl is not None and effective_ttl <= 0:
|
|
93
|
+
self._storage.pop(full_key, None)
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
expires_at = (
|
|
97
|
+
self._clock() + effective_ttl if effective_ttl is not None else None
|
|
98
|
+
)
|
|
99
|
+
self._storage[full_key] = (val_to_store, expires_at)
|
|
100
|
+
self._storage.move_to_end(full_key)
|
|
101
|
+
if self.max_items is not None and len(self._storage) > self.max_items:
|
|
102
|
+
self._storage.popitem(last=False)
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
async def forget(self, key: str) -> bool:
|
|
106
|
+
full_key = self._prefixed_key(key)
|
|
107
|
+
async with self._lock:
|
|
108
|
+
if full_key in self._storage:
|
|
109
|
+
del self._storage[full_key]
|
|
110
|
+
return True
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
async def has(self, key: str) -> bool:
|
|
114
|
+
return await self.get(key, _SENTINEL) is not _SENTINEL
|
|
115
|
+
|
|
116
|
+
async def flush(self) -> bool:
|
|
117
|
+
async with self._lock:
|
|
118
|
+
if not self.prefix:
|
|
119
|
+
self._storage.clear()
|
|
120
|
+
self._sweep_cursor = 0
|
|
121
|
+
else:
|
|
122
|
+
keys_to_delete = [k for k in self._storage if k.startswith(self.prefix)]
|
|
123
|
+
for k in keys_to_delete:
|
|
124
|
+
del self._storage[k]
|
|
125
|
+
if not self._storage:
|
|
126
|
+
self._sweep_cursor = 0
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
async def add(
|
|
130
|
+
self,
|
|
131
|
+
key: str,
|
|
132
|
+
value: Any,
|
|
133
|
+
ttl: float | None = None,
|
|
134
|
+
) -> bool:
|
|
135
|
+
full_key = self._prefixed_key(key)
|
|
136
|
+
effective_ttl = ttl if ttl is not None else self.default_ttl
|
|
137
|
+
val_to_store = copy.deepcopy(value)
|
|
138
|
+
|
|
139
|
+
async with self._lock:
|
|
140
|
+
self._sweep_expired()
|
|
141
|
+
if full_key in self._storage:
|
|
142
|
+
_, expires_at = self._storage[full_key]
|
|
143
|
+
if expires_at is None or self._clock() < expires_at:
|
|
144
|
+
return False
|
|
145
|
+
# Expired key can be replaced
|
|
146
|
+
del self._storage[full_key]
|
|
147
|
+
|
|
148
|
+
if effective_ttl is not None and effective_ttl <= 0:
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
expires_at = (
|
|
152
|
+
self._clock() + effective_ttl if effective_ttl is not None else None
|
|
153
|
+
)
|
|
154
|
+
self._storage[full_key] = (val_to_store, expires_at)
|
|
155
|
+
self._storage.move_to_end(full_key)
|
|
156
|
+
if self.max_items is not None and len(self._storage) > self.max_items:
|
|
157
|
+
self._storage.popitem(last=False)
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
async def increment(
|
|
161
|
+
self, key: str, value: int = 1, ttl: float | None = None
|
|
162
|
+
) -> int:
|
|
163
|
+
full_key = self._prefixed_key(key)
|
|
164
|
+
effective_ttl = ttl if ttl is not None else self.default_ttl
|
|
165
|
+
async with self._lock:
|
|
166
|
+
self._sweep_expired()
|
|
167
|
+
current: Any = 0
|
|
168
|
+
expires_at: float | None = None
|
|
169
|
+
|
|
170
|
+
if full_key in self._storage:
|
|
171
|
+
stored_val, stored_exp = self._storage[full_key]
|
|
172
|
+
if stored_exp is None or self._clock() < stored_exp:
|
|
173
|
+
current = stored_val
|
|
174
|
+
expires_at = stored_exp
|
|
175
|
+
else:
|
|
176
|
+
del self._storage[full_key]
|
|
177
|
+
if effective_ttl is not None and effective_ttl > 0:
|
|
178
|
+
expires_at = self._clock() + effective_ttl
|
|
179
|
+
else:
|
|
180
|
+
if effective_ttl is not None and effective_ttl > 0:
|
|
181
|
+
expires_at = self._clock() + effective_ttl
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
new_value = int(current) + value
|
|
185
|
+
except (ValueError, TypeError):
|
|
186
|
+
raise TypeError(
|
|
187
|
+
f"Cannot increment non-numeric cached value of type {type(current).__name__}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
self._storage[full_key] = (new_value, expires_at)
|
|
191
|
+
self._storage.move_to_end(full_key)
|
|
192
|
+
if self.max_items is not None and len(self._storage) > self.max_items:
|
|
193
|
+
self._storage.popitem(last=False)
|
|
194
|
+
return new_value
|
|
195
|
+
|
|
196
|
+
async def close(self) -> None:
|
|
197
|
+
async with self._lock:
|
|
198
|
+
self._storage.clear()
|
|
199
|
+
self._sweep_cursor = 0
|