svc-infra 0.1.175__tar.gz → 0.1.177__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.
- {svc_infra-0.1.175 → svc_infra-0.1.177}/PKG-INFO +1 -1
- {svc_infra-0.1.175 → svc_infra-0.1.177}/pyproject.toml +1 -1
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/add.py +18 -1
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/repository.py +9 -3
- svc_infra-0.1.177/src/svc_infra/api/fastapi/db/resource.py +30 -0
- svc_infra-0.1.177/src/svc_infra/api/fastapi/db/service_hooks.py +17 -0
- svc_infra-0.1.177/src/svc_infra/auth/__init__.py +7 -0
- svc_infra-0.1.177/src/svc_infra/auth/user_default.py +27 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/models_schemas/auth/models.py.tmpl +9 -1
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/models_schemas/auth/schemas.py.tmpl +1 -0
- svc_infra-0.1.175/src/svc_infra/api/fastapi/db/resource.py +0 -31
- svc_infra-0.1.175/src/svc_infra/auth/__init__.py +0 -3
- {svc_infra-0.1.175 → svc_infra-0.1.177}/README.md +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/README.md +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/crud_router.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/health.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/http.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/management.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/service.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/session.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/errors/error_handlers.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/settings.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/app/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/app/env.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/app/logging.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/app/root.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/app/settings.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/auth/integration.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/auth/oauth_router.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/auth/providers.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/auth/settings.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/auth/users.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/cmds/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/cmds/alembic_cmds.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/cmds/help.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/cmds/scaffold_cmds.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/foundation/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/foundation/runner.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/README.md +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/base.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/constants.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/core.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/scaffold.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/models_schemas/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/models_schemas/entity/models.py.tmpl +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/models_schemas/entity/schemas.py.tmpl +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/setup/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/setup/alembic.ini.tmpl +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/setup/env_async.py.tmpl +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/setup/env_sync.py.tmpl +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/setup/script.py.mako.tmpl +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/utils.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/mcp/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/README.md +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/metrics/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/metrics/asgi.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/metrics/base.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/metrics/http.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/metrics/sqlalchemy.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/settings.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/templates/grafana_dashboard.json +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/templates/otel-collector.yaml +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/templates/prometheus_rules.yml +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/tracing/__init__.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/tracing/setup.py +0 -0
- {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/py.typed +0 -0
@@ -4,6 +4,7 @@ from typing import Optional, Sequence
|
|
4
4
|
from contextlib import asynccontextmanager
|
5
5
|
from fastapi import FastAPI
|
6
6
|
|
7
|
+
from svc_infra.auth.user_default import make_default_user_service
|
7
8
|
from .repository import Repository
|
8
9
|
from .service import Service
|
9
10
|
from .crud_router import make_crud_router_plus
|
@@ -12,10 +13,25 @@ from .resource import Resource
|
|
12
13
|
from .session import initialize_session, dispose_session
|
13
14
|
from .health import _make_db_health_router
|
14
15
|
|
16
|
+
def _looks_like_user_model(model: type) -> bool:
|
17
|
+
# minimal/robust: users have email + password_hash.
|
18
|
+
# (You can tighten this with hasattr(model, "extra") / "roles" if you want.)
|
19
|
+
return hasattr(model, "email") and hasattr(model, "password_hash")
|
20
|
+
|
15
21
|
def add_resources(app: FastAPI, resources: Sequence[Resource]) -> None:
|
16
22
|
for r in resources:
|
17
23
|
repo = Repository(model=r.model, id_attr=r.id_attr, soft_delete=r.soft_delete)
|
18
|
-
|
24
|
+
|
25
|
+
# 1) explicit app-provided factory wins
|
26
|
+
if r.service_factory:
|
27
|
+
svc = r.service_factory(repo)
|
28
|
+
# 2) otherwise, auto-apply our default user service if model looks like a user
|
29
|
+
elif _looks_like_user_model(r.model):
|
30
|
+
svc = make_default_user_service(repo)
|
31
|
+
# 3) else, generic service
|
32
|
+
else:
|
33
|
+
svc = Service(repo)
|
34
|
+
|
19
35
|
if r.read_schema and r.create_schema and r.update_schema:
|
20
36
|
Read, Create, Update = r.read_schema, r.create_schema, r.update_schema
|
21
37
|
else:
|
@@ -26,6 +42,7 @@ def add_resources(app: FastAPI, resources: Sequence[Resource]) -> None:
|
|
26
42
|
create_name=r.create_name,
|
27
43
|
update_name=r.update_name,
|
28
44
|
)
|
45
|
+
|
29
46
|
router = make_crud_router_plus(
|
30
47
|
model=r.model,
|
31
48
|
service=svc,
|
@@ -5,7 +5,7 @@ from typing import Any, Optional, Sequence, Iterable, Set
|
|
5
5
|
from sqlalchemy import Select, and_, func, select, or_, String
|
6
6
|
from sqlalchemy.ext.asyncio import AsyncSession
|
7
7
|
from sqlalchemy.orm import InstrumentedAttribute
|
8
|
-
|
8
|
+
from sqlalchemy.orm import class_mapper
|
9
9
|
|
10
10
|
class Repository:
|
11
11
|
"""
|
@@ -29,6 +29,9 @@ class Repository:
|
|
29
29
|
self.soft_delete_flag_field = soft_delete_flag_field
|
30
30
|
self.immutable_fields: Set[str] = set(immutable_fields or {"id", "created_at", "updated_at"})
|
31
31
|
|
32
|
+
def _model_columns(self) -> set[str]:
|
33
|
+
return {c.key for c in class_mapper(self.model).columns}
|
34
|
+
|
32
35
|
def _id_column(self) -> InstrumentedAttribute:
|
33
36
|
return getattr(self.model, self.id_attr)
|
34
37
|
|
@@ -61,7 +64,9 @@ class Repository:
|
|
61
64
|
return (await session.execute(stmt)).scalars().first()
|
62
65
|
|
63
66
|
async def create(self, session: AsyncSession, data: dict[str, Any]) -> Any:
|
64
|
-
|
67
|
+
valid = self._model_columns()
|
68
|
+
filtered = {k: v for k, v in data.items() if k in valid}
|
69
|
+
obj = self.model(**filtered)
|
65
70
|
session.add(obj)
|
66
71
|
await session.flush()
|
67
72
|
return obj
|
@@ -70,8 +75,9 @@ class Repository:
|
|
70
75
|
obj = await self.get(session, id_value)
|
71
76
|
if not obj:
|
72
77
|
return None
|
78
|
+
valid = self._model_columns()
|
73
79
|
for k, v in data.items():
|
74
|
-
if k not in self.immutable_fields:
|
80
|
+
if k in valid and k not in self.immutable_fields:
|
75
81
|
setattr(obj, k, v)
|
76
82
|
await session.flush()
|
77
83
|
return obj
|
@@ -0,0 +1,30 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Any, Optional, Type, Callable
|
3
|
+
|
4
|
+
from .service import Service
|
5
|
+
from .repository import Repository
|
6
|
+
|
7
|
+
@dataclass
|
8
|
+
class Resource:
|
9
|
+
model: type[object]
|
10
|
+
prefix: str
|
11
|
+
tags: Optional[list[str]] = None
|
12
|
+
|
13
|
+
id_attr: str = "id"
|
14
|
+
soft_delete: bool = False
|
15
|
+
search_fields: Optional[list[str]] = None
|
16
|
+
ordering_default: Optional[str] = None
|
17
|
+
allowed_order_fields: Optional[list[str]] = None
|
18
|
+
|
19
|
+
read_schema: Optional[Type[Any]] = None
|
20
|
+
create_schema: Optional[Type[Any]] = None
|
21
|
+
update_schema: Optional[Type[Any]] = None
|
22
|
+
|
23
|
+
read_name: Optional[str] = None
|
24
|
+
create_name: Optional[str] = None
|
25
|
+
update_name: Optional[str] = None
|
26
|
+
|
27
|
+
create_exclude: tuple[str, ...] = ("id",)
|
28
|
+
|
29
|
+
# NEW – optional hook to build a custom Service
|
30
|
+
service_factory: Optional[Callable[[Repository], Service]] = None
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from typing import Any, Callable, Optional
|
2
|
+
|
3
|
+
from .service import Service
|
4
|
+
|
5
|
+
PreHook = Callable[[dict[str, Any]], dict[str, Any]]
|
6
|
+
|
7
|
+
class ServiceWithHooks(Service):
|
8
|
+
def __init__(self, repo, pre_create: Optional[PreHook] = None, pre_update: Optional[PreHook] = None):
|
9
|
+
super().__init__(repo)
|
10
|
+
self._pre_create = pre_create
|
11
|
+
self._pre_update = pre_update
|
12
|
+
|
13
|
+
async def pre_create(self, data: dict[str, Any]) -> dict[str, Any]:
|
14
|
+
return self._pre_create(data) if self._pre_create else data
|
15
|
+
|
16
|
+
async def pre_update(self, data: dict[str, Any]) -> dict[str, Any]:
|
17
|
+
return self._pre_update(data) if self._pre_update else data
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from typing import Any, Dict
|
2
|
+
from fastapi_users.password import PasswordHelper
|
3
|
+
|
4
|
+
from svc_infra.api.fastapi.db.service_hooks import ServiceWithHooks
|
5
|
+
from svc_infra.api.fastapi.db.repository import Repository
|
6
|
+
|
7
|
+
_pwd = PasswordHelper()
|
8
|
+
|
9
|
+
def _pre_create(data: Dict[str, Any]) -> Dict[str, Any]:
|
10
|
+
data = dict(data)
|
11
|
+
if "password" in data:
|
12
|
+
data["password_hash"] = _pwd.hash(data.pop("password"))
|
13
|
+
if "metadata" in data: # pydantic alias -> model column "metadata" (extra)
|
14
|
+
data["extra"] = data.pop("metadata")
|
15
|
+
data.setdefault("roles", [])
|
16
|
+
return data
|
17
|
+
|
18
|
+
def _pre_update(data: Dict[str, Any]) -> Dict[str, Any]:
|
19
|
+
data = dict(data)
|
20
|
+
if "password" in data:
|
21
|
+
data["password_hash"] = _pwd.hash(data.pop("password"))
|
22
|
+
if "metadata" in data:
|
23
|
+
data["extra"] = data.pop("metadata")
|
24
|
+
return data
|
25
|
+
|
26
|
+
def make_default_user_service(repo: Repository):
|
27
|
+
return ServiceWithHooks(repo, pre_create=_pre_create, pre_update=_pre_update)
|
@@ -23,8 +23,16 @@ class User(ModelBase):
|
|
23
23
|
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
24
24
|
is_verified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
25
25
|
|
26
|
-
# auth state
|
27
26
|
password_hash: Mapped[str] = mapped_column(String(512), nullable=False)
|
27
|
+
|
28
|
+
@property
|
29
|
+
def password(self) -> str:
|
30
|
+
raise AttributeError("password is write-only")
|
31
|
+
|
32
|
+
@password.setter
|
33
|
+
def password(self, raw: str) -> None:
|
34
|
+
self.password_hash = _pwd.hash(raw)
|
35
|
+
|
28
36
|
last_login: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
29
37
|
disabled_reason: Mapped[Optional[str]] = mapped_column(Text)
|
30
38
|
|
@@ -35,6 +35,7 @@ class UserCreate(BaseModel):
|
|
35
35
|
class UserUpdate(BaseModel):
|
36
36
|
email: Optional[EmailStr] = None
|
37
37
|
full_name: Optional[str] = None
|
38
|
+
password: Optional[str] = None
|
38
39
|
is_active: Optional[bool] = None
|
39
40
|
is_superuser: Optional[bool] = None
|
40
41
|
is_verified: Optional[bool] = None
|
@@ -1,31 +0,0 @@
|
|
1
|
-
from typing import Any, Optional, Type
|
2
|
-
from dataclasses import dataclass
|
3
|
-
|
4
|
-
@dataclass
|
5
|
-
class Resource:
|
6
|
-
# Required
|
7
|
-
model: type[object]
|
8
|
-
prefix: str # e.g. "/projects"
|
9
|
-
|
10
|
-
# Optional FastAPI presentation
|
11
|
-
tags: Optional[list[str]] = None
|
12
|
-
|
13
|
-
# Optional overrides / knobs
|
14
|
-
id_attr: str = "id"
|
15
|
-
soft_delete: bool = False # enables soft-delete endpoints if your model has deleted_at
|
16
|
-
search_fields: Optional[list[str]] = None # used by router_plus if implemented there
|
17
|
-
ordering_default: Optional[str] = None
|
18
|
-
allowed_order_fields: Optional[list[str]] = None # expose to router
|
19
|
-
|
20
|
-
# If you already have Pydantic classes, pass them here and we won't autogen
|
21
|
-
read_schema: Optional[Type[Any]] = None
|
22
|
-
create_schema: Optional[Type[Any]] = None
|
23
|
-
update_schema: Optional[Type[Any]] = None
|
24
|
-
|
25
|
-
# Autogen schema names (only used when the three above are None)
|
26
|
-
read_name: Optional[str] = None
|
27
|
-
create_name: Optional[str] = None
|
28
|
-
update_name: Optional[str] = None
|
29
|
-
|
30
|
-
# When autogenerating Create, exclude these model columns (e.g. "id", server defaults)
|
31
|
-
create_exclude: tuple[str, ...] = ("id",)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/errors/__init__.py
RENAMED
File without changes
|
{svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/errors/catchall.py
RENAMED
File without changes
|
File without changes
|
{svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/models_schemas/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/setup/script.py.mako.tmpl
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/templates/grafana_dashboard.json
RENAMED
File without changes
|
{svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/templates/otel-collector.yaml
RENAMED
File without changes
|
{svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/templates/prometheus_rules.yml
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|