fastloom 0.4.2__tar.gz

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 (63) hide show
  1. fastloom-0.4.2/PKG-INFO +155 -0
  2. fastloom-0.4.2/README.md +109 -0
  3. fastloom-0.4.2/fastloom/__init__.py +0 -0
  4. fastloom-0.4.2/fastloom/auth/__init__.py +5 -0
  5. fastloom-0.4.2/fastloom/auth/depends.py +145 -0
  6. fastloom-0.4.2/fastloom/auth/schemas.py +69 -0
  7. fastloom-0.4.2/fastloom/cache/__init__.py +0 -0
  8. fastloom-0.4.2/fastloom/cache/base.py +33 -0
  9. fastloom-0.4.2/fastloom/cache/gate.py +67 -0
  10. fastloom-0.4.2/fastloom/cache/healthcheck.py +22 -0
  11. fastloom-0.4.2/fastloom/cache/lifehooks.py +37 -0
  12. fastloom-0.4.2/fastloom/cache/settings.py +13 -0
  13. fastloom-0.4.2/fastloom/crypto.py +20 -0
  14. fastloom-0.4.2/fastloom/date.py +24 -0
  15. fastloom-0.4.2/fastloom/db/__init__.py +0 -0
  16. fastloom-0.4.2/fastloom/db/healthcheck.py +22 -0
  17. fastloom-0.4.2/fastloom/db/lifehooks.py +83 -0
  18. fastloom-0.4.2/fastloom/db/schemas.py +89 -0
  19. fastloom-0.4.2/fastloom/db/settings.py +6 -0
  20. fastloom-0.4.2/fastloom/db/signals.py +135 -0
  21. fastloom-0.4.2/fastloom/db/transactions.py +48 -0
  22. fastloom-0.4.2/fastloom/file/__init__.py +0 -0
  23. fastloom-0.4.2/fastloom/file/models.py +46 -0
  24. fastloom-0.4.2/fastloom/file/schema.py +112 -0
  25. fastloom-0.4.2/fastloom/file/signals.py +32 -0
  26. fastloom-0.4.2/fastloom/file/utils.py +19 -0
  27. fastloom-0.4.2/fastloom/healthcheck/__init__.py +0 -0
  28. fastloom-0.4.2/fastloom/healthcheck/handler.py +26 -0
  29. fastloom-0.4.2/fastloom/i18n/__init__.py +0 -0
  30. fastloom-0.4.2/fastloom/i18n/base.py +65 -0
  31. fastloom-0.4.2/fastloom/i18n/handler.py +70 -0
  32. fastloom-0.4.2/fastloom/i18n/settings.py +7 -0
  33. fastloom-0.4.2/fastloom/i18n/types.py +24 -0
  34. fastloom-0.4.2/fastloom/launcher/__init__.py +0 -0
  35. fastloom-0.4.2/fastloom/launcher/main.py +108 -0
  36. fastloom-0.4.2/fastloom/launcher/schemas.py +164 -0
  37. fastloom-0.4.2/fastloom/launcher/settings.py +13 -0
  38. fastloom-0.4.2/fastloom/launcher/utils.py +93 -0
  39. fastloom-0.4.2/fastloom/meta.py +57 -0
  40. fastloom-0.4.2/fastloom/monitoring.py +301 -0
  41. fastloom-0.4.2/fastloom/observability/__init__.py +0 -0
  42. fastloom-0.4.2/fastloom/observability/settings.py +10 -0
  43. fastloom-0.4.2/fastloom/py.typed +0 -0
  44. fastloom-0.4.2/fastloom/settings/__init__.py +0 -0
  45. fastloom-0.4.2/fastloom/settings/base.py +38 -0
  46. fastloom-0.4.2/fastloom/settings/utils.py +14 -0
  47. fastloom-0.4.2/fastloom/signals/__init__.py +0 -0
  48. fastloom-0.4.2/fastloom/signals/depends.py +311 -0
  49. fastloom-0.4.2/fastloom/signals/healthcheck.py +21 -0
  50. fastloom-0.4.2/fastloom/signals/lifehooks.py +47 -0
  51. fastloom-0.4.2/fastloom/signals/middlewares.py +60 -0
  52. fastloom-0.4.2/fastloom/signals/settings.py +7 -0
  53. fastloom-0.4.2/fastloom/tenant/__init__.py +3 -0
  54. fastloom-0.4.2/fastloom/tenant/depends.py +218 -0
  55. fastloom-0.4.2/fastloom/tenant/handler.py +63 -0
  56. fastloom-0.4.2/fastloom/tenant/protocols.py +16 -0
  57. fastloom-0.4.2/fastloom/tenant/schemas.py +15 -0
  58. fastloom-0.4.2/fastloom/tenant/settings.py +212 -0
  59. fastloom-0.4.2/fastloom/tenant/utils.py +79 -0
  60. fastloom-0.4.2/fastloom/tests.py +50 -0
  61. fastloom-0.4.2/fastloom/types.py +88 -0
  62. fastloom-0.4.2/fastloom/utils.py +27 -0
  63. fastloom-0.4.2/pyproject.toml +103 -0
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.3
2
+ Name: fastloom
3
+ Version: 0.4.2
4
+ Summary: Core package
5
+ Requires-Python: >=3.12,<3.14
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3.12
8
+ Classifier: Programming Language :: Python :: 3.13
9
+ Provides-Extra: celery
10
+ Provides-Extra: dev
11
+ Provides-Extra: fastapi
12
+ Provides-Extra: httpx
13
+ Provides-Extra: mongodb
14
+ Provides-Extra: openai
15
+ Provides-Extra: rabbit
16
+ Provides-Extra: redis
17
+ Provides-Extra: requests
18
+ Requires-Dist: babel (>=2.17.0,<3.0.0)
19
+ Requires-Dist: beanie (>=2.0.0,<3.0.0) ; extra == "mongodb"
20
+ Requires-Dist: celery (>=5.5.3,<6.0.0) ; extra == "celery"
21
+ Requires-Dist: fastapi (>=0,<1) ; extra == "fastapi" or extra == "rabbit"
22
+ Requires-Dist: faststream[otel,rabbit] (>=0.6.0,<0.7.0) ; extra == "rabbit"
23
+ Requires-Dist: httpx (>=0.28.0,<0.29.0) ; extra == "httpx"
24
+ Requires-Dist: ipykernel (>=6.30.0,<7.0.0) ; extra == "dev"
25
+ Requires-Dist: jdatetime (>=4.1.1,<5.0.0)
26
+ Requires-Dist: jinja2 (>=3.1.6,<4.0.0)
27
+ Requires-Dist: logfire[asgi,celery,fastapi,httpx,openai,pymongo,redis,requests,system-metrics] (>=4.0.0,<5.0.0)
28
+ Requires-Dist: mypy (>=1.17.0,<2.0.0) ; extra == "dev"
29
+ Requires-Dist: openai (>=1,<2) ; extra == "openai"
30
+ Requires-Dist: opentelemetry-distro
31
+ Requires-Dist: opentelemetry-exporter-otlp
32
+ Requires-Dist: opentelemetry-instrumentation-aio-pika ; extra == "rabbit"
33
+ Requires-Dist: orjson (==3.10.14)
34
+ Requires-Dist: pre-commit (>=4.2.0,<5.0.0) ; extra == "dev"
35
+ Requires-Dist: pydantic[email] (>=2.12,<3.0)
36
+ Requires-Dist: python-jose (>=3.5.0,<4.0.0)
37
+ Requires-Dist: python-multipart ; extra == "fastapi"
38
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
39
+ Requires-Dist: redis-om (>=1.0,<2.0) ; extra == "redis"
40
+ Requires-Dist: requests (>=2.32.0,<3.0.0) ; extra == "requests"
41
+ Requires-Dist: ruff (>=0.12.5,<0.13.0) ; extra == "dev"
42
+ Requires-Dist: sentry-sdk[fastapi] (>=2.0.0,<3.0.0)
43
+ Requires-Dist: uvicorn (>=0.35.0,<0.36.0) ; extra == "fastapi" or extra == "rabbit"
44
+ Description-Content-Type: text/markdown
45
+
46
+ # Fastloom – The Open Foundation for Building Event-Driven Services
47
+
48
+ Fastloom is a lightweight, batteries-included foundation for building modern backends. Define your settings, schemas, and endpoints; Fastloom wires up the rest: FastAPI, Mongo (Beanie), Rabbit (FastStream), metrics/traces/logs/errors, and more.
49
+
50
+ Think of it as the glue for your stack: web, messaging, caching, DB, observability, and integrations with best-in-class tools.
51
+
52
+ ---
53
+
54
+ ## Why FastLoom
55
+
56
+ - No boilerplate: minimal scaffolding/templating; most wiring is handled inside Core.
57
+ - Composable: opt into only what you need (`FastAPI`, `Rabbit`, `MongoDB`, `Redis`, `OpenAI`).
58
+ - Pydantic-first: type-safe models, validators, and clear input/output contracts.
59
+ - Multi-tenant by design: tenant context flows through DI and storage.
60
+ - AuthN/Z via DI: OIDC token introspection and pluggable PDP (ABAC/RBAC/ReBAC) hooks.
61
+ - Event-driven ready: publish/subscribe with routing keys and health.
62
+ - Observability-native: metrics, traces, logs from day one.
63
+ - Self-hostable: production parity with a cloud/aaS setup.
64
+
65
+ ---
66
+
67
+ ## Integrated Services (the platform)
68
+
69
+ Core plugs into a family of self-hostable services:
70
+
71
+ - IAM → OIDC/SSO, authN/Z, RBAC/ABAC/ReBAC.
72
+ - Notify → realtime notifications, Pusher-compatible API.
73
+ - Pulse → user activity + event tracking with OpenTelemetry hooks.
74
+ - File → object storage on MinIO (S3-compatible).
75
+ - Finance, Subscription, SMS/Email, Meet, Persona → optional services you can wire in.
76
+
77
+ Each service is:
78
+ - self-hostable (Docker Compose or Helm),
79
+ - BaaS-available,
80
+
81
+ ---
82
+
83
+ ## Quick start
84
+
85
+ ```bash
86
+ # Install core
87
+ poetry add core-bluprint -E FastAPI
88
+
89
+ # Scaffold a new service
90
+ launch init myservice --stack fastapi
91
+
92
+ # Run it
93
+ cd myservice
94
+ launch dev
95
+ ```
96
+
97
+ See pyproject extras for the full list.
98
+
99
+ ---
100
+
101
+ ## What you get out of the box
102
+
103
+ - App orchestrator (`core_bluprint.launcher`)
104
+ - Loads your routes, models, signals, and healthchecks
105
+ - Exposes settings and health endpoints (public toggle)
106
+ - FastAPI-native
107
+ - Dependency-injected request/tenant context and guards
108
+ - Clear routing, OpenAPI, and dependency injection patterns
109
+ - Auth & Access
110
+ - DI-based guards with OIDC token introspection
111
+ - Pluggable PDP for ABAC/RBAC/ReBAC decisions
112
+ - Multi-tenancy
113
+ - Tenant-aware DI context across web, DB, and messaging
114
+ - Automatic per-tenant settings endpoint backed by DB + cache
115
+ - Database layer (MongoDB via Beanie)
116
+ - Created/updated mixins, pagination utilities, typed helpers
117
+ - Helper classes/methods for common patterns (queries, projections, pagination)
118
+ - Auto model discovery for DB init
119
+ - Signals / Messaging (Rabbit via FastStream)
120
+ - Event-driven publish/subscribe integration with DI and retries
121
+ - Subscriber wiring and healthchecks
122
+ - Observability
123
+ - OpenTelemetry distro + OTLP exporter, Logfire, Sentry (error/bug tracking)
124
+ - I18N
125
+ - Exception handler and template utils with Babel/Jinja2
126
+ - Healthchecks
127
+ - Automatic app/DB/messaging checks + system routes
128
+ - Pydantic-native schemas and validators
129
+ - SchemaIn/Out validation for request/response contracts
130
+ - Common types and validators (`core_bluprint.types`)
131
+
132
+ Dive deeper in the docs below.
133
+
134
+ ---
135
+
136
+ ## Documentation
137
+
138
+ - Auth → docs/auth.md
139
+ - Tenant → docs/tenant.md
140
+ - DB (Mongo/Beanie) → docs/db.md
141
+ - Signals (Rabbit) → docs/signals.md
142
+ - Observability → docs/observability.md
143
+ - File storage → docs/file.md
144
+ - I18N → docs/i18n.md
145
+ - Settings & Configs → docs/settings.md
146
+ - Launcher & App model → docs/launcher.md
147
+
148
+ ---
149
+
150
+ ## Roadmap
151
+
152
+ - More CLI scaffolds and blueprints.
153
+ - Automatic `pydantic ai` agentic tool creation from apis
154
+ - Migrate PDP to [`OPAL`](https://github.com/permitio/opal) [opa](https://github.com/open-policy-agent/opa) based
155
+
@@ -0,0 +1,109 @@
1
+ # Fastloom – The Open Foundation for Building Event-Driven Services
2
+
3
+ Fastloom is a lightweight, batteries-included foundation for building modern backends. Define your settings, schemas, and endpoints; Fastloom wires up the rest: FastAPI, Mongo (Beanie), Rabbit (FastStream), metrics/traces/logs/errors, and more.
4
+
5
+ Think of it as the glue for your stack: web, messaging, caching, DB, observability, and integrations with best-in-class tools.
6
+
7
+ ---
8
+
9
+ ## Why FastLoom
10
+
11
+ - No boilerplate: minimal scaffolding/templating; most wiring is handled inside Core.
12
+ - Composable: opt into only what you need (`FastAPI`, `Rabbit`, `MongoDB`, `Redis`, `OpenAI`).
13
+ - Pydantic-first: type-safe models, validators, and clear input/output contracts.
14
+ - Multi-tenant by design: tenant context flows through DI and storage.
15
+ - AuthN/Z via DI: OIDC token introspection and pluggable PDP (ABAC/RBAC/ReBAC) hooks.
16
+ - Event-driven ready: publish/subscribe with routing keys and health.
17
+ - Observability-native: metrics, traces, logs from day one.
18
+ - Self-hostable: production parity with a cloud/aaS setup.
19
+
20
+ ---
21
+
22
+ ## Integrated Services (the platform)
23
+
24
+ Core plugs into a family of self-hostable services:
25
+
26
+ - IAM → OIDC/SSO, authN/Z, RBAC/ABAC/ReBAC.
27
+ - Notify → realtime notifications, Pusher-compatible API.
28
+ - Pulse → user activity + event tracking with OpenTelemetry hooks.
29
+ - File → object storage on MinIO (S3-compatible).
30
+ - Finance, Subscription, SMS/Email, Meet, Persona → optional services you can wire in.
31
+
32
+ Each service is:
33
+ - self-hostable (Docker Compose or Helm),
34
+ - BaaS-available,
35
+
36
+ ---
37
+
38
+ ## Quick start
39
+
40
+ ```bash
41
+ # Install core
42
+ poetry add core-bluprint -E FastAPI
43
+
44
+ # Scaffold a new service
45
+ launch init myservice --stack fastapi
46
+
47
+ # Run it
48
+ cd myservice
49
+ launch dev
50
+ ```
51
+
52
+ See pyproject extras for the full list.
53
+
54
+ ---
55
+
56
+ ## What you get out of the box
57
+
58
+ - App orchestrator (`core_bluprint.launcher`)
59
+ - Loads your routes, models, signals, and healthchecks
60
+ - Exposes settings and health endpoints (public toggle)
61
+ - FastAPI-native
62
+ - Dependency-injected request/tenant context and guards
63
+ - Clear routing, OpenAPI, and dependency injection patterns
64
+ - Auth & Access
65
+ - DI-based guards with OIDC token introspection
66
+ - Pluggable PDP for ABAC/RBAC/ReBAC decisions
67
+ - Multi-tenancy
68
+ - Tenant-aware DI context across web, DB, and messaging
69
+ - Automatic per-tenant settings endpoint backed by DB + cache
70
+ - Database layer (MongoDB via Beanie)
71
+ - Created/updated mixins, pagination utilities, typed helpers
72
+ - Helper classes/methods for common patterns (queries, projections, pagination)
73
+ - Auto model discovery for DB init
74
+ - Signals / Messaging (Rabbit via FastStream)
75
+ - Event-driven publish/subscribe integration with DI and retries
76
+ - Subscriber wiring and healthchecks
77
+ - Observability
78
+ - OpenTelemetry distro + OTLP exporter, Logfire, Sentry (error/bug tracking)
79
+ - I18N
80
+ - Exception handler and template utils with Babel/Jinja2
81
+ - Healthchecks
82
+ - Automatic app/DB/messaging checks + system routes
83
+ - Pydantic-native schemas and validators
84
+ - SchemaIn/Out validation for request/response contracts
85
+ - Common types and validators (`core_bluprint.types`)
86
+
87
+ Dive deeper in the docs below.
88
+
89
+ ---
90
+
91
+ ## Documentation
92
+
93
+ - Auth → docs/auth.md
94
+ - Tenant → docs/tenant.md
95
+ - DB (Mongo/Beanie) → docs/db.md
96
+ - Signals (Rabbit) → docs/signals.md
97
+ - Observability → docs/observability.md
98
+ - File storage → docs/file.md
99
+ - I18N → docs/i18n.md
100
+ - Settings & Configs → docs/settings.md
101
+ - Launcher & App model → docs/launcher.md
102
+
103
+ ---
104
+
105
+ ## Roadmap
106
+
107
+ - More CLI scaffolds and blueprints.
108
+ - Automatic `pydantic ai` agentic tool creation from apis
109
+ - Migrate PDP to [`OPAL`](https://github.com/permitio/opal) [opa](https://github.com/open-policy-agent/opa) based
File without changes
@@ -0,0 +1,5 @@
1
+ from contextvars import ContextVar
2
+
3
+ from fastloom.auth.schemas import UserClaims
4
+
5
+ Claims: ContextVar[UserClaims] = ContextVar("claims")
@@ -0,0 +1,145 @@
1
+ from collections.abc import Callable, Coroutine
2
+ from typing import Annotated, Any
3
+
4
+ import httpx
5
+ from fastapi import Depends, HTTPException, Request
6
+ from fastapi.security import OAuth2, OpenIdConnect
7
+ from jose.jwt import get_unverified_claims
8
+
9
+ from fastloom.auth import Claims
10
+ from fastloom.auth.schemas import (
11
+ IntrospectionResponse,
12
+ UserClaims,
13
+ )
14
+ from fastloom.settings.base import IAMSettings
15
+
16
+
17
+ class OptionalJWTAuth:
18
+ settings: IAMSettings
19
+ _security_scheme: OAuth2 | OpenIdConnect | None = None
20
+
21
+ def __init__(self, settings: IAMSettings):
22
+ self.settings = settings
23
+
24
+ if self.settings.oidc_enabled:
25
+ assert self.settings.OIDC_URL is not None
26
+ self._security_scheme = OpenIdConnect(
27
+ openIdConnectUrl=self.settings.OIDC_URL,
28
+ scheme_name="OIDC",
29
+ auto_error=False,
30
+ )
31
+ elif self.settings.oauth2_enabled:
32
+ self._security_scheme = OAuth2(
33
+ flows=self.settings.flows, auto_error=False
34
+ )
35
+
36
+ async def _introspect(
37
+ self, token: Annotated[str, Depends(_security_scheme)]
38
+ ):
39
+ async with httpx.AsyncClient() as client:
40
+ response: httpx.Response = await client.post(
41
+ f"{self.settings.IAM_SIDECAR_URL}/introspect",
42
+ json=dict(token=token),
43
+ )
44
+ if response.status_code != 200:
45
+ raise HTTPException(status_code=403, detail=response.text)
46
+ data = IntrospectionResponse.model_validate(response.json())
47
+ if not data.active:
48
+ raise HTTPException(status_code=403, detail="Inactive token")
49
+
50
+ def _transform_bearer(self, token: str) -> str:
51
+ if token.startswith("Bearer "):
52
+ return token.removeprefix("Bearer ").strip()
53
+ return token
54
+
55
+ async def _acl(
56
+ self,
57
+ request: Request,
58
+ token: Annotated[str, Depends(_security_scheme)],
59
+ ) -> None:
60
+ async with httpx.AsyncClient() as client:
61
+ response: httpx.Response = await client.post(
62
+ url=f"{self.settings.IAM_SIDECAR_URL}/acl",
63
+ json={
64
+ "token": token,
65
+ "endpoint": request.url.path,
66
+ "method": request.method,
67
+ },
68
+ )
69
+
70
+ if response.status_code != 200:
71
+ raise HTTPException(status_code=403, detail=response.text)
72
+ if not response.json():
73
+ raise HTTPException(status_code=403)
74
+
75
+ @classmethod
76
+ def _parse_token(cls, token: str) -> UserClaims:
77
+ return UserClaims.model_validate(get_unverified_claims(token))
78
+
79
+ async def _validate_token(
80
+ self, token: str, request: Request
81
+ ) -> UserClaims:
82
+ token = self._transform_bearer(token)
83
+ if self.settings.INTROSPECT:
84
+ await self._introspect(token)
85
+ if self.settings.ACL:
86
+ await self._acl(request, token)
87
+ claims = self._parse_token(token)
88
+ Claims.set(claims)
89
+ return claims
90
+
91
+ @property
92
+ def get_token(
93
+ self,
94
+ ) -> Callable[..., Coroutine[Any, Any, str | None]]:
95
+ async def _inner(
96
+ token: Annotated[str | None, Depends(self._security_scheme)],
97
+ ) -> str | None:
98
+ if token is None:
99
+ return None
100
+ return self._transform_bearer(token)
101
+
102
+ return _inner
103
+
104
+ @property
105
+ def get_claims(
106
+ self,
107
+ ) -> Callable[..., Coroutine[Any, Any, UserClaims | None]]:
108
+ async def _inner(
109
+ request: Request,
110
+ token: Annotated[str | None, Depends(self._security_scheme)],
111
+ ) -> UserClaims | None:
112
+ if token is None:
113
+ return None
114
+
115
+ return await self._validate_token(token, request)
116
+
117
+ return _inner
118
+
119
+
120
+ class JWTAuth(OptionalJWTAuth):
121
+ def __init__(self, settings: IAMSettings):
122
+ super().__init__(settings)
123
+ assert self._security_scheme is not None
124
+ self._security_scheme.auto_error = True
125
+
126
+ @property
127
+ def get_claims(self) -> Callable[..., Coroutine[Any, Any, UserClaims]]:
128
+ async def _inner(
129
+ request: Request,
130
+ token: Annotated[str, Depends(self._security_scheme)],
131
+ ) -> UserClaims:
132
+ return await self._validate_token(token, request)
133
+
134
+ return _inner
135
+
136
+ @property
137
+ def get_token(
138
+ self,
139
+ ) -> Callable[..., Coroutine[Any, Any, str]]:
140
+ async def _inner(
141
+ token: Annotated[str, Depends(self._security_scheme)],
142
+ ) -> str:
143
+ return self._transform_bearer(token)
144
+
145
+ return _inner
@@ -0,0 +1,69 @@
1
+ from fastapi.openapi.models import OAuthFlow, OAuthFlows
2
+ from pydantic import BaseModel, Field, HttpUrl, computed_field, field_validator
3
+
4
+ from fastloom.types import Str
5
+
6
+ ADMIN_ROLE = "ADMIN"
7
+
8
+
9
+ class OAuth2MergedScheme(OAuthFlow):
10
+ authorizationUrl: Str[HttpUrl] | None = None
11
+ tokenUrl: Str[HttpUrl] | None = None
12
+
13
+ @computed_field # type: ignore[prop-decorator]
14
+ @property
15
+ def flows(self) -> OAuthFlows:
16
+ if self.authorizationUrl is None and self.tokenUrl is None:
17
+ return OAuthFlows()
18
+ return OAuthFlows.model_validate(
19
+ dict(
20
+ authorizationCode=self.model_dump(
21
+ exclude_computed_fields=True
22
+ ),
23
+ )
24
+ )
25
+ # ^ implicit & ROPC are deprecated in OAUTH2.1
26
+
27
+ @computed_field # type: ignore[prop-decorator]
28
+ @property
29
+ def oauth2_enabled(self) -> bool:
30
+ return self.authorizationUrl is not None and self.tokenUrl is not None
31
+
32
+
33
+ class OIDCCScheme(BaseModel):
34
+ OIDC_URL: Str[HttpUrl] | None = None
35
+
36
+ @computed_field # type: ignore[misc]
37
+ @property
38
+ def oidc_enabled(self) -> bool:
39
+ return self.OIDC_URL is not None
40
+
41
+
42
+ class IntrospectionResponse(BaseModel):
43
+ active: bool
44
+
45
+
46
+ class Role(BaseModel):
47
+ name: str
48
+ users: list[str] | None = None
49
+
50
+
51
+ class UserClaims(BaseModel):
52
+ tenant: str = Field(alias="owner")
53
+ id: str
54
+ username: str = Field(..., validation_alias="name")
55
+ email: str | None = None
56
+ phone: str | None = None
57
+ roles: list[Role] = Field(default_factory=list)
58
+
59
+ @field_validator("roles", mode="before")
60
+ @classmethod
61
+ def validate_roles(cls, v: list[Role] | None) -> list[Role]:
62
+ if not v:
63
+ return []
64
+ return v
65
+
66
+ @computed_field # type: ignore[misc]
67
+ @property
68
+ def is_admin(self) -> bool:
69
+ return any(role.name == ADMIN_ROLE for role in self.roles or [])
File without changes
@@ -0,0 +1,33 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from aredis_om import Field, JsonModel
5
+ else:
6
+ try:
7
+ from aredis_om import Field, JsonModel
8
+ except ImportError:
9
+ from pydantic import BaseModel as JsonModel
10
+ from pydantic import Field
11
+
12
+
13
+ class BaseCache(JsonModel):
14
+ class Meta:
15
+ global_key_prefix = "cache"
16
+ model_key_prefix = "base"
17
+ # ^should be overriden in sub
18
+
19
+ @property
20
+ async def invalidate(self):
21
+ return await self.expire(0)
22
+
23
+
24
+ class BaseTenantSettingCache(BaseCache):
25
+ id: str = Field(primary_key=True)
26
+
27
+
28
+ class HostTenantMapping(BaseCache, index=True): # type: ignore[call-arg]
29
+ host: str = Field(primary_key=True)
30
+ tenant: str = Field(index=True)
31
+
32
+ class Meta:
33
+ model_key_prefix = "host_mapping"
@@ -0,0 +1,67 @@
1
+ from collections.abc import Awaitable
2
+ from functools import wraps
3
+ from os import getpid
4
+ from typing import Callable
5
+
6
+ from fastloom.cache.lifehooks import RedisHandler
7
+ from fastloom.settings.base import ProjectSettings
8
+ from fastloom.tenant.settings import ConfigAlias as Configs
9
+
10
+
11
+ class RedisGuardGate:
12
+ """
13
+ - *context manager*:
14
+ ```
15
+ async with RedisGuardGate("boostrap", ttl=30, grace=10) as acquired:
16
+ if acquired:
17
+ await lifespan_init()
18
+ ```
19
+ - *decorator*:
20
+ ```
21
+ @RedisGuardGate("boostrap", ttl=30)
22
+ async def lifespan_init():
23
+ ...
24
+ ```
25
+ """
26
+
27
+ key: str
28
+ ttl: int
29
+ grace: int
30
+ _acquired: bool = False
31
+
32
+ def __init__(self, key: str, ttl: int = 60, grace: int = 0):
33
+ self.key = (
34
+ f"{Configs[ProjectSettings].general.PROJECT_NAME}:{key}:leader" # type: ignore[misc]
35
+ )
36
+ self.ttl = ttl
37
+ self.grace = grace
38
+
39
+ def __call__[T, **P](self, func: Callable[P, Awaitable[T]]):
40
+ @wraps(func)
41
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | None:
42
+ async with self as acquired:
43
+ if acquired:
44
+ return await func(*args, **kwargs)
45
+ return None
46
+
47
+ return wrapper
48
+
49
+ async def _acquire(self):
50
+ acquired = await RedisHandler.redis.set(
51
+ self.key,
52
+ str(getpid()),
53
+ nx=True,
54
+ ex=self.ttl,
55
+ )
56
+ return acquired is not None
57
+
58
+ async def _release(self):
59
+ await RedisHandler.redis.expire(self.key, self.grace)
60
+
61
+ async def __aenter__(self) -> bool:
62
+ self._acquired = await self._acquire()
63
+ return self._acquired
64
+
65
+ async def __aexit__(self, exc_type, exc, tb):
66
+ if self._acquired:
67
+ await self._release()
@@ -0,0 +1,22 @@
1
+ from collections.abc import Callable, Coroutine
2
+ from functools import partial
3
+ from typing import Any
4
+
5
+
6
+ class RedisConnectionError(Exception): ...
7
+
8
+
9
+ async def check_redis_connection(redis_url: str) -> None:
10
+ from redis.asyncio import Redis
11
+
12
+ try:
13
+ client: Redis = Redis.from_url(redis_url)
14
+ await client.ping()
15
+ except Exception as er:
16
+ raise RedisConnectionError(f"Redis connection error: {er}") from er
17
+
18
+
19
+ def get_healthcheck(
20
+ redis_url: str,
21
+ ) -> Callable[[], Coroutine[Any, Any, None]]:
22
+ return partial(check_redis_connection, redis_url=redis_url)
@@ -0,0 +1,37 @@
1
+ from contextlib import suppress
2
+ from typing import TYPE_CHECKING
3
+
4
+ from fastloom.cache.settings import RedisSettings
5
+ from fastloom.meta import SelfSustaining
6
+
7
+ _HAS_REDIS = False
8
+ with suppress(ImportError):
9
+ from aredis_om import get_redis_connection
10
+ from redis import Redis as SyncRedis
11
+ from redis.asyncio import Redis
12
+ from redis.exceptions import ConnectionError
13
+
14
+ _HAS_REDIS = True
15
+
16
+ if TYPE_CHECKING:
17
+ from redis import Redis as SyncRedis
18
+ from redis.asyncio import Redis
19
+
20
+ if not TYPE_CHECKING and not _HAS_REDIS:
21
+ SyncRedis = None
22
+ Redis = None
23
+
24
+
25
+ class RedisHandler(SelfSustaining):
26
+ enabled: bool = False
27
+ redis: Redis
28
+ sync_redis: SyncRedis
29
+
30
+ def __init__(self, settings: RedisSettings) -> None:
31
+ super().__init__()
32
+ if not _HAS_REDIS:
33
+ return
34
+ self.redis = get_redis_connection(url=str(settings.REDIS_URL))
35
+ self.sync_redis = SyncRedis.from_url(url=str(settings.REDIS_URL))
36
+ with suppress(ConnectionError):
37
+ self.enabled = self.sync_redis.ping()
@@ -0,0 +1,13 @@
1
+ from pydantic import (
2
+ BaseModel,
3
+ Field,
4
+ RedisDsn,
5
+ )
6
+
7
+ from fastloom.types import Str
8
+
9
+
10
+ class RedisSettings(BaseModel):
11
+ REDIS_URL: Str[RedisDsn] = Field(
12
+ "redis://localhost:6379/0", validate_default=True
13
+ )
@@ -0,0 +1,20 @@
1
+ import secrets
2
+
3
+
4
+ def generate_token(length: int):
5
+ """
6
+ Generates a n-digits numeral token, that used for OTP
7
+ """
8
+ min_value = 10 ** (length - 1)
9
+ max_value = 10**length - 1
10
+ return str(secrets.randbelow(max_value - min_value + 1) + min_value).zfill(
11
+ length
12
+ )
13
+
14
+
15
+ def generate_alphanumeric_token(length: int):
16
+ """
17
+ Generates a n-digits alphanumeric token, that used for OTP
18
+ """
19
+ alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
20
+ return "".join(secrets.choice(alphabet) for _ in range(length))