fast-platform 0.12.2__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 (372) hide show
  1. admin/__init__.py +31 -0
  2. admin/abstraction.py +14 -0
  3. admin/abstractions.py +24 -0
  4. admin/audit_hooks.py +98 -0
  5. admin/crud.py +203 -0
  6. admin/repositories.py +105 -0
  7. admin/router.py +98 -0
  8. admin/schemas.py +49 -0
  9. analytics/__init__.py +31 -0
  10. analytics/abstraction.py +14 -0
  11. analytics/base.py +57 -0
  12. analytics/buffer.py +86 -0
  13. analytics/http_sink.py +89 -0
  14. analytics/middleware.py +89 -0
  15. analytics/pii.py +84 -0
  16. analytics/rate_limit.py +60 -0
  17. analytics/schema_registry.py +70 -0
  18. analytics/validating_backend.py +38 -0
  19. cache/__init__.py +17 -0
  20. cache/abstraction.py +14 -0
  21. cache/backend.py +222 -0
  22. channels/__init__.py +49 -0
  23. channels/abstraction.py +14 -0
  24. channels/acl.py +61 -0
  25. channels/base.py +25 -0
  26. channels/config_loader.py +52 -0
  27. channels/dto.py +20 -0
  28. channels/heartbeat.py +40 -0
  29. channels/hub.py +182 -0
  30. channels/kafka_backend.py +18 -0
  31. channels/metrics.py +53 -0
  32. channels/presence.py +123 -0
  33. channels/py.typed +0 -0
  34. channels/redis_backend.py +32 -0
  35. channels/subscriber_counters.py +96 -0
  36. configuration/__init__.py +92 -0
  37. configuration/abstraction.py +77 -0
  38. configuration/analytics.py +16 -0
  39. configuration/cache.py +16 -0
  40. configuration/datadog.py +16 -0
  41. configuration/db.py +16 -0
  42. configuration/events.py +16 -0
  43. configuration/feature_flags.py +16 -0
  44. configuration/identity_providers.py +16 -0
  45. configuration/jobs.py +16 -0
  46. configuration/kafka.py +54 -0
  47. configuration/llm.py +16 -0
  48. configuration/notifications.py +69 -0
  49. configuration/payments.py +60 -0
  50. configuration/queues.py +16 -0
  51. configuration/realtime.py +16 -0
  52. configuration/search.py +16 -0
  53. configuration/secrets.py +16 -0
  54. configuration/storage.py +16 -0
  55. configuration/streams.py +16 -0
  56. configuration/telemetry.py +16 -0
  57. configuration/vectors.py +16 -0
  58. datastores/__init__.py +25 -0
  59. datastores/abstraction.py +14 -0
  60. datastores/cassandra.py +72 -0
  61. datastores/cosmos.py +168 -0
  62. datastores/dynamo.py +119 -0
  63. datastores/elasticsearch.py +90 -0
  64. datastores/interfaces.py +175 -0
  65. datastores/mongo.py +86 -0
  66. datastores/py.typed +0 -0
  67. datastores/redis_kv.py +85 -0
  68. datastores/scylla.py +73 -0
  69. db/__init__.py +101 -0
  70. db/abstraction.py +14 -0
  71. db/async_dependency.py +39 -0
  72. db/async_engine.py +228 -0
  73. db/dependency.py +24 -0
  74. db/engine.py +153 -0
  75. db/migration_lock.py +69 -0
  76. db/replica.py +135 -0
  77. db/table.py +15 -0
  78. db/url.py +38 -0
  79. dtos/__init__.py +97 -0
  80. dtos/abstraction.py +14 -0
  81. dtos/analytics.py +13 -0
  82. dtos/aws_secrets.py +16 -0
  83. dtos/cache.py +16 -0
  84. dtos/celery_jobs.py +15 -0
  85. dtos/datadog.py +14 -0
  86. dtos/db.py +27 -0
  87. dtos/dramatiq_jobs.py +14 -0
  88. dtos/event_bridge.py +18 -0
  89. dtos/event_hubs.py +14 -0
  90. dtos/events.py +19 -0
  91. dtos/feature_flags.py +17 -0
  92. dtos/feature_flags_snapshot.py +13 -0
  93. dtos/gcp_secrets.py +14 -0
  94. dtos/http_sink.py +14 -0
  95. dtos/identity_providers.py +15 -0
  96. dtos/jobs.py +22 -0
  97. dtos/kafka.py +49 -0
  98. dtos/kafka_event.py +13 -0
  99. dtos/launchdarkly_feature_flags.py +14 -0
  100. dtos/llm.py +80 -0
  101. dtos/meilisearch.py +14 -0
  102. dtos/nats_config.py +16 -0
  103. dtos/notifications.py +120 -0
  104. dtos/oauth_provider.py +21 -0
  105. dtos/payments.py +59 -0
  106. dtos/pinecone_config.py +15 -0
  107. dtos/qdrant_config.py +14 -0
  108. dtos/queues.py +19 -0
  109. dtos/rabbit_mq_config.py +15 -0
  110. dtos/realtime.py +13 -0
  111. dtos/rq_jobs.py +15 -0
  112. dtos/s3_storage.py +16 -0
  113. dtos/scheduler_jobs.py +12 -0
  114. dtos/search.py +48 -0
  115. dtos/secrets.py +17 -0
  116. dtos/service_bus_config.py +14 -0
  117. dtos/sns_notification.py +16 -0
  118. dtos/sqs_config.py +16 -0
  119. dtos/storage.py +14 -0
  120. dtos/streams.py +15 -0
  121. dtos/telemetry.py +15 -0
  122. dtos/unleash_feature_flags.py +16 -0
  123. dtos/vault_secrets.py +15 -0
  124. dtos/vectors.py +17 -0
  125. dtos/weaviate_config.py +14 -0
  126. dtos/webrtc_ice_config.py +18 -0
  127. dtos/webrtc_ice_server.py +16 -0
  128. errors/__init__.py +44 -0
  129. errors/abstraction.py +7 -0
  130. errors/bad_input_error.py +27 -0
  131. errors/conflict_error.py +23 -0
  132. errors/crypto_configuration_error.py +22 -0
  133. errors/error.py +74 -0
  134. errors/forbidden_error.py +23 -0
  135. errors/llm_dependency_error.py +32 -0
  136. errors/llm_feature_not_available_error.py +22 -0
  137. errors/not_found_error.py +27 -0
  138. errors/rate_limit_error.py +22 -0
  139. errors/service_unavailable_error.py +23 -0
  140. errors/token_budget_exceeded_error.py +19 -0
  141. errors/unauthorized_error.py +22 -0
  142. errors/unexpected_response_error.py +27 -0
  143. errors/unsupported_llm_provider_error.py +22 -0
  144. events/__init__.py +15 -0
  145. events/abstraction.py +14 -0
  146. events/bus.py +238 -0
  147. fast_platform/__init__.py +15 -0
  148. fast_platform/abstraction.py +14 -0
  149. fast_platform/py.typed +0 -0
  150. fast_platform/taxonomy.py +159 -0
  151. fast_platform-0.12.2.dist-info/METADATA +208 -0
  152. fast_platform-0.12.2.dist-info/RECORD +372 -0
  153. fast_platform-0.12.2.dist-info/WHEEL +5 -0
  154. fast_platform-0.12.2.dist-info/licenses/LICENSE +9 -0
  155. fast_platform-0.12.2.dist-info/top_level.txt +35 -0
  156. features/__init__.py +36 -0
  157. features/abstraction.py +119 -0
  158. features/evaluation.py +30 -0
  159. features/flags.py +432 -0
  160. features/kill_switch.py +80 -0
  161. features/launchdarkly_client.py +69 -0
  162. features/request_context.py +58 -0
  163. features/snapshot.py +120 -0
  164. features/streaming.py +69 -0
  165. features/unleash_client.py +52 -0
  166. identity/__init__.py +42 -0
  167. identity/abstraction.py +14 -0
  168. identity/api_key.py +63 -0
  169. identity/claims_normalize.py +106 -0
  170. identity/jwks_cache.py +67 -0
  171. identity/multi_issuer_jwks.py +45 -0
  172. identity/providers.py +204 -0
  173. jobs/__init__.py +41 -0
  174. jobs/abstraction.py +14 -0
  175. jobs/cancel.py +75 -0
  176. jobs/celery_app.py +51 -0
  177. jobs/enqueue.py +244 -0
  178. jobs/result.py +224 -0
  179. jobs/schedule.py +76 -0
  180. jobs/timeout.py +40 -0
  181. kafka/__init__.py +58 -0
  182. kafka/abstraction.py +14 -0
  183. kafka/consumer.py +93 -0
  184. kafka/dlq.py +42 -0
  185. kafka/health.py +56 -0
  186. kafka/idempotent.py +56 -0
  187. kafka/lag.py +101 -0
  188. kafka/outbox.py +91 -0
  189. kafka/producer.py +104 -0
  190. kafka/serde.py +41 -0
  191. kafka/worker.py +33 -0
  192. llm/__init__.py +107 -0
  193. llm/abstraction.py +172 -0
  194. llm/budget.py +56 -0
  195. llm/caching.py +77 -0
  196. llm/constants.py +11 -0
  197. llm/instrumented.py +147 -0
  198. llm/providers/__init__.py +27 -0
  199. llm/providers/anthropic_llm_service.py +42 -0
  200. llm/providers/factory.py +59 -0
  201. llm/providers/gemini_llm_service.py +52 -0
  202. llm/providers/groq_llm_service.py +19 -0
  203. llm/providers/illm_service.py +14 -0
  204. llm/providers/mistral_llm_service.py +19 -0
  205. llm/providers/ollama_llm_service.py +41 -0
  206. llm/providers/openai_llm_service.py +34 -0
  207. llm/streaming.py +102 -0
  208. llm/token_usage.py +54 -0
  209. llm/tools.py +97 -0
  210. media/__init__.py +46 -0
  211. media/abstraction.py +85 -0
  212. media/generator.py +75 -0
  213. media/memory_store.py +58 -0
  214. media/pipeline.py +117 -0
  215. media/upload.py +53 -0
  216. media/variants.py +55 -0
  217. media/virus_scan.py +123 -0
  218. notifications/__init__.py +65 -0
  219. notifications/abstraction.py +14 -0
  220. notifications/digest.py +88 -0
  221. notifications/fanout.py +129 -0
  222. notifications/idempotency.py +77 -0
  223. notifications/preferences.py +36 -0
  224. notifications/push.py +64 -0
  225. notifications/retry_policy.py +44 -0
  226. notifications/service.py +60 -0
  227. notifications/templating.py +25 -0
  228. notifications/webhook_retry_compat.py +34 -0
  229. observability/__init__.py +24 -0
  230. observability/abstraction.py +14 -0
  231. observability/audit.py +342 -0
  232. observability/datadog.py +51 -0
  233. observability/logging.py +246 -0
  234. observability/metrics.py +345 -0
  235. observability/otel.py +85 -0
  236. observability/py.typed +0 -0
  237. observability/tracing.py +358 -0
  238. otel/__init__.py +11 -0
  239. otel/abstraction.py +14 -0
  240. otel/bridge.py +87 -0
  241. payments/__init__.py +49 -0
  242. payments/abstraction.py +91 -0
  243. payments/reconciliation.py +127 -0
  244. payments/sca.py +54 -0
  245. payments/subscription_events.py +37 -0
  246. payments/webhook_idempotency.py +52 -0
  247. queues/__init__.py +61 -0
  248. queues/abstraction.py +14 -0
  249. queues/broker.py +249 -0
  250. queues/dlq.py +124 -0
  251. queues/envelope.py +115 -0
  252. resilience/__init__.py +39 -0
  253. resilience/abstraction.py +14 -0
  254. resilience/circuit_breaker.py +253 -0
  255. resilience/py.typed +0 -0
  256. resilience/retry.py +276 -0
  257. search/__init__.py +28 -0
  258. search/abstraction.py +14 -0
  259. search/base.py +160 -0
  260. search/bulk.py +85 -0
  261. search/dto.py +7 -0
  262. search/meilisearch_backend.py +105 -0
  263. search/opensearch_backend.py +152 -0
  264. search/rollover.py +26 -0
  265. search/suggest.py +25 -0
  266. search/typesense_backend.py +121 -0
  267. secrets/__init__.py +51 -0
  268. secrets/abstraction.py +14 -0
  269. secrets/aws_backend.py +49 -0
  270. secrets/base.py +78 -0
  271. secrets/cache.py +88 -0
  272. secrets/gcp_backend.py +56 -0
  273. secrets/lease.py +106 -0
  274. secrets/redact.py +64 -0
  275. secrets/vault_backend.py +42 -0
  276. security/__init__.py +57 -0
  277. security/abstraction.py +14 -0
  278. security/api_keys.py +395 -0
  279. security/encryption.py +196 -0
  280. security/llm_provider_keys.py +55 -0
  281. security/py.typed +0 -0
  282. security/webhooks.py +292 -0
  283. service/__init__.py +17 -0
  284. service/abstraction.py +14 -0
  285. service/crypto.py +134 -0
  286. storage/__init__.py +21 -0
  287. storage/abstraction.py +14 -0
  288. storage/azure_backend.py +90 -0
  289. storage/base.py +129 -0
  290. storage/gcs_backend.py +85 -0
  291. storage/local_backend.py +63 -0
  292. storage/multipart.py +138 -0
  293. storage/s3_backend.py +108 -0
  294. streams/__init__.py +11 -0
  295. streams/abstraction.py +14 -0
  296. streams/abstractions.py +68 -0
  297. streams/market.py +235 -0
  298. tenancy/__init__.py +57 -0
  299. tenancy/abstraction.py +14 -0
  300. tenancy/context.py +198 -0
  301. tenancy/middleware.py +149 -0
  302. tenancy/resolution.py +179 -0
  303. utils/__init__.py +70 -0
  304. utils/abstraction.py +26 -0
  305. utils/archive.py +158 -0
  306. utils/clock/__init__.py +15 -0
  307. utils/clock/frozen_clock.py +23 -0
  308. utils/clock/protocol.py +18 -0
  309. utils/clock/registry.py +40 -0
  310. utils/clock/system_clock.py +16 -0
  311. utils/currency.py +61 -0
  312. utils/datatype/__init__.py +18 -0
  313. utils/datatype/abstraction.py +14 -0
  314. utils/datatype/boolean.py +71 -0
  315. utils/datatype/integer.py +70 -0
  316. utils/datatype/string.py +54 -0
  317. utils/decimal.py +73 -0
  318. utils/digests.py +41 -0
  319. utils/encryption/__init__.py +18 -0
  320. utils/encryption/abstraction.py +27 -0
  321. utils/encryption/aes.py +50 -0
  322. utils/encryption/fernet.py +108 -0
  323. utils/hashing.py +168 -0
  324. utils/html/__init__.py +5 -0
  325. utils/html/html.py +125 -0
  326. utils/html/html_strip_tags_parser.py +35 -0
  327. utils/idempotency.py +59 -0
  328. utils/media/abstraction.py +14 -0
  329. utils/media/audio.py +93 -0
  330. utils/media/image.py +163 -0
  331. utils/media/pdf.py +141 -0
  332. utils/media/text.py +117 -0
  333. utils/media/video.py +96 -0
  334. utils/metrics/__init__.py +13 -0
  335. utils/metrics/counter.py +30 -0
  336. utils/metrics/histogram.py +28 -0
  337. utils/metrics/registry.py +42 -0
  338. utils/nutrition.py +19 -0
  339. utils/optional_imports.py +63 -0
  340. utils/request_id_context.py +39 -0
  341. utils/retry.py +54 -0
  342. utils/sanitization/abstraction.py +14 -0
  343. utils/sanitization/json.py +42 -0
  344. utils/structured_log/__init__.py +14 -0
  345. utils/structured_log/fields.py +19 -0
  346. utils/structured_log/log.py +63 -0
  347. utils/structured_log/sink.py +16 -0
  348. utils/time.py +40 -0
  349. vectors/__init__.py +28 -0
  350. vectors/abstraction.py +14 -0
  351. vectors/base.py +86 -0
  352. vectors/names.py +75 -0
  353. vectors/pinecone_backend.py +53 -0
  354. vectors/qdrant_backend.py +53 -0
  355. vectors/weaviate_backend.py +55 -0
  356. versioning/__init__.py +19 -0
  357. versioning/abstraction.py +14 -0
  358. versioning/router.py +340 -0
  359. webhooks/__init__.py +27 -0
  360. webhooks/abstraction.py +14 -0
  361. webhooks/delivery.py +102 -0
  362. webhooks/fastapi_deps.py +86 -0
  363. webhooks/signing.py +66 -0
  364. webrtc/__init__.py +37 -0
  365. webrtc/abstraction.py +14 -0
  366. webrtc/config_loader.py +54 -0
  367. webrtc/consent.py +38 -0
  368. webrtc/dto.py +24 -0
  369. webrtc/ice_config.py +42 -0
  370. webrtc/rooms.py +100 -0
  371. webrtc/signaling.py +129 -0
  372. webrtc/turn_twilio.py +78 -0
admin/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """
2
+ fast_admin – CRUD API for admin resources (users, roles, audit log) for FastMVC.
3
+ """
4
+
5
+ from .audit_hooks import (
6
+ AuditLogHook,
7
+ AuditTarget,
8
+ as_audit_hook,
9
+ audit_repository_hook,
10
+ )
11
+ from .crud import crud_router_from_model
12
+ from .repositories import IAdminRoleRepository, IAdminUserRepository, IAuditLogRepository
13
+ from .router import get_admin_router
14
+ from .schemas import AdminRoleSummary, AdminUserSummary, AuditLogEntry
15
+
16
+ __version__ = "0.1.1"
17
+
18
+ __all__ = [
19
+ "AdminUserSummary",
20
+ "AdminRoleSummary",
21
+ "AuditLogEntry",
22
+ "IAdminUserRepository",
23
+ "IAdminRoleRepository",
24
+ "IAuditLogRepository",
25
+ "get_admin_router",
26
+ "crud_router_from_model",
27
+ "AuditLogHook",
28
+ "AuditTarget",
29
+ "audit_repository_hook",
30
+ "as_audit_hook",
31
+ ]
admin/abstraction.py ADDED
@@ -0,0 +1,14 @@
1
+ """admin package abstractions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC
6
+
7
+
8
+ class IAdmin(ABC):
9
+ """Marker base for concrete types in the ``admin`` package."""
10
+
11
+ __slots__ = ()
12
+
13
+
14
+ __all__ = ["IAdmin"]
admin/abstractions.py ADDED
@@ -0,0 +1,24 @@
1
+ """
2
+ Admin resource abstractions: users, roles, audit log.
3
+
4
+ Prefer importing from :mod:`admin.schemas` and :mod:`admin.repositories`;
5
+ this module re-exports the same names for backward compatibility.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .repositories import (
11
+ IAdminRoleRepository,
12
+ IAdminUserRepository,
13
+ IAuditLogRepository,
14
+ )
15
+ from .schemas import AdminRoleSummary, AdminUserSummary, AuditLogEntry
16
+
17
+ __all__ = [
18
+ "AdminRoleSummary",
19
+ "AdminUserSummary",
20
+ "AuditLogEntry",
21
+ "IAdminUserRepository",
22
+ "IAdminRoleRepository",
23
+ "IAuditLogRepository",
24
+ ]
admin/audit_hooks.py ADDED
@@ -0,0 +1,98 @@
1
+ """
2
+ Optional audit hooks for CRUD and admin flows.
3
+
4
+ Use :class:`AuditLogHook` with :func:`crud_router_from_model`, or wrap
5
+ :class:`~fast_admin.repositories.IAuditLogRepository` with
6
+ :func:`audit_repository_hook`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Callable, Optional, Protocol, Union, cast
12
+
13
+ from .abstractions import IAuditLogRepository
14
+
15
+
16
+ class AuditLogHook(Protocol):
17
+ """Async callback invoked after a successful create, update, or delete."""
18
+
19
+ async def __call__(
20
+ self,
21
+ *,
22
+ action: str,
23
+ resource_type: str,
24
+ resource_id: Optional[str],
25
+ details: Optional[dict[str, Any]],
26
+ request: Optional[Any] = None,
27
+ ) -> None: ...
28
+
29
+
30
+ AuditTarget = Union[AuditLogHook, IAuditLogRepository, None]
31
+
32
+
33
+ def audit_repository_hook(
34
+ repo: IAuditLogRepository,
35
+ *,
36
+ get_actor_id: Optional[Callable[[Any], Optional[str]]] = None,
37
+ get_ip_address: Optional[Callable[[Any], Optional[str]]] = None,
38
+ get_user_agent: Optional[Callable[[Any], Optional[str]]] = None,
39
+ ) -> AuditLogHook:
40
+ """
41
+ Wrap an :class:`~fast_admin.repositories.IAuditLogRepository` as an
42
+ :class:`AuditLogHook`, mapping ``request`` through optional extractors.
43
+ """
44
+
45
+ async def hook(
46
+ *,
47
+ action: str,
48
+ resource_type: str,
49
+ resource_id: Optional[str],
50
+ details: Optional[dict[str, Any]],
51
+ request: Optional[Any] = None,
52
+ ) -> None:
53
+ actor_id = get_actor_id(request) if get_actor_id and request else None
54
+ ip = get_ip_address(request) if get_ip_address and request else None
55
+ ua = get_user_agent(request) if get_user_agent and request else None
56
+ await repo.append(
57
+ action,
58
+ resource_type,
59
+ actor_id=actor_id,
60
+ resource_id=resource_id,
61
+ details=details,
62
+ ip_address=ip,
63
+ user_agent=ua,
64
+ )
65
+
66
+ return hook
67
+
68
+
69
+ def as_audit_hook(
70
+ target: AuditTarget,
71
+ *,
72
+ get_actor_id: Optional[Callable[[Any], Optional[str]]] = None,
73
+ get_ip_address: Optional[Callable[[Any], Optional[str]]] = None,
74
+ get_user_agent: Optional[Callable[[Any], Optional[str]]] = None,
75
+ ) -> Optional[AuditLogHook]:
76
+ """
77
+ Normalize ``None``, a plain :class:`AuditLogHook`, or an
78
+ :class:`~fast_admin.repositories.IAuditLogRepository` into a single hook
79
+ (or ``None``).
80
+ """
81
+ if target is None:
82
+ return None
83
+ if isinstance(target, IAuditLogRepository):
84
+ return audit_repository_hook(
85
+ cast("IAuditLogRepository", target),
86
+ get_actor_id=get_actor_id,
87
+ get_ip_address=get_ip_address,
88
+ get_user_agent=get_user_agent,
89
+ )
90
+ return cast("AuditLogHook", target)
91
+
92
+
93
+ __all__ = [
94
+ "AuditLogHook",
95
+ "AuditTarget",
96
+ "audit_repository_hook",
97
+ "as_audit_hook",
98
+ ]
admin/crud.py ADDED
@@ -0,0 +1,203 @@
1
+ """
2
+ Generic FastAPI CRUD router from a SQLAlchemy 2.0 model and Pydantic v2 schemas.
3
+
4
+ Requires SQLAlchemy (included with ``fast-platform``; install ``sqlalchemy`` if using a minimal env).
5
+
6
+ Note: ``from __future__ import annotations`` is omitted so FastAPI can resolve
7
+ dynamic ``create_schema`` / ``update_schema`` / ``read_schema`` types on routes.
8
+ """
9
+
10
+ from typing import Any, Callable, Optional, Type
11
+
12
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
13
+ from pydantic import BaseModel
14
+
15
+ from .audit_hooks import AuditLogHook, AuditTarget, as_audit_hook
16
+
17
+
18
+ def crud_router_from_model(
19
+ model: Any,
20
+ *,
21
+ create_schema: Type[BaseModel],
22
+ update_schema: Type[BaseModel],
23
+ read_schema: Type[BaseModel],
24
+ session_dep: Callable[..., Any],
25
+ resource_type: Optional[str] = None,
26
+ prefix: str = "",
27
+ tags: Optional[list[str]] = None,
28
+ audit: AuditTarget = None,
29
+ get_actor_id_from_request: Optional[Callable[[Request], Optional[str]]] = None,
30
+ get_ip_from_request: Optional[Callable[[Request], Optional[str]]] = None,
31
+ get_user_agent_from_request: Optional[Callable[[Request], Optional[str]]] = None,
32
+ ) -> APIRouter:
33
+ """
34
+ Build an ``APIRouter`` with list/create/read/update/delete for a single table.
35
+
36
+ **Requirements**
37
+
38
+ - Model must use a **single-column** primary key.
39
+ - ``read_schema`` should use ``model_config = ConfigDict(from_attributes=True)``
40
+ so ORM instances serialize correctly.
41
+ - ``create_schema`` / ``update_schema`` field names should match mapped
42
+ attribute names on the model (excluding the primary key on create if
43
+ auto-generated).
44
+
45
+ **Audit**
46
+
47
+ Pass ``audit`` as an :class:`~fast_admin.audit_hooks.AuditLogHook` or
48
+ :class:`~fast_admin.repositories.IAuditLogRepository`. Hooks run **after**
49
+ a successful create, update, or delete. Use ``get_*_from_request`` when
50
+ ``audit`` is a repository to populate actor / IP / user-agent.
51
+
52
+ **Session**
53
+
54
+ ``session_dep`` is used as ``Depends(session_dep)`` and must yield or return
55
+ a SQLAlchemy :class:`~sqlalchemy.orm.Session`.
56
+ """
57
+ try:
58
+ from sqlalchemy import inspect as sa_inspect
59
+ from sqlalchemy import select
60
+ except ImportError as e: # pragma: no cover - exercised when sqlalchemy missing
61
+ raise RuntimeError(
62
+ "crud_router_from_model requires SQLAlchemy. Install: pip install sqlalchemy"
63
+ ) from e
64
+
65
+ mapper = sa_inspect(model).mapper
66
+ if len(mapper.primary_key) != 1:
67
+ raise ValueError("crud_router_from_model requires exactly one primary key column")
68
+
69
+ pk_col = mapper.primary_key[0]
70
+ pk_name = pk_col.key
71
+ pk_python = getattr(pk_col.type, "python_type", None)
72
+
73
+ rtype = resource_type or getattr(model, "__tablename__", model.__name__).lower()
74
+
75
+ def _parse_id(raw: str) -> Any:
76
+ if pk_python is int:
77
+ try:
78
+ return int(raw)
79
+ except ValueError as err:
80
+ raise HTTPException(
81
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid id"
82
+ ) from err
83
+ return raw
84
+
85
+ hook: Optional[AuditLogHook] = as_audit_hook(
86
+ audit,
87
+ get_actor_id=get_actor_id_from_request,
88
+ get_ip_address=get_ip_from_request,
89
+ get_user_agent=get_user_agent_from_request,
90
+ )
91
+
92
+ async def _audit(
93
+ *,
94
+ action: str,
95
+ resource_id: Optional[str],
96
+ details: Optional[dict[str, Any]],
97
+ request: Optional[Request],
98
+ ) -> None:
99
+ if hook is None:
100
+ return
101
+ await hook(
102
+ action=action,
103
+ resource_type=rtype,
104
+ resource_id=resource_id,
105
+ details=details,
106
+ request=request,
107
+ )
108
+
109
+ tag_list = tags if tags is not None else [rtype]
110
+ router = APIRouter(prefix=prefix, tags=tag_list)
111
+
112
+ @router.get("", response_model=list[read_schema])
113
+ async def list_items(
114
+ skip: int = Query(0, ge=0),
115
+ limit: int = Query(50, ge=1, le=500),
116
+ db: Any = Depends(session_dep),
117
+ ) -> list[BaseModel]:
118
+ rows = db.scalars(select(model).offset(skip).limit(limit)).all()
119
+ return [read_schema.model_validate(r) for r in rows]
120
+
121
+ @router.post("", response_model=read_schema, status_code=status.HTTP_201_CREATED)
122
+ async def create_item(
123
+ body: create_schema,
124
+ request: Request,
125
+ db: Any = Depends(session_dep),
126
+ ) -> BaseModel:
127
+ data = body.model_dump(exclude_unset=True)
128
+ if pk_name in data:
129
+ del data[pk_name]
130
+ obj = model(**data)
131
+ db.add(obj)
132
+ db.commit()
133
+ db.refresh(obj)
134
+ pk_val = getattr(obj, pk_name)
135
+ await _audit(
136
+ action="create",
137
+ resource_id=str(pk_val),
138
+ details={"payload": body.model_dump()},
139
+ request=request,
140
+ )
141
+ return read_schema.model_validate(obj)
142
+
143
+ @router.get("/{item_id}", response_model=read_schema)
144
+ async def get_item(
145
+ item_id: str,
146
+ db: Any = Depends(session_dep),
147
+ ) -> BaseModel:
148
+ pk = _parse_id(item_id)
149
+ obj = db.get(model, pk)
150
+ if obj is None:
151
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
152
+ return read_schema.model_validate(obj)
153
+
154
+ @router.patch("/{item_id}", response_model=read_schema)
155
+ async def update_item(
156
+ item_id: str,
157
+ body: update_schema,
158
+ request: Request,
159
+ db: Any = Depends(session_dep),
160
+ ) -> BaseModel:
161
+ pk = _parse_id(item_id)
162
+ obj = db.get(model, pk)
163
+ if obj is None:
164
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
165
+ patch = body.model_dump(exclude_unset=True)
166
+ before = {k: getattr(obj, k, None) for k in patch}
167
+ for k, v in patch.items():
168
+ setattr(obj, k, v)
169
+ db.commit()
170
+ db.refresh(obj)
171
+ await _audit(
172
+ action="update",
173
+ resource_id=str(pk),
174
+ details={"before": before, "after": patch},
175
+ request=request,
176
+ )
177
+ return read_schema.model_validate(obj)
178
+
179
+ @router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
180
+ async def delete_item(
181
+ item_id: str,
182
+ request: Request,
183
+ db: Any = Depends(session_dep),
184
+ ) -> None:
185
+ pk = _parse_id(item_id)
186
+ obj = db.get(model, pk)
187
+ if obj is None:
188
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
189
+ snapshot = read_schema.model_validate(obj).model_dump()
190
+ db.delete(obj)
191
+ db.commit()
192
+ await _audit(
193
+ action="delete",
194
+ resource_id=str(pk),
195
+ details={"snapshot": snapshot},
196
+ request=request,
197
+ )
198
+ return None
199
+
200
+ return router
201
+
202
+
203
+ __all__ = ["crud_router_from_model"]
admin/repositories.py ADDED
@@ -0,0 +1,105 @@
1
+ """
2
+ Repository interfaces for admin users, roles, and audit log.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from abc import ABC, abstractmethod
8
+ from typing import TYPE_CHECKING, Any, Optional
9
+
10
+ from .abstraction import IAdmin
11
+
12
+ if TYPE_CHECKING:
13
+ from datetime import datetime
14
+
15
+ from .schemas import AdminRoleSummary, AdminUserSummary, AuditLogEntry
16
+
17
+ __all__ = [
18
+ "IAdminRoleRepository",
19
+ "IAdminUserRepository",
20
+ "IAuditLogRepository",
21
+ ]
22
+
23
+
24
+ class IAdminUserRepository(IAdmin, ABC):
25
+ """Admin view over users (list, toggle active, assign roles)."""
26
+
27
+ @abstractmethod
28
+ async def list_users(
29
+ self,
30
+ *,
31
+ skip: int = 0,
32
+ limit: int = 50,
33
+ search: Optional[str] = None,
34
+ active_only: Optional[bool] = None,
35
+ ) -> list[AdminUserSummary]:
36
+ raise NotImplementedError
37
+
38
+ @abstractmethod
39
+ async def get_user(self, user_id: str) -> Optional[AdminUserSummary]:
40
+ raise NotImplementedError
41
+
42
+ @abstractmethod
43
+ async def set_user_active(self, user_id: str, is_active: bool) -> None:
44
+ raise NotImplementedError
45
+
46
+ @abstractmethod
47
+ async def set_user_roles(self, user_id: str, role_ids: list[str]) -> None:
48
+ raise NotImplementedError
49
+
50
+
51
+ class IAdminRoleRepository(IAdmin, ABC):
52
+ """Admin CRUD for roles."""
53
+
54
+ @abstractmethod
55
+ async def list_roles(self) -> list[AdminRoleSummary]:
56
+ raise NotImplementedError
57
+
58
+ @abstractmethod
59
+ async def get_role(self, role_id: str) -> Optional[AdminRoleSummary]:
60
+ raise NotImplementedError
61
+
62
+ @abstractmethod
63
+ async def create_role(self, name: str, permissions: list[str]) -> AdminRoleSummary:
64
+ raise NotImplementedError
65
+
66
+ @abstractmethod
67
+ async def update_role(
68
+ self, role_id: str, name: Optional[str] = None, permissions: Optional[list[str]] = None
69
+ ) -> Optional[AdminRoleSummary]:
70
+ raise NotImplementedError
71
+
72
+ @abstractmethod
73
+ async def delete_role(self, role_id: str) -> None:
74
+ raise NotImplementedError
75
+
76
+
77
+ class IAuditLogRepository(IAdmin, ABC):
78
+ """Append-only audit log for admin review."""
79
+
80
+ @abstractmethod
81
+ async def append(
82
+ self,
83
+ action: str,
84
+ resource_type: str,
85
+ *,
86
+ actor_id: Optional[str] = None,
87
+ resource_id: Optional[str] = None,
88
+ details: Optional[dict[str, Any]] = None,
89
+ ip_address: Optional[str] = None,
90
+ user_agent: Optional[str] = None,
91
+ ) -> AuditLogEntry:
92
+ raise NotImplementedError
93
+
94
+ @abstractmethod
95
+ async def list_entries(
96
+ self,
97
+ *,
98
+ skip: int = 0,
99
+ limit: int = 100,
100
+ actor_id: Optional[str] = None,
101
+ resource_type: Optional[str] = None,
102
+ resource_id: Optional[str] = None,
103
+ since: Optional[datetime] = None,
104
+ ) -> list[AuditLogEntry]:
105
+ raise NotImplementedError
admin/router.py ADDED
@@ -0,0 +1,98 @@
1
+ """
2
+ Optional FastAPI router for admin API.
3
+
4
+ Mount under /admin and protect with your auth (e.g. require admin role).
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ from fastapi import APIRouter, Query
10
+ from pydantic import BaseModel
11
+
12
+ from .repositories import IAdminRoleRepository, IAdminUserRepository, IAuditLogRepository
13
+ from .schemas import AdminRoleSummary, AdminUserSummary, AuditLogEntry
14
+
15
+
16
+ def get_admin_router(
17
+ user_repo: Optional[IAdminUserRepository] = None,
18
+ role_repo: Optional[IAdminRoleRepository] = None,
19
+ audit_repo: Optional[IAuditLogRepository] = None,
20
+ prefix: str = "/admin",
21
+ ) -> APIRouter:
22
+ """
23
+ Build an admin API router. Pass repos for the sections you want to expose.
24
+ """
25
+ router = APIRouter(prefix=prefix, tags=["admin"])
26
+
27
+ if user_repo is not None:
28
+
29
+ @router.get("/users", response_model=list[AdminUserSummary])
30
+ async def list_users(
31
+ skip: int = Query(0, ge=0),
32
+ limit: int = Query(50, ge=1, le=100),
33
+ search: Optional[str] = None,
34
+ active_only: Optional[bool] = None,
35
+ ) -> list[AdminUserSummary]:
36
+ return await user_repo.list_users(
37
+ skip=skip, limit=limit, search=search, active_only=active_only
38
+ )
39
+
40
+ @router.get("/users/{user_id}", response_model=AdminUserSummary)
41
+ async def get_user(user_id: str) -> AdminUserSummary:
42
+ u = await user_repo.get_user(user_id)
43
+ if u is None:
44
+ from fastapi import HTTPException
45
+
46
+ raise HTTPException(404, "User not found")
47
+ return u
48
+
49
+ class ActiveBody(BaseModel):
50
+ is_active: bool
51
+
52
+ @router.patch("/users/{user_id}/active")
53
+ async def set_user_active(user_id: str, body: ActiveBody) -> dict[str, bool]:
54
+ await user_repo.set_user_active(user_id, body.is_active)
55
+ return {"ok": True}
56
+
57
+ class RolesBody(BaseModel):
58
+ role_ids: list[str] = []
59
+
60
+ @router.put("/users/{user_id}/roles")
61
+ async def set_user_roles(user_id: str, body: RolesBody) -> dict[str, bool]:
62
+ await user_repo.set_user_roles(user_id, body.role_ids)
63
+ return {"ok": True}
64
+
65
+ if role_repo is not None:
66
+
67
+ @router.get("/roles", response_model=list[AdminRoleSummary])
68
+ async def list_roles() -> list[AdminRoleSummary]:
69
+ return await role_repo.list_roles()
70
+
71
+ @router.get("/roles/{role_id}", response_model=AdminRoleSummary)
72
+ async def get_role(role_id: str) -> AdminRoleSummary:
73
+ r = await role_repo.get_role(role_id)
74
+ if r is None:
75
+ from fastapi import HTTPException
76
+
77
+ raise HTTPException(404, "Role not found")
78
+ return r
79
+
80
+ if audit_repo is not None:
81
+
82
+ @router.get("/audit", response_model=list[AuditLogEntry])
83
+ async def list_audit(
84
+ skip: int = Query(0, ge=0),
85
+ limit: int = Query(100, ge=1, le=500),
86
+ actor_id: Optional[str] = None,
87
+ resource_type: Optional[str] = None,
88
+ resource_id: Optional[str] = None,
89
+ ) -> list[AuditLogEntry]:
90
+ return await audit_repo.list_entries(
91
+ skip=skip,
92
+ limit=limit,
93
+ actor_id=actor_id,
94
+ resource_type=resource_type,
95
+ resource_id=resource_id,
96
+ )
97
+
98
+ return router
admin/schemas.py ADDED
@@ -0,0 +1,49 @@
1
+ """
2
+ Pydantic DTOs for the admin API (users, roles, audit log).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from datetime import datetime # noqa: TC003
8
+ from typing import Any, Optional
9
+
10
+ from pydantic import BaseModel
11
+
12
+ __all__ = [
13
+ "AdminRoleSummary",
14
+ "AdminUserSummary",
15
+ "AuditLogEntry",
16
+ ]
17
+
18
+
19
+ class AdminUserSummary(BaseModel):
20
+ """Summary of a user for admin listing."""
21
+
22
+ id: str
23
+ email: str
24
+ is_active: bool
25
+ created_at: datetime
26
+ roles: list[str] = []
27
+
28
+
29
+ class AdminRoleSummary(BaseModel):
30
+ """Summary of a role for admin listing."""
31
+
32
+ id: str
33
+ name: str
34
+ permissions: list[str] = []
35
+
36
+
37
+ class AuditLogEntry(BaseModel):
38
+ """Single audit log entry."""
39
+
40
+ id: str
41
+ actor_id: Optional[str] = None
42
+ actor_type: str = "user"
43
+ action: str
44
+ resource_type: str
45
+ resource_id: Optional[str] = None
46
+ details: dict[str, Any] = {}
47
+ ip_address: Optional[str] = None
48
+ user_agent: Optional[str] = None
49
+ created_at: datetime
analytics/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """
2
+ fast_analytics – Analytics and event tracking for FastMVC.
3
+ """
4
+
5
+ from fast_platform import AnalyticsConfiguration, AnalyticsConfigurationDTO
6
+
7
+ from .base import IAnalyticsBackend, build_analytics_client
8
+ from .buffer import BufferedAnalyticsBackend
9
+ from .middleware import AnalyticsSamplingMiddleware, default_analytics_user_key
10
+ from .pii import ScrubbingAnalyticsBackend, scrub_pii_properties
11
+ from .rate_limit import RateLimitedAnalyticsBackend
12
+ from .schema_registry import EventSchemaRegistry, parse_versioned_event_name
13
+ from .validating_backend import ValidatingAnalyticsBackend
14
+
15
+ __version__ = "0.3.0"
16
+
17
+ __all__ = [
18
+ "AnalyticsSamplingMiddleware",
19
+ "BufferedAnalyticsBackend",
20
+ "EventSchemaRegistry",
21
+ "IAnalyticsBackend",
22
+ "RateLimitedAnalyticsBackend",
23
+ "ScrubbingAnalyticsBackend",
24
+ "ValidatingAnalyticsBackend",
25
+ "AnalyticsConfiguration",
26
+ "AnalyticsConfigurationDTO",
27
+ "build_analytics_client",
28
+ "default_analytics_user_key",
29
+ "parse_versioned_event_name",
30
+ "scrub_pii_properties",
31
+ ]
@@ -0,0 +1,14 @@
1
+ """analytics package abstractions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC
6
+
7
+
8
+ class IAnalytics(ABC):
9
+ """Marker base for concrete types in the ``analytics`` package."""
10
+
11
+ __slots__ = ()
12
+
13
+
14
+ __all__ = ["IAnalytics"]