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,223 @@
1
+ """
2
+ Centralized Redis connection configuration resolver.
3
+
4
+ Guarantees single-source-of-truth configuration resolution through Config,
5
+ with proper URL-encoding of user credentials and consistent defaults.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+ from urllib.parse import quote
12
+
13
+ from swiftpy.core.config import Config
14
+
15
+
16
+ def _first_not_none(*values: Any) -> Any:
17
+ """Return the first value that is not None."""
18
+ for val in values:
19
+ if val is not None:
20
+ return val
21
+ return None
22
+
23
+
24
+ def _resolve_section_dict(cfg: Config, section: str) -> dict[str, Any]:
25
+ """Retrieve dictionary for the given section or known subsystem fallback."""
26
+ # Subsystem-specific structures
27
+ if section in ("cache", "cache.redis", "cache.stores.redis"):
28
+ stores = cfg.get("cache.stores")
29
+ if isinstance(stores, dict):
30
+ redis_store = stores.get("redis")
31
+ if isinstance(redis_store, dict):
32
+ return {str(k): v for k, v in redis_store.items()}
33
+ cache_redis = cfg.get("cache.redis")
34
+ if isinstance(cache_redis, dict):
35
+ return {str(k): v for k, v in cache_redis.items()}
36
+
37
+ if section in ("queue", "queue.redis", "queue.connections.redis"):
38
+ connections = cfg.get("queue.connections")
39
+ if isinstance(connections, dict):
40
+ redis_conn = connections.get("redis")
41
+ if isinstance(redis_conn, dict):
42
+ return {str(k): v for k, v in redis_conn.items()}
43
+ queue_redis = cfg.get("queue.redis")
44
+ if isinstance(queue_redis, dict):
45
+ return {str(k): v for k, v in queue_redis.items()}
46
+
47
+ if section in ("events", "events.redis"):
48
+ events_redis = cfg.get("events.redis")
49
+ if isinstance(events_redis, dict):
50
+ return {str(k): v for k, v in events_redis.items()}
51
+
52
+ val = cfg.get(section)
53
+ if isinstance(val, dict):
54
+ val_redis = val.get("redis")
55
+ if isinstance(val_redis, dict):
56
+ return {str(k): v for k, v in val_redis.items()}
57
+ return {str(k): v for k, v in val.items()}
58
+
59
+ return {}
60
+
61
+
62
+ def resolve_redis_params(
63
+ config: Config | None = None,
64
+ section: str = "redis",
65
+ *,
66
+ host: str | None = None,
67
+ port: int | None = None,
68
+ password: str | None = None,
69
+ database: int | None = None,
70
+ username: str | None = None,
71
+ ssl: bool | None = None,
72
+ ) -> dict[str, Any]:
73
+ """
74
+ Resolve Redis connection parameters from Config and keyword overrides.
75
+
76
+ Reads only through Config (never raw os.getenv in runtime callers),
77
+ ensuring deterministic precedence and environment variable integration.
78
+ """
79
+ if config is None:
80
+ try:
81
+ cfg = Config.get_instance()
82
+ except (RuntimeError, ValueError):
83
+ cfg = Config()
84
+ else:
85
+ cfg = config
86
+
87
+ # Check for direct URL in section or sub-path
88
+ section_dict = _resolve_section_dict(cfg, section)
89
+ explicit_url = _first_not_none(
90
+ section_dict.get("url"),
91
+ cfg.get(f"{section}.url"),
92
+ cfg.get(f"{section}.redis_url"),
93
+ cfg.get("redis.url"),
94
+ cfg.get("redis_url"),
95
+ )
96
+
97
+ # Resolve individual components
98
+ cfg_host = _first_not_none(
99
+ section_dict.get("host"),
100
+ cfg.get(f"{section}.host"),
101
+ cfg.get("redis.host"),
102
+ cfg.get("redis_host"),
103
+ )
104
+ cfg_port = _first_not_none(
105
+ section_dict.get("port"),
106
+ cfg.get(f"{section}.port"),
107
+ cfg.get("redis.port"),
108
+ cfg.get("redis_port"),
109
+ )
110
+ cfg_password = (
111
+ section_dict.get("password")
112
+ if "password" in section_dict
113
+ else _first_not_none(
114
+ cfg.get(f"{section}.password"),
115
+ cfg.get("redis.password"),
116
+ cfg.get("redis_password"),
117
+ )
118
+ )
119
+
120
+ # Per-subsystem database fallback
121
+ subsystem_db = None
122
+ if section in ("queue", "queue.redis", "queue.connections.redis"):
123
+ subsystem_db = _first_not_none(cfg.get("redis_queue_db"), cfg.get("redis.queue_db"))
124
+ elif section in ("cache", "cache.redis", "cache.stores.redis"):
125
+ subsystem_db = _first_not_none(cfg.get("redis_cache_db"), cfg.get("redis.cache_db"))
126
+ elif section in ("events", "events.redis"):
127
+ subsystem_db = _first_not_none(cfg.get("redis_events_db"), cfg.get("redis.events_db"))
128
+
129
+ cfg_database = (
130
+ section_dict.get("database")
131
+ if "database" in section_dict
132
+ else _first_not_none(
133
+ subsystem_db,
134
+ cfg.get(f"{section}.database"),
135
+ cfg.get("redis.database"),
136
+ cfg.get("redis_db"),
137
+ )
138
+ )
139
+
140
+ cfg_username = _first_not_none(
141
+ section_dict.get("username"),
142
+ cfg.get(f"{section}.username"),
143
+ cfg.get("redis.username"),
144
+ )
145
+ cfg_ssl = (
146
+ section_dict.get("ssl")
147
+ if "ssl" in section_dict
148
+ else _first_not_none(cfg.get(f"{section}.ssl"), cfg.get("redis.ssl"))
149
+ )
150
+
151
+ # Apply overrides over config
152
+ final_host = host if host is not None else (str(cfg_host) if cfg_host is not None else "127.0.0.1")
153
+ final_port = port if port is not None else (int(cfg_port) if cfg_port is not None else 6379)
154
+ final_password = password if password is not None else (str(cfg_password) if cfg_password is not None else None)
155
+ final_database = database if database is not None else (int(cfg_database) if cfg_database is not None else 0)
156
+ final_username = username if username is not None else (str(cfg_username) if cfg_username is not None else None)
157
+ final_ssl = ssl if ssl is not None else (bool(cfg_ssl) if cfg_ssl is not None else False)
158
+
159
+ # If an explicit URL was configured and no individual component override was provided, use it
160
+ has_overrides = any(
161
+ arg is not None
162
+ for arg in (host, port, password, database, username, ssl)
163
+ )
164
+ if explicit_url and not has_overrides:
165
+ return {
166
+ "host": final_host,
167
+ "port": final_port,
168
+ "password": final_password,
169
+ "database": final_database,
170
+ "username": final_username,
171
+ "ssl": final_ssl,
172
+ "url": str(explicit_url),
173
+ }
174
+
175
+ # Construct the percent-encoded URL
176
+ scheme = "rediss" if final_ssl else "redis"
177
+ userinfo = ""
178
+ if final_username is not None and final_password is not None:
179
+ userinfo = f"{quote(final_username, safe='')}:{quote(final_password, safe='')}@"
180
+ elif final_password is not None:
181
+ userinfo = f":{quote(final_password, safe='')}@"
182
+ elif final_username is not None:
183
+ userinfo = f"{quote(final_username, safe='')}@"
184
+
185
+ url = f"{scheme}://{userinfo}{final_host}:{final_port}/{final_database}"
186
+
187
+ return {
188
+ "host": final_host,
189
+ "port": final_port,
190
+ "password": final_password,
191
+ "database": final_database,
192
+ "username": final_username,
193
+ "ssl": final_ssl,
194
+ "url": url,
195
+ }
196
+
197
+
198
+ def build_redis_url(
199
+ config: Config | None = None,
200
+ section: str = "redis",
201
+ *,
202
+ host: str | None = None,
203
+ port: int | None = None,
204
+ password: str | None = None,
205
+ database: int | None = None,
206
+ username: str | None = None,
207
+ ssl: bool | None = None,
208
+ ) -> str:
209
+ """Build a sanitized and credential-encoded Redis connection URL."""
210
+ params = resolve_redis_params(
211
+ config=config,
212
+ section=section,
213
+ host=host,
214
+ port=port,
215
+ password=password,
216
+ database=database,
217
+ username=username,
218
+ ssl=ssl,
219
+ )
220
+ return str(params["url"])
221
+
222
+
223
+ __all__ = ["build_redis_url", "resolve_redis_params"]
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ _UNCOUNTABLES = {
6
+ "aircraft",
7
+ "deer",
8
+ "equipment",
9
+ "fish",
10
+ "information",
11
+ "money",
12
+ "police",
13
+ "rice",
14
+ "series",
15
+ "sheep",
16
+ "species",
17
+ }
18
+
19
+ _IRREGULARS = {
20
+ "child": "children",
21
+ "foot": "feet",
22
+ "goose": "geese",
23
+ "man": "men",
24
+ "mouse": "mice",
25
+ "ox": "oxen",
26
+ "person": "people",
27
+ "quiz": "quizzes",
28
+ "tooth": "teeth",
29
+ "woman": "women",
30
+ }
31
+
32
+ _PLURAL_RULES: list[tuple[re.Pattern[str], str]] = [
33
+ (re.compile(r"(quiz)$", re.IGNORECASE), r"\1zes"),
34
+ (re.compile(r"^(ox)$", re.IGNORECASE), r"\1en"),
35
+ (re.compile(r"([ml])ouse$", re.IGNORECASE), r"\1ice"),
36
+ (re.compile(r"(matr|vert|ind)(?:ix|ex)$", re.IGNORECASE), r"\1ices"),
37
+ (re.compile(r"(ax|test)is$", re.IGNORECASE), r"\1es"),
38
+ (re.compile(r"sis$", re.IGNORECASE), "ses"),
39
+ (re.compile(r"(x|ch|ss|sh)$", re.IGNORECASE), r"\1es"),
40
+ (re.compile(r"([^aeiouy]|qu)y$", re.IGNORECASE), r"\1ies"),
41
+ (re.compile(r"(?:([^f])fe|([lr])f)$", re.IGNORECASE), r"\1\2ves"),
42
+ (re.compile(r"([ti])um$", re.IGNORECASE), r"\1a"),
43
+ (re.compile(r"(buffal|tomat)o$", re.IGNORECASE), r"\1oes"),
44
+ (re.compile(r"(bu)s$", re.IGNORECASE), r"\1ses"),
45
+ (re.compile(r"(alias|status)$", re.IGNORECASE), r"\1es"),
46
+ (re.compile(r"(octop|vir)us$", re.IGNORECASE), r"\1uses"),
47
+ (re.compile(r"s$", re.IGNORECASE), "s"),
48
+ (re.compile(r"$"), "s"),
49
+ ]
50
+
51
+
52
+ def snake_case(name: str) -> str:
53
+ """
54
+ Convert PascalCase, camelCase, or mixed acronyms to snake_case.
55
+
56
+ Examples:
57
+ snake_case("UserProfile") -> "user_profile"
58
+ snake_case("APIKey") -> "api_key"
59
+ snake_case("HTTPServer") -> "http_server"
60
+ snake_case("HTTPRequest") -> "http_request"
61
+ snake_case("UserAPIKey") -> "user_api_key"
62
+ snake_case("Oauth2Client") -> "oauth2_client"
63
+ """
64
+ if not name:
65
+ return name
66
+
67
+ # 1. Separate consecutive uppercase runs followed by titlecase/lowercase
68
+ # e.g., 'APIKey' -> 'API_Key', 'HTTPRequest' -> 'HTTP_Request'
69
+ s1 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
70
+
71
+ # 2. Separate lowercase/digit followed by uppercase
72
+ # e.g., 'UserAPI_Key' -> 'User_API_Key', 'oauth2Client' -> 'oauth2_Client'
73
+ s2 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1)
74
+
75
+ return s2.lower()
76
+
77
+
78
+ def pluralize(word: str) -> str:
79
+ """Pluralize an English word or snake_cased identifier, preserving capitalization."""
80
+ if not word:
81
+ return word
82
+
83
+ parts = word.split("_")
84
+ last = parts[-1]
85
+ last_lower = last.lower()
86
+
87
+ if last_lower in _UNCOUNTABLES:
88
+ plural_last = last
89
+ elif last_lower in _IRREGULARS:
90
+ replacement = _IRREGULARS[last_lower]
91
+ if last.isupper():
92
+ plural_last = replacement.upper()
93
+ elif last.istitle():
94
+ plural_last = replacement.title()
95
+ else:
96
+ plural_last = replacement
97
+ else:
98
+ for pattern, replacement in _PLURAL_RULES:
99
+ if pattern.search(last):
100
+ plural_last = pattern.sub(replacement, last)
101
+ break
102
+ else:
103
+ plural_last = last + "s"
104
+
105
+ if last.isupper():
106
+ plural_last = plural_last.upper()
107
+ elif last.istitle():
108
+ plural_last = plural_last[0].upper() + plural_last[1:]
109
+
110
+ parts[-1] = plural_last
111
+ return "_".join(parts)
112
+
113
+
114
+ __all__ = ["pluralize", "snake_case"]
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from swiftpy.testing.fakes.ai_fake import AIFake, FakeCall
4
+ from swiftpy.testing.fakes.vectorstore_fake import VectorStoreCall, VectorStoreFake
5
+ from swiftpy.testing.fixtures import db_transaction, transactional_test
6
+ from swiftpy.testing.http_client import TestClient, TestResponse, WebSocketTestSession
7
+ from swiftpy.testing.test_case import TestCase
8
+
9
+ __all__ = [
10
+ "AIFake",
11
+ "FakeCall",
12
+ "TestCase",
13
+ "TestClient",
14
+ "TestResponse",
15
+ "VectorStoreCall",
16
+ "VectorStoreFake",
17
+ "WebSocketTestSession",
18
+ "db_transaction",
19
+ "transactional_test",
20
+ ]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from swiftpy.testing.fakes.ai_fake import AIFake, FakeCall
4
+ from swiftpy.testing.fakes.vectorstore_fake import VectorStoreCall, VectorStoreFake
5
+
6
+ __all__ = [
7
+ "AIFake",
8
+ "FakeCall",
9
+ "VectorStoreCall",
10
+ "VectorStoreFake",
11
+ ]
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from swiftpy.ai._deterministic import (
4
+ generate_deterministic_embedding as _deterministic_fake_vector,
5
+ )
6
+ from swiftpy.ai._fake import (
7
+ AIFake,
8
+ FakeCall,
9
+ FakeEmbeddingCall,
10
+ FixtureType,
11
+ _task_fake_stack,
12
+ )
13
+
14
+ __all__ = [
15
+ "AIFake",
16
+ "FakeCall",
17
+ "FakeEmbeddingCall",
18
+ "FixtureType",
19
+ "_deterministic_fake_vector",
20
+ "_task_fake_stack",
21
+ ]
@@ -0,0 +1,194 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+ from swiftpy.ai.vectorstore.base import SearchResult, VectorRecord
8
+ from swiftpy.ai.vectorstore.in_memory_adapter import InMemoryVectorStore
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class VectorStoreCall:
13
+ """Recorded invocation made against a VectorStoreFake."""
14
+
15
+ method: str
16
+ collection: str
17
+ args: dict[str, Any] = field(default_factory=dict)
18
+
19
+
20
+ class VectorStoreFake(InMemoryVectorStore):
21
+ """
22
+ In-memory vector store test fake that records operations for assertions.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ collection_name: str = "default",
28
+ dimensions: int | None = None,
29
+ metric: str = "cosine",
30
+ *,
31
+ max_records: int | None = 100_000,
32
+ ) -> None:
33
+ super().__init__(
34
+ collection_name=collection_name,
35
+ dimensions=dimensions,
36
+ metric=metric,
37
+ max_records=max_records,
38
+ )
39
+ self.calls: list[VectorStoreCall] = []
40
+
41
+ def reset(self) -> None:
42
+ """Clear all stored vectors and recorded calls."""
43
+ self.calls.clear()
44
+ self._collections.clear()
45
+
46
+ def recorded(self, method: str | None = None) -> list[VectorStoreCall]:
47
+ """Return recorded invocations, optionally filtered by method name."""
48
+ if method is None:
49
+ return list(self.calls)
50
+ return [call for call in self.calls if call.method == method]
51
+
52
+ def assert_called(self, method: str, *, times: int | None = None) -> None:
53
+ """Assert that `method` was invoked (and optionally how many times)."""
54
+ matched = self.recorded(method)
55
+ if not matched and (times is None or times > 0):
56
+ raise AssertionError(f"Expected VectorStore method '{method}' to be called.")
57
+ if times is not None and len(matched) != times:
58
+ raise AssertionError(
59
+ f"Expected VectorStore method '{method}' to be called {times} time(s), got {len(matched)}."
60
+ )
61
+
62
+ def assert_upserted(self, record_id: str | None = None) -> None:
63
+ """Assert that an upsert/upsert_batch occurred (optionally for a specific id)."""
64
+ upserts = [
65
+ call for call in self.calls if call.method in ("upsert", "upsert_batch")
66
+ ]
67
+ if not upserts:
68
+ raise AssertionError("Expected at least one vectorstore upsert call.")
69
+ if record_id is not None:
70
+ found = any(
71
+ call.args.get("id") == record_id
72
+ or record_id in call.args.get("ids", ())
73
+ for call in upserts
74
+ )
75
+ if not found:
76
+ raise AssertionError(
77
+ f"Expected record id '{record_id}' to be upserted, got {upserts!r}."
78
+ )
79
+
80
+ def assert_searched(self, *, times: int | None = None) -> None:
81
+ """Assert that search() was invoked."""
82
+ self.assert_called("search", times=times)
83
+
84
+ async def create_collection(
85
+ self,
86
+ name: str,
87
+ dimensions: int,
88
+ metric: str = "cosine",
89
+ ) -> None:
90
+ await super().create_collection(name, dimensions, metric=metric)
91
+ self.calls.append(
92
+ VectorStoreCall(
93
+ method="create_collection",
94
+ collection=name,
95
+ args={"name": name, "dimensions": dimensions, "metric": metric},
96
+ )
97
+ )
98
+
99
+ async def upsert(
100
+ self,
101
+ id: str,
102
+ vector: list[float],
103
+ metadata: dict[str, Any] | None = None,
104
+ content: str = "",
105
+ ) -> None:
106
+ await super().upsert(id, vector, metadata=metadata, content=content)
107
+ self.calls.append(
108
+ VectorStoreCall(
109
+ method="upsert",
110
+ collection=self._current_collection,
111
+ args={"id": id, "dimensions": len(vector), "metadata": metadata},
112
+ )
113
+ )
114
+
115
+ async def upsert_batch(
116
+ self,
117
+ records: Sequence[
118
+ VectorRecord
119
+ | dict[str, Any]
120
+ | tuple[str, list[float], dict[str, Any] | None]
121
+ | tuple[str, list[float], dict[str, Any] | None, str]
122
+ ],
123
+ ) -> None:
124
+ ids: list[str] = []
125
+ for rec in records:
126
+ if isinstance(rec, VectorRecord):
127
+ ids.append(rec.id)
128
+ elif isinstance(rec, dict):
129
+ ids.append(str(rec.get("id", "")))
130
+ elif isinstance(rec, tuple) and rec:
131
+ ids.append(str(rec[0]))
132
+ await super().upsert_batch(records)
133
+ self.calls.append(
134
+ VectorStoreCall(
135
+ method="upsert_batch",
136
+ collection=self._current_collection,
137
+ args={"count": len(records), "ids": tuple(ids)},
138
+ )
139
+ )
140
+
141
+ async def search(
142
+ self,
143
+ vector: list[float],
144
+ limit: int = 10,
145
+ filter: dict[str, Any] | None = None,
146
+ ) -> list[SearchResult]:
147
+ results = await super().search(vector, limit=limit, filter=filter)
148
+ self.calls.append(
149
+ VectorStoreCall(
150
+ method="search",
151
+ collection=self._current_collection,
152
+ args={
153
+ "limit": limit,
154
+ "filter": filter,
155
+ "result_count": len(results),
156
+ },
157
+ )
158
+ )
159
+ return results
160
+
161
+ async def delete(self, id: str) -> bool:
162
+ deleted = await super().delete(id)
163
+ self.calls.append(
164
+ VectorStoreCall(
165
+ method="delete",
166
+ collection=self._current_collection,
167
+ args={"id": id, "deleted": deleted},
168
+ )
169
+ )
170
+ return deleted
171
+
172
+ async def delete_batch(self, ids: Sequence[str]) -> int:
173
+ count = await super().delete_batch(ids)
174
+ self.calls.append(
175
+ VectorStoreCall(
176
+ method="delete_batch",
177
+ collection=self._current_collection,
178
+ args={"ids": tuple(ids), "deleted_count": count},
179
+ )
180
+ )
181
+ return count
182
+
183
+ async def drop_collection(self, name: str) -> None:
184
+ await super().drop_collection(name)
185
+ self.calls.append(
186
+ VectorStoreCall(
187
+ method="drop_collection",
188
+ collection=name,
189
+ args={"name": name},
190
+ )
191
+ )
192
+
193
+
194
+ __all__ = ["VectorStoreCall", "VectorStoreFake"]