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,418 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import uuid
5
+ from collections.abc import Sequence
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from swiftpy.ai.exceptions import (
11
+ AIConnectionError,
12
+ AIModuleDisabledError,
13
+ )
14
+ from swiftpy.ai.pool import AIHttpPool
15
+ from swiftpy.ai.vectorstore.base import (
16
+ ALLOWED_METRICS,
17
+ RESERVED_META_KEYS,
18
+ SearchResult,
19
+ VectorRecord,
20
+ VectorStore,
21
+ VectorStoreCapabilities,
22
+ dedupe_preserve_order,
23
+ encode_path_segment,
24
+ handle_adapter_http_error,
25
+ normalize_record,
26
+ validate_collection_name,
27
+ validate_vector,
28
+ )
29
+ from swiftpy.core.config import Config
30
+
31
+ _HTTP_ERRORS: tuple[type[Exception], ...] = (httpx.TimeoutException, httpx.RequestError, OSError)
32
+
33
+ _METRIC_MAP = {
34
+ "cosine": "Cosine",
35
+ "l2": "Euclid",
36
+ "inner_product": "Dot",
37
+ }
38
+
39
+
40
+ def _to_qdrant_id(orig_id: str) -> str:
41
+ """Ensure ID conforms to Qdrant UUID or integer requirement, deterministic for strings."""
42
+ try:
43
+ uuid.UUID(orig_id)
44
+ return orig_id
45
+ except (ValueError, AttributeError):
46
+ return str(uuid.uuid5(uuid.NAMESPACE_DNS, orig_id))
47
+
48
+
49
+ def _flatten_qdrant_must(flt: dict[str, Any], prefix: str = "") -> list[dict[str, Any]]:
50
+ """Flatten nested metadata filter dicts into Qdrant dot-path must conditions."""
51
+ if not isinstance(flt, dict):
52
+ raise TypeError(f"Filter must be a dict, got {type(flt).__name__}.")
53
+ clauses: list[dict[str, Any]] = []
54
+ for k, v in flt.items():
55
+ if not isinstance(k, str) or not k or "\x00" in k:
56
+ raise ValueError(f"Invalid filter key {k!r}.")
57
+ if k.startswith("$"):
58
+ raise ValueError(f"Qdrant metadata filters do not support Mongo-style operator {k!r}.")
59
+ full_key = f"{prefix}.{k}" if prefix else k
60
+ if isinstance(v, dict):
61
+ if any(isinstance(sub_k, str) and sub_k.startswith("$") for sub_k in v):
62
+ raise ValueError(f"Qdrant metadata filters do not support operator dicts on {full_key!r}.")
63
+ if not v:
64
+ raise ValueError(f"Empty nested filter dict on key {full_key!r} is not supported.")
65
+ clauses.extend(_flatten_qdrant_must(v, prefix=full_key))
66
+ elif v is None:
67
+ clauses.append({"is_null": {"key": full_key}})
68
+ else:
69
+ clauses.append({"key": full_key, "match": {"value": v}})
70
+ return clauses
71
+
72
+
73
+ class QdrantAdapter(VectorStore):
74
+ """
75
+ Qdrant vector store adapter via REST API through the shared async HTTP pool.
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ collection_name: str = "default",
81
+ dimensions: int | None = None,
82
+ metric: str = "cosine",
83
+ base_url: str | None = None,
84
+ api_key: str | None = None,
85
+ timeout: float = 30.0,
86
+ config: Config | None = None,
87
+ http_pool: AIHttpPool | None = None,
88
+ **kwargs: Any,
89
+ ) -> None:
90
+ validate_collection_name(collection_name)
91
+ self._config = config or Config()
92
+ if not self._config.get("ai.enabled", True):
93
+ raise AIModuleDisabledError("AI module is disabled in configuration (ai.enabled is false).")
94
+
95
+ norm_metric = metric.lower().strip()
96
+ if norm_metric not in ALLOWED_METRICS:
97
+ raise ValueError(f"Unsupported metric '{metric}'. Expected one of {sorted(ALLOWED_METRICS)}")
98
+
99
+ self._collection = collection_name
100
+ self._dimensions = dimensions
101
+ self._metric = norm_metric
102
+
103
+ cfg_base = self._config.get("ai.vectorstores.qdrant.base_url", "http://localhost:6333")
104
+ self._base_url = (base_url or str(cfg_base)).rstrip("/")
105
+ cfg_key = self._config.get("ai.vectorstores.qdrant.api_key", "")
106
+ self._api_key = api_key if api_key is not None else str(cfg_key)
107
+ self._timeout = timeout
108
+
109
+ self._http_pool = http_pool or AIHttpPool.get_instance()
110
+
111
+ @property
112
+ def capabilities(self) -> VectorStoreCapabilities:
113
+ """Return the declared capabilities supported by Qdrant."""
114
+ return VectorStoreCapabilities(
115
+ metadata_filtering=True,
116
+ nested_metadata_filtering=True,
117
+ namespaces=False,
118
+ pagination=False,
119
+ batch_upsert=True,
120
+ native_distance_metrics=("cosine", "l2", "inner_product"),
121
+ )
122
+
123
+ def _get_headers(self) -> dict[str, str]:
124
+ headers = {"Content-Type": "application/json"}
125
+ if self._api_key:
126
+ headers["api-key"] = self._api_key
127
+ return headers
128
+
129
+ def _collection_url(self, name: str | None = None, suffix: str = "") -> str:
130
+ coll = self._collection if name is None else validate_collection_name(name)
131
+ return f"{self._base_url}/collections/{encode_path_segment(coll)}{suffix}"
132
+
133
+ async def create_collection(
134
+ self,
135
+ name: str,
136
+ dimensions: int,
137
+ metric: str = "cosine",
138
+ ) -> None:
139
+ validate_collection_name(name)
140
+ norm_metric = metric.lower().strip()
141
+ if norm_metric not in ALLOWED_METRICS:
142
+ raise ValueError(f"Unsupported metric '{metric}'. Expected one of {sorted(ALLOWED_METRICS)}")
143
+ if isinstance(dimensions, bool) or not isinstance(dimensions, int) or dimensions <= 0:
144
+ raise ValueError(f"Dimensions must be positive, got {dimensions}")
145
+
146
+ qdrant_metric = _METRIC_MAP[norm_metric]
147
+ url = self._collection_url(name)
148
+ payload = {
149
+ "vectors": {
150
+ "size": dimensions,
151
+ "distance": qdrant_metric,
152
+ }
153
+ }
154
+
155
+ client = self._http_pool.get_client()
156
+ try:
157
+ resp = await client.put(
158
+ url,
159
+ json=payload,
160
+ headers=self._get_headers(),
161
+ timeout=self._timeout,
162
+ )
163
+ if resp.status_code not in (200, 409):
164
+ handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
165
+ except _HTTP_ERRORS as err:
166
+ raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
167
+
168
+ self._collection = name
169
+ self._dimensions = dimensions
170
+ self._metric = norm_metric
171
+
172
+ async def upsert(
173
+ self,
174
+ id: str,
175
+ vector: list[float],
176
+ metadata: dict[str, Any] | None = None,
177
+ content: str = "",
178
+ ) -> None:
179
+ await self.upsert_batch([
180
+ VectorRecord(id=id, vector=vector, metadata=metadata or {}, content=content)
181
+ ])
182
+
183
+ async def upsert_batch(
184
+ self,
185
+ records: Sequence[
186
+ VectorRecord
187
+ | dict[str, Any]
188
+ | tuple[str, list[float], dict[str, Any] | None]
189
+ ],
190
+ ) -> None:
191
+ if not records:
192
+ return
193
+
194
+ norm_records = [
195
+ normalize_record(r, self._dimensions, self._metric) for r in records
196
+ ]
197
+
198
+ points: list[dict[str, Any]] = []
199
+ for r in norm_records:
200
+ qid = _to_qdrant_id(r.id)
201
+ user_meta = {
202
+ k: copy.deepcopy(v)
203
+ for k, v in r.metadata.items()
204
+ if k not in RESERVED_META_KEYS
205
+ }
206
+ payload = {
207
+ **user_meta,
208
+ "_orig_id": r.id,
209
+ "_content": r.content,
210
+ }
211
+ points.append({
212
+ "id": qid,
213
+ "vector": r.vector,
214
+ "payload": payload,
215
+ })
216
+
217
+ url = self._collection_url(suffix="/points?wait=true")
218
+ client = self._http_pool.get_client()
219
+ try:
220
+ resp = await client.put(
221
+ url,
222
+ json={"points": points},
223
+ headers=self._get_headers(),
224
+ timeout=self._timeout,
225
+ )
226
+ if resp.status_code != 200:
227
+ handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
228
+ except _HTTP_ERRORS as err:
229
+ raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
230
+
231
+ async def search(
232
+ self,
233
+ vector: list[float],
234
+ limit: int = 10,
235
+ filter: dict[str, Any] | None = None,
236
+ ) -> list[SearchResult]:
237
+ if limit <= 0:
238
+ raise ValueError(f"Limit must be a positive integer, got {limit}")
239
+
240
+ parsed_vec = validate_vector(vector, self._dimensions, self._metric)
241
+ url = self._collection_url(suffix="/points/search")
242
+
243
+ body: dict[str, Any] = {
244
+ "vector": parsed_vec,
245
+ "limit": limit,
246
+ "with_payload": True,
247
+ "with_vector": True,
248
+ }
249
+
250
+ if filter is not None:
251
+ must_clauses = _flatten_qdrant_must(filter)
252
+ if must_clauses:
253
+ body["filter"] = {"must": must_clauses}
254
+
255
+ client = self._http_pool.get_client()
256
+ try:
257
+ resp = await client.post(
258
+ url,
259
+ json=body,
260
+ headers=self._get_headers(),
261
+ timeout=self._timeout,
262
+ )
263
+ if resp.status_code != 200:
264
+ handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
265
+ data = resp.json().get("result", [])
266
+ except _HTTP_ERRORS as err:
267
+ raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
268
+
269
+ results: list[SearchResult] = []
270
+ for item in data:
271
+ payload = item.get("payload") or {}
272
+ orig_id = str(payload.get("_orig_id", item.get("id", "")))
273
+ content = str(payload.get("_content", ""))
274
+ meta = {k: v for k, v in payload.items() if k not in RESERVED_META_KEYS}
275
+ raw_score = float(item.get("score", 0.0))
276
+ score = -abs(raw_score) if self._metric == "l2" else raw_score
277
+ vec = item.get("vector")
278
+ results.append(
279
+ SearchResult(
280
+ id=orig_id,
281
+ score=score,
282
+ metadata=meta,
283
+ content=content,
284
+ vector=vec,
285
+ )
286
+ )
287
+ return results
288
+
289
+ async def get(self, id: str) -> SearchResult | None:
290
+ if not id or not isinstance(id, str):
291
+ return None
292
+
293
+ qid = _to_qdrant_id(id)
294
+ url = self._collection_url(suffix="/points")
295
+ client = self._http_pool.get_client()
296
+ try:
297
+ resp = await client.post(
298
+ url,
299
+ json={"ids": [qid], "with_payload": True, "with_vector": True},
300
+ headers=self._get_headers(),
301
+ timeout=self._timeout,
302
+ )
303
+ if resp.status_code == 404:
304
+ return None
305
+ if resp.status_code != 200:
306
+ handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
307
+ items = resp.json().get("result", [])
308
+ if not items:
309
+ return None
310
+ item = items[0]
311
+ payload = item.get("payload") or {}
312
+ orig_id = str(payload.get("_orig_id", item.get("id", "")))
313
+ content = str(payload.get("_content", ""))
314
+ meta = {k: v for k, v in payload.items() if k not in RESERVED_META_KEYS}
315
+ vec = item.get("vector")
316
+ return SearchResult(
317
+ id=orig_id,
318
+ score=1.0,
319
+ metadata=meta,
320
+ content=content,
321
+ vector=vec,
322
+ )
323
+ except _HTTP_ERRORS as err:
324
+ raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
325
+
326
+ async def delete(self, id: str) -> bool:
327
+ existing = await self.get(id)
328
+ if existing is None:
329
+ return False
330
+
331
+ qid = _to_qdrant_id(id)
332
+ url = self._collection_url(suffix="/points/delete?wait=true")
333
+ client = self._http_pool.get_client()
334
+ try:
335
+ resp = await client.post(
336
+ url,
337
+ json={"points": [qid]},
338
+ headers=self._get_headers(),
339
+ timeout=self._timeout,
340
+ )
341
+ if resp.status_code != 200:
342
+ handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
343
+ return True
344
+ except _HTTP_ERRORS as err:
345
+ raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
346
+
347
+ async def delete_batch(self, ids: Sequence[str]) -> int:
348
+ unique_ids = dedupe_preserve_order(ids)
349
+ if not unique_ids:
350
+ return 0
351
+
352
+ candidate_qids = [_to_qdrant_id(item_id) for item_id in unique_ids]
353
+ lookup_url = self._collection_url(suffix="/points")
354
+ client = self._http_pool.get_client()
355
+ try:
356
+ lookup_resp = await client.post(
357
+ lookup_url,
358
+ json={"ids": candidate_qids, "with_payload": False, "with_vector": False},
359
+ headers=self._get_headers(),
360
+ timeout=self._timeout,
361
+ )
362
+ if lookup_resp.status_code == 404:
363
+ return 0
364
+ if lookup_resp.status_code != 200:
365
+ handle_adapter_http_error(lookup_resp.status_code, lookup_resp.text, "qdrant")
366
+ found_items = lookup_resp.json().get("result", [])
367
+ except _HTTP_ERRORS as err:
368
+ raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
369
+
370
+ found_qids = dedupe_preserve_order(
371
+ [str(item["id"]) for item in found_items if isinstance(item, dict) and item.get("id") is not None]
372
+ )
373
+ if not found_qids:
374
+ return 0
375
+
376
+ url = self._collection_url(suffix="/points/delete?wait=true")
377
+ try:
378
+ resp = await client.post(
379
+ url,
380
+ json={"points": found_qids},
381
+ headers=self._get_headers(),
382
+ timeout=self._timeout,
383
+ )
384
+ if resp.status_code != 200:
385
+ handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
386
+ return len(found_qids)
387
+ except _HTTP_ERRORS as err:
388
+ raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
389
+
390
+ async def count(self) -> int:
391
+ url = self._collection_url(suffix="/points/count")
392
+ client = self._http_pool.get_client()
393
+ try:
394
+ resp = await client.post(
395
+ url,
396
+ json={"exact": True},
397
+ headers=self._get_headers(),
398
+ timeout=self._timeout,
399
+ )
400
+ if resp.status_code != 200:
401
+ handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
402
+ return int(resp.json().get("result", {}).get("count", 0))
403
+ except _HTTP_ERRORS as err:
404
+ raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
405
+
406
+ async def drop_collection(self, name: str) -> None:
407
+ url = self._collection_url(name)
408
+ client = self._http_pool.get_client()
409
+ try:
410
+ resp = await client.delete(
411
+ url,
412
+ headers=self._get_headers(),
413
+ timeout=self._timeout,
414
+ )
415
+ if resp.status_code not in (200, 404):
416
+ handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
417
+ except _HTTP_ERRORS as err:
418
+ raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from swiftpy.auth.api_key_guard import (
4
+ ApiKeyGuard,
5
+ generate_api_key,
6
+ get_server_pepper,
7
+ hash_api_key,
8
+ require_api_key,
9
+ verify_api_key_hash,
10
+ )
11
+ from swiftpy.auth.auth_manager import AuthManager, CurrentUser, current_user
12
+ from swiftpy.auth.exceptions import (
13
+ AuthenticationError,
14
+ InvalidApiKeyError,
15
+ TokenRevokedError,
16
+ )
17
+ from swiftpy.auth.jwt_guard import JWTGuard, require_jwt
18
+ from swiftpy.auth.jwt_issuer import (
19
+ InvalidAlgorithmError,
20
+ InvalidSignatureError,
21
+ InvalidTokenError,
22
+ JWTError,
23
+ JWTIssuer,
24
+ TokenExpiredError,
25
+ )
26
+ from swiftpy.auth.token_blacklist import TokenBlacklist
27
+
28
+ __all__ = [
29
+ "ApiKeyGuard",
30
+ "AuthManager",
31
+ "AuthenticationError",
32
+ "CurrentUser",
33
+ "InvalidAlgorithmError",
34
+ "InvalidApiKeyError",
35
+ "InvalidSignatureError",
36
+ "InvalidTokenError",
37
+ "JWTError",
38
+ "JWTGuard",
39
+ "JWTIssuer",
40
+ "TokenBlacklist",
41
+ "TokenExpiredError",
42
+ "TokenRevokedError",
43
+ "current_user",
44
+ "generate_api_key",
45
+ "get_server_pepper",
46
+ "hash_api_key",
47
+ "require_api_key",
48
+ "require_jwt",
49
+ "verify_api_key_hash",
50
+ ]