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.
Files changed (211) hide show
  1. swiftpy/.gitignore +0 -0
  2. swiftpy/__init__.py +572 -0
  3. swiftpy/admin/__init__.py +50 -0
  4. swiftpy/admin/auth.py +522 -0
  5. swiftpy/admin/registry.py +567 -0
  6. swiftpy/admin/views.py +1583 -0
  7. swiftpy/ai/__init__.py +199 -0
  8. swiftpy/ai/_deterministic.py +40 -0
  9. swiftpy/ai/_fake.py +604 -0
  10. swiftpy/ai/agents/__init__.py +30 -0
  11. swiftpy/ai/agents/base_agent.py +506 -0
  12. swiftpy/ai/agents/memory.py +864 -0
  13. swiftpy/ai/client.py +1011 -0
  14. swiftpy/ai/conversation.py +171 -0
  15. swiftpy/ai/embeddings.py +321 -0
  16. swiftpy/ai/exceptions.py +75 -0
  17. swiftpy/ai/history_strategies.py +157 -0
  18. swiftpy/ai/middleware.py +918 -0
  19. swiftpy/ai/observability/__init__.py +28 -0
  20. swiftpy/ai/observability/drivers.py +763 -0
  21. swiftpy/ai/observability/observer.py +364 -0
  22. swiftpy/ai/pool.py +102 -0
  23. swiftpy/ai/pricing.py +101 -0
  24. swiftpy/ai/prompts/__init__.py +6 -0
  25. swiftpy/ai/prompts/loader.py +343 -0
  26. swiftpy/ai/providers/__init__.py +22 -0
  27. swiftpy/ai/providers/anthropic_provider.py +353 -0
  28. swiftpy/ai/providers/base.py +273 -0
  29. swiftpy/ai/providers/openai_provider.py +275 -0
  30. swiftpy/ai/rag/__init__.py +53 -0
  31. swiftpy/ai/rag/context_formatter.py +263 -0
  32. swiftpy/ai/rag/pipeline_builder.py +544 -0
  33. swiftpy/ai/rag/reranker.py +484 -0
  34. swiftpy/ai/rag/result.py +78 -0
  35. swiftpy/ai/rag/retriever.py +315 -0
  36. swiftpy/ai/redis_store.py +227 -0
  37. swiftpy/ai/streaming.py +115 -0
  38. swiftpy/ai/tools/__init__.py +22 -0
  39. swiftpy/ai/tools/base_tool.py +489 -0
  40. swiftpy/ai/tools/first_party.py +384 -0
  41. swiftpy/ai/tools/tool_loop.py +417 -0
  42. swiftpy/ai/vectorstore/__init__.py +46 -0
  43. swiftpy/ai/vectorstore/base.py +542 -0
  44. swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
  45. swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
  46. swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
  47. swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
  48. swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
  49. swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
  50. swiftpy/auth/__init__.py +50 -0
  51. swiftpy/auth/api_key_guard.py +544 -0
  52. swiftpy/auth/auth_manager.py +259 -0
  53. swiftpy/auth/exceptions.py +71 -0
  54. swiftpy/auth/jwt_guard.py +230 -0
  55. swiftpy/auth/jwt_issuer.py +658 -0
  56. swiftpy/auth/token_blacklist.py +300 -0
  57. swiftpy/cache/__init__.py +14 -0
  58. swiftpy/cache/cache.py +251 -0
  59. swiftpy/cache/driver.py +216 -0
  60. swiftpy/cache/drivers/memory.py +199 -0
  61. swiftpy/cache/drivers/redis.py +421 -0
  62. swiftpy/console/__init__.py +0 -0
  63. swiftpy/console/cli.py +68 -0
  64. swiftpy/console/commands/__init__.py +0 -0
  65. swiftpy/console/commands/cache.py +164 -0
  66. swiftpy/console/commands/make.py +935 -0
  67. swiftpy/console/commands/migrate.py +280 -0
  68. swiftpy/console/commands/new.py +1806 -0
  69. swiftpy/console/commands/queue.py +306 -0
  70. swiftpy/console/commands/seed.py +252 -0
  71. swiftpy/console/commands/start.py +171 -0
  72. swiftpy/console/commands/storage.py +104 -0
  73. swiftpy/console/loader.py +21 -0
  74. swiftpy/console/stubs/__init__.py +26 -0
  75. swiftpy/console/stubs/controller.stub +14 -0
  76. swiftpy/console/stubs/event.stub +14 -0
  77. swiftpy/console/stubs/job.stub +22 -0
  78. swiftpy/console/stubs/listener.stub +14 -0
  79. swiftpy/console/stubs/mail.stub +20 -0
  80. swiftpy/console/stubs/middleware.stub +16 -0
  81. swiftpy/console/stubs/migration.alter.stub +17 -0
  82. swiftpy/console/stubs/migration.api_keys.stub +22 -0
  83. swiftpy/console/stubs/migration.blank.stub +11 -0
  84. swiftpy/console/stubs/migration.create.stub +15 -0
  85. swiftpy/console/stubs/migration.stub +11 -0
  86. swiftpy/console/stubs/migration.update.stub +17 -0
  87. swiftpy/console/stubs/model.stub +17 -0
  88. swiftpy/console/stubs/provider.stub +18 -0
  89. swiftpy/console/stubs/repository.stub +10 -0
  90. swiftpy/console/stubs/request.stub +16 -0
  91. swiftpy/console/stubs/resource.stub +21 -0
  92. swiftpy/console/stubs/schema.stub +12 -0
  93. swiftpy/console/stubs/seeder.stub +14 -0
  94. swiftpy/console/stubs/service.stub +10 -0
  95. swiftpy/core/__init__.py +0 -0
  96. swiftpy/core/application.py +1306 -0
  97. swiftpy/core/bootstrap.py +30 -0
  98. swiftpy/core/config.py +432 -0
  99. swiftpy/core/container.py +596 -0
  100. swiftpy/core/container_exceptions.py +18 -0
  101. swiftpy/core/context.py +148 -0
  102. swiftpy/core/exceptions.py +352 -0
  103. swiftpy/core/providers/base.py +35 -0
  104. swiftpy/database/__init__.py +41 -0
  105. swiftpy/database/exceptions.py +42 -0
  106. swiftpy/database/identifiers.py +105 -0
  107. swiftpy/database/migrations/__init__.py +36 -0
  108. swiftpy/database/migrations/blueprint.py +431 -0
  109. swiftpy/database/migrations/lock.py +104 -0
  110. swiftpy/database/migrations/migration.py +22 -0
  111. swiftpy/database/migrations/migrator.py +507 -0
  112. swiftpy/database/migrations/schema.py +435 -0
  113. swiftpy/database/orm/__init__.py +33 -0
  114. swiftpy/database/orm/fields.py +92 -0
  115. swiftpy/database/orm/model.py +2034 -0
  116. swiftpy/database/orm/registry.py +33 -0
  117. swiftpy/database/orm/relationships.py +836 -0
  118. swiftpy/database/pagination/__init__.py +16 -0
  119. swiftpy/database/pagination/contracts.py +60 -0
  120. swiftpy/database/pagination/paginator.py +112 -0
  121. swiftpy/database/pagination/simple_paginator.py +97 -0
  122. swiftpy/database/pool.py +218 -0
  123. swiftpy/database/query_builder.py +1803 -0
  124. swiftpy/database/seeder.py +39 -0
  125. swiftpy/database/transaction.py +276 -0
  126. swiftpy/events/__init__.py +10 -0
  127. swiftpy/events/dispatcher.py +308 -0
  128. swiftpy/events/redis_bus.py +552 -0
  129. swiftpy/filesystem/__init__.py +227 -0
  130. swiftpy/filesystem/disk.py +378 -0
  131. swiftpy/filesystem/drivers/__init__.py +11 -0
  132. swiftpy/filesystem/drivers/base.py +156 -0
  133. swiftpy/filesystem/drivers/local.py +559 -0
  134. swiftpy/filesystem/drivers/s3.py +655 -0
  135. swiftpy/filesystem/events.py +37 -0
  136. swiftpy/filesystem/exceptions.py +127 -0
  137. swiftpy/filesystem/manager.py +138 -0
  138. swiftpy/filesystem/metadata.py +22 -0
  139. swiftpy/filesystem/paths.py +99 -0
  140. swiftpy/filesystem/service_provider.py +17 -0
  141. swiftpy/filesystem/uploads/__init__.py +13 -0
  142. swiftpy/filesystem/uploads/handlers.py +362 -0
  143. swiftpy/filesystem/uploads/uploaded_file.py +222 -0
  144. swiftpy/filesystem/uploads/validators.py +186 -0
  145. swiftpy/filesystem/visibility.py +22 -0
  146. swiftpy/helpers/__init__.py +42 -0
  147. swiftpy/helpers/helper.py +180 -0
  148. swiftpy/http/exceptions.py +41 -0
  149. swiftpy/http/middleware.py +307 -0
  150. swiftpy/http/request.py +375 -0
  151. swiftpy/http/response.py +442 -0
  152. swiftpy/http/router.py +1050 -0
  153. swiftpy/mail/__init__.py +58 -0
  154. swiftpy/mail/attachments.py +255 -0
  155. swiftpy/mail/config.py +71 -0
  156. swiftpy/mail/envelope.py +183 -0
  157. swiftpy/mail/exceptions.py +90 -0
  158. swiftpy/mail/facade.py +312 -0
  159. swiftpy/mail/mailable.py +53 -0
  160. swiftpy/mail/manager.py +240 -0
  161. swiftpy/mail/message.py +186 -0
  162. swiftpy/mail/models.py +19 -0
  163. swiftpy/mail/queue/__init__.py +5 -0
  164. swiftpy/mail/queue/job.py +279 -0
  165. swiftpy/mail/service_provider.py +19 -0
  166. swiftpy/mail/templates/__init__.py +5 -0
  167. swiftpy/mail/templates/renderer.py +173 -0
  168. swiftpy/mail/transports/__init__.py +21 -0
  169. swiftpy/mail/transports/api/__init__.py +13 -0
  170. swiftpy/mail/transports/api/base.py +194 -0
  171. swiftpy/mail/transports/api/mailgun.py +158 -0
  172. swiftpy/mail/transports/api/resend.py +156 -0
  173. swiftpy/mail/transports/api/sendgrid.py +172 -0
  174. swiftpy/mail/transports/base.py +24 -0
  175. swiftpy/mail/transports/console.py +91 -0
  176. swiftpy/mail/transports/sendmail.py +128 -0
  177. swiftpy/mail/transports/smtp.py +466 -0
  178. swiftpy/queue/__init__.py +13 -0
  179. swiftpy/queue/dispatcher.py +813 -0
  180. swiftpy/queue/job.py +541 -0
  181. swiftpy/queue/worker.py +556 -0
  182. swiftpy/support/__init__.py +12 -0
  183. swiftpy/support/keys.py +108 -0
  184. swiftpy/support/redis_config.py +223 -0
  185. swiftpy/support/strings.py +114 -0
  186. swiftpy/testing/__init__.py +20 -0
  187. swiftpy/testing/fakes/__init__.py +11 -0
  188. swiftpy/testing/fakes/ai_fake.py +21 -0
  189. swiftpy/testing/fakes/vectorstore_fake.py +194 -0
  190. swiftpy/testing/fixtures.py +124 -0
  191. swiftpy/testing/http_client.py +730 -0
  192. swiftpy/testing/test_case.py +126 -0
  193. swiftpy/validation/__init__.py +97 -0
  194. swiftpy/validation/exceptions.py +26 -0
  195. swiftpy/validation/form_request.py +302 -0
  196. swiftpy/validation/protocols.py +33 -0
  197. swiftpy/validation/rules/__init__.py +89 -0
  198. swiftpy/validation/rules/builtin.py +391 -0
  199. swiftpy/validation/rules/exists.py +86 -0
  200. swiftpy/validation/rules/registry.py +290 -0
  201. swiftpy/validation/rules/required.py +55 -0
  202. swiftpy/validation/rules/unique.py +138 -0
  203. swiftpy/validation/validator.py +612 -0
  204. swiftpy/websockets/__init__.py +19 -0
  205. swiftpy/websockets/connection.py +317 -0
  206. swiftpy/websockets/manager.py +271 -0
  207. swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
  208. swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
  209. swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
  210. swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
  211. swiftpy_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -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