svc-infra 0.1.183__tar.gz → 0.1.185__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.183 → svc_infra-0.1.185}/PKG-INFO +1 -1
- {svc_infra-0.1.183 → svc_infra-0.1.185}/pyproject.toml +1 -1
- svc_infra-0.1.185/src/svc_infra/api/fastapi/db/uniq.py +82 -0
- svc_infra-0.1.185/src/svc_infra/auth/user_default.py +81 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/uniq.py +9 -30
- svc_infra-0.1.183/src/svc_infra/auth/user_default.py +0 -53
- {svc_infra-0.1.183 → svc_infra-0.1.185}/README.md +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/README.md +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/add.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/crud_router.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/health.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/http.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/management.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/repository.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/resource.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/service.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/service_hooks.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/db/session.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/middleware/errors/error_handlers.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/settings.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/app/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/app/env.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/app/logging.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/app/root.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/app/settings.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/auth/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/auth/integration.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/auth/oauth_router.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/auth/providers.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/auth/settings.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/auth/users.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/cli/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/cli/cmds/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/cli/cmds/alembic_cmds.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/cli/cmds/help.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/cli/cmds/scaffold_cmds.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/cli/foundation/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/cli/foundation/runner.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/README.md +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/base.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/constants.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/core.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/scaffold.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/templates/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/templates/models_schemas/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/templates/models_schemas/auth/models.py.tmpl +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/templates/models_schemas/auth/schemas.py.tmpl +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/templates/models_schemas/entity/models.py.tmpl +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/templates/models_schemas/entity/schemas.py.tmpl +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/templates/setup/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/templates/setup/alembic.ini.tmpl +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/templates/setup/env_async.py.tmpl +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/templates/setup/env_sync.py.tmpl +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/templates/setup/script.py.mako.tmpl +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/db/utils.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/mcp/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/README.md +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/metrics/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/metrics/asgi.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/metrics/base.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/metrics/http.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/metrics/sqlalchemy.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/settings.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/templates/grafana_dashboard.json +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/templates/otel-collector.yaml +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/templates/prometheus_rules.yml +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/tracing/__init__.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/tracing/setup.py +0 -0
- {svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/py.typed +0 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Iterable, Optional, Dict, Any, Callable
|
4
|
+
from sqlalchemy import func, and_
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
6
|
+
|
7
|
+
from .repository import Repository
|
8
|
+
from svc_infra.db.uniq import ColumnSpec, _as_tuple
|
9
|
+
|
10
|
+
def make_uniqueness_hooks(
|
11
|
+
*,
|
12
|
+
model: type,
|
13
|
+
repo: Repository,
|
14
|
+
unique_cs: Iterable[ColumnSpec] = (),
|
15
|
+
unique_ci: Iterable[ColumnSpec] = (),
|
16
|
+
tenant_field: Optional[str] = None,
|
17
|
+
duplicate_message: str = "Duplicate resource violates uniqueness policy.",
|
18
|
+
) -> tuple[
|
19
|
+
Callable[[Dict[str, Any]], Any], # async pre_create(data) -> data
|
20
|
+
Callable[[Dict[str, Any]], Any], # async pre_update(data) -> data
|
21
|
+
]:
|
22
|
+
"""Return (pre_create, pre_update) hooks that check duplicates *before* insert/update.
|
23
|
+
|
24
|
+
Usage with ServiceWithHooks:
|
25
|
+
pre_c, pre_u = make_uniqueness_hooks(model=User, repo=repo, unique_ci=["email"], tenant_field="tenant_id")
|
26
|
+
svc = ServiceWithHooks(repo, pre_create=pre_c, pre_update=pre_u)
|
27
|
+
"""
|
28
|
+
cs_specs = [_as_tuple(s) for s in unique_cs]
|
29
|
+
ci_specs = [_as_tuple(s) for s in unique_ci]
|
30
|
+
|
31
|
+
def _build_wheres(data: Dict[str, Any]):
|
32
|
+
wheres = []
|
33
|
+
|
34
|
+
def col(name: str):
|
35
|
+
return getattr(model, name)
|
36
|
+
|
37
|
+
tenant_val = data.get(tenant_field) if tenant_field else None
|
38
|
+
|
39
|
+
# case-sensitive groups
|
40
|
+
for spec in cs_specs:
|
41
|
+
if not all(k in data for k in spec):
|
42
|
+
continue
|
43
|
+
parts = []
|
44
|
+
if tenant_field:
|
45
|
+
parts.append(col(tenant_field).is_(None) if tenant_val is None else col(tenant_field) == tenant_val)
|
46
|
+
for k in spec:
|
47
|
+
parts.append(col(k) == data[k])
|
48
|
+
wheres.append(and_(*parts))
|
49
|
+
|
50
|
+
# case-insensitive groups
|
51
|
+
for spec in ci_specs:
|
52
|
+
if not all(k in data for k in spec):
|
53
|
+
continue
|
54
|
+
parts = []
|
55
|
+
if tenant_field:
|
56
|
+
parts.append(col(tenant_field).is_(None) if tenant_val is None else col(tenant_field) == tenant_val)
|
57
|
+
for k in spec:
|
58
|
+
# compare lower(db_col) == lower(<value>) safely
|
59
|
+
parts.append(func.lower(col(k)) == func.lower(func.cast(data[k], col(k).type)))
|
60
|
+
wheres.append(and_(*parts))
|
61
|
+
|
62
|
+
return wheres
|
63
|
+
|
64
|
+
async def _deferred_check(session: AsyncSession, data: Dict[str, Any]):
|
65
|
+
wheres = _build_wheres(data)
|
66
|
+
for where in wheres:
|
67
|
+
if await repo.exists(session, where=[where]):
|
68
|
+
from fastapi import HTTPException
|
69
|
+
raise HTTPException(status_code=409, detail=duplicate_message)
|
70
|
+
|
71
|
+
async def _pre_create(data: Dict[str, Any]) -> Dict[str, Any]:
|
72
|
+
# store a coroutine to be awaited when a session is available in Service.create
|
73
|
+
data = dict(data)
|
74
|
+
data.setdefault("__uniq_check__", _deferred_check)
|
75
|
+
return data
|
76
|
+
|
77
|
+
async def _pre_update(data: Dict[str, Any]) -> Dict[str, Any]:
|
78
|
+
data = dict(data)
|
79
|
+
data.setdefault("__uniq_check__", _deferred_check)
|
80
|
+
return data
|
81
|
+
|
82
|
+
return _pre_create, _pre_update
|
@@ -0,0 +1,81 @@
|
|
1
|
+
from typing import Any, Dict
|
2
|
+
from fastapi import HTTPException
|
3
|
+
from fastapi_users.password import PasswordHelper
|
4
|
+
from sqlalchemy import func
|
5
|
+
|
6
|
+
from svc_infra.api.fastapi.db.service_hooks import ServiceWithHooks
|
7
|
+
from svc_infra.api.fastapi.db.repository import Repository
|
8
|
+
|
9
|
+
_pwd = PasswordHelper()
|
10
|
+
|
11
|
+
def make_default_user_service(repo: Repository):
|
12
|
+
Model = repo.model # <-- capture the actual mapped model from the repo
|
13
|
+
|
14
|
+
def _user_pre_create(data: Dict[str, Any]) -> Dict[str, Any]:
|
15
|
+
data = dict(data)
|
16
|
+
# normalize + map fields
|
17
|
+
if "password" in data:
|
18
|
+
data["password_hash"] = _pwd.hash(data.pop("password"))
|
19
|
+
if "metadata" in data:
|
20
|
+
data["extra"] = data.pop("metadata")
|
21
|
+
data.setdefault("roles", [])
|
22
|
+
|
23
|
+
# BEFORE insert: app-level guard (nice 409) aligned with DB uniqueness
|
24
|
+
email = data.get("email")
|
25
|
+
tenant_id = data.get("tenant_id")
|
26
|
+
if email is not None:
|
27
|
+
# case-insensitive email; scope by tenant (NULL => global)
|
28
|
+
where = [func.lower(Model.email) == func.lower(email)]
|
29
|
+
if hasattr(Model, "tenant_id"):
|
30
|
+
if tenant_id is None:
|
31
|
+
where.append(Model.tenant_id.is_(None))
|
32
|
+
else:
|
33
|
+
where.append(Model.tenant_id == tenant_id)
|
34
|
+
|
35
|
+
async def _exists(session):
|
36
|
+
return await repo.exists(session, where=where)
|
37
|
+
|
38
|
+
# stash the callback so Service.create can await it (has session)
|
39
|
+
data["_precreate_exists_check"] = _exists
|
40
|
+
|
41
|
+
return data
|
42
|
+
|
43
|
+
def _user_pre_update(data: Dict[str, Any]) -> Dict[str, Any]:
|
44
|
+
data = dict(data)
|
45
|
+
if "password" in data:
|
46
|
+
data["password_hash"] = _pwd.hash(data.pop("password"))
|
47
|
+
if "metadata" in data:
|
48
|
+
data["extra"] = data.pop("metadata")
|
49
|
+
|
50
|
+
# Optional: also protect email change from creating dupes
|
51
|
+
email = data.get("email")
|
52
|
+
tenant_id = data.get("tenant_id")
|
53
|
+
if email is not None:
|
54
|
+
where = [func.lower(Model.email) == func.lower(email)]
|
55
|
+
if hasattr(Model, "tenant_id"):
|
56
|
+
if tenant_id is None:
|
57
|
+
where.append(Model.tenant_id.is_(None))
|
58
|
+
else:
|
59
|
+
where.append(Model.tenant_id == tenant_id)
|
60
|
+
|
61
|
+
async def _exists(session):
|
62
|
+
return await repo.exists(session, where=where)
|
63
|
+
|
64
|
+
data["_precreate_exists_check"] = _exists # reuse same key
|
65
|
+
|
66
|
+
return data
|
67
|
+
|
68
|
+
class _Svc(ServiceWithHooks):
|
69
|
+
async def create(self, session, data):
|
70
|
+
exists_cb = data.pop("_precreate_exists_check", None)
|
71
|
+
if exists_cb and await exists_cb(session):
|
72
|
+
raise HTTPException(status_code=409, detail="User with this email already exists.")
|
73
|
+
return await super().create(session, data)
|
74
|
+
|
75
|
+
async def update(self, session, id_value, data):
|
76
|
+
exists_cb = data.pop("_precreate_exists_check", None)
|
77
|
+
if exists_cb and await exists_cb(session):
|
78
|
+
raise HTTPException(status_code=409, detail="User with this email already exists.")
|
79
|
+
return await super().update(session, id_value, data)
|
80
|
+
|
81
|
+
return _Svc(repo, pre_create=_user_pre_create, pre_update=_user_pre_update)
|
@@ -2,42 +2,28 @@ from __future__ import annotations
|
|
2
2
|
from typing import Iterable, Sequence, Tuple, Union, List, Optional
|
3
3
|
from sqlalchemy import Index, func
|
4
4
|
|
5
|
-
|
6
|
-
ColumnSpec = Union[str, Sequence[str]] # "email" or ("first_name","last_name")
|
7
|
-
|
5
|
+
ColumnSpec = Union[str, Sequence[str]]
|
8
6
|
|
9
7
|
def _as_tuple(spec: ColumnSpec) -> Tuple[str, ...]:
|
10
8
|
return (spec,) if isinstance(spec, str) else tuple(spec)
|
11
9
|
|
12
|
-
|
13
10
|
def make_unique_indexes(
|
14
11
|
model: type,
|
15
12
|
*,
|
16
|
-
# Case-sensitive uniqueness specs (exact match)
|
17
13
|
unique_cs: Iterable[ColumnSpec] = (),
|
18
|
-
# Case-insensitive uniqueness specs: lower() applied to string columns
|
19
14
|
unique_ci: Iterable[ColumnSpec] = (),
|
20
|
-
# Optional tenant scoping (e.g. "tenant_id"). If provided:
|
21
|
-
# - when tenant is NULL -> global uniqueness
|
22
|
-
# - when tenant is NOT NULL -> uniqueness within tenant
|
23
15
|
tenant_field: Optional[str] = None,
|
24
|
-
# Prefix for index names
|
25
16
|
name_prefix: str = "uq",
|
26
17
|
) -> List[Index]:
|
27
|
-
"""
|
28
|
-
|
18
|
+
"""Return SQLAlchemy Index objects that enforce uniqueness.
|
19
|
+
|
20
|
+
- unique_cs: case-sensitive unique specs
|
21
|
+
- unique_ci: case-insensitive unique specs (lower(column))
|
22
|
+
- tenant_field: if provided, create two partial unique indexes:
|
23
|
+
* tenant IS NULL (global bucket)
|
24
|
+
* tenant IS NOT NULL (scoped per-tenant)
|
29
25
|
|
30
|
-
|
31
|
-
- For case-insensitive specs, we use functional unique indexes with lower(col).
|
32
|
-
- If tenant_field is given and the tenant column is nullable, we generate two partial
|
33
|
-
unique indexes: one for tenant IS NULL (global), one for tenant IS NOT NULL (scoped).
|
34
|
-
- Attach these indexes after your model class definition, before migrations/DDL run:
|
35
|
-
Indexes = make_unique_indexes(User, unique_ci=["email"], tenant_field="tenant_id")
|
36
|
-
for ix in Indexes:
|
37
|
-
ix.create(model.metadata.bind) # OR just rely on Alembic autogenerate / metadata.create_all
|
38
|
-
Most apps just *define* them at module import time:
|
39
|
-
for ix in Indexes: # they auto-register with the Table; no manual create() needed
|
40
|
-
pass
|
26
|
+
Declare right after your model class; Alembic or metadata.create_all will pick them up.
|
41
27
|
"""
|
42
28
|
idxs: List[Index] = []
|
43
29
|
|
@@ -53,7 +39,6 @@ def make_unique_indexes(
|
|
53
39
|
|
54
40
|
tenant_col = _col(tenant_field) if tenant_field else None
|
55
41
|
|
56
|
-
# Helper: name like uq_<table>_<tenant?>_<ci/cs>_<joined-columns>
|
57
42
|
def _name(ci: bool, spec: Tuple[str, ...], null_bucket: Optional[str] = None):
|
58
43
|
parts = [name_prefix, model.__tablename__]
|
59
44
|
if tenant_field:
|
@@ -64,18 +49,14 @@ def make_unique_indexes(
|
|
64
49
|
parts.extend(spec)
|
65
50
|
return "_".join(parts)
|
66
51
|
|
67
|
-
# Build indexes for both CS and CI specs
|
68
52
|
for ci, spec_list in ((False, unique_cs), (True, unique_ci)):
|
69
53
|
for spec in spec_list:
|
70
54
|
spec_t = _as_tuple(spec)
|
71
55
|
cols = _to_sa_cols(spec_t, ci=ci)
|
72
56
|
|
73
57
|
if tenant_col is None:
|
74
|
-
# simple global unique (with or without lower())
|
75
58
|
idxs.append(Index(_name(ci, spec_t), *cols, unique=True))
|
76
59
|
else:
|
77
|
-
# two partial unique indexes to treat NULL tenant as its own bucket
|
78
|
-
# 1) global bucket (tenant IS NULL)
|
79
60
|
idxs.append(
|
80
61
|
Index(
|
81
62
|
_name(ci, spec_t, "null"),
|
@@ -84,7 +65,6 @@ def make_unique_indexes(
|
|
84
65
|
postgresql_where=tenant_col.is_(None),
|
85
66
|
)
|
86
67
|
)
|
87
|
-
# 2) per-tenant bucket (tenant IS NOT NULL)
|
88
68
|
idxs.append(
|
89
69
|
Index(
|
90
70
|
_name(ci, spec_t, "notnull"),
|
@@ -94,5 +74,4 @@ def make_unique_indexes(
|
|
94
74
|
postgresql_where=tenant_col.isnot(None),
|
95
75
|
)
|
96
76
|
)
|
97
|
-
|
98
77
|
return idxs
|
@@ -1,53 +0,0 @@
|
|
1
|
-
from typing import Any, Dict
|
2
|
-
from fastapi import HTTPException
|
3
|
-
from fastapi_users.password import PasswordHelper
|
4
|
-
from sqlalchemy import func
|
5
|
-
|
6
|
-
from svc_infra.api.fastapi.db.service_hooks import ServiceWithHooks
|
7
|
-
from svc_infra.api.fastapi.db.repository import Repository
|
8
|
-
|
9
|
-
_pwd = PasswordHelper()
|
10
|
-
|
11
|
-
def _user_pre_create(data: Dict[str, Any]) -> Dict[str, Any]:
|
12
|
-
data = dict(data)
|
13
|
-
# normalize + map fields
|
14
|
-
if "password" in data:
|
15
|
-
data["password_hash"] = _pwd.hash(data.pop("password"))
|
16
|
-
if "metadata" in data:
|
17
|
-
data["extra"] = data.pop("metadata")
|
18
|
-
|
19
|
-
# BEFORE insert: application-level guard (nice error)
|
20
|
-
# case-insensitive email; handle tenant/null logic the same way as your unique index
|
21
|
-
email = data.get("email")
|
22
|
-
tenant_id = data.get("tenant_id")
|
23
|
-
if email:
|
24
|
-
where = [func.lower(Repository.model.email) == func.lower(email)]
|
25
|
-
if tenant_id is None:
|
26
|
-
where.append(Repository.model.tenant_id.is_(None))
|
27
|
-
else:
|
28
|
-
where.append(Repository.model.tenant_id == tenant_id)
|
29
|
-
|
30
|
-
# use a small “exists” helper on the repo if you have it
|
31
|
-
async def _exists(session):
|
32
|
-
return await Repository.exists(session, where=where)
|
33
|
-
|
34
|
-
data["_precreate_exists_check"] = _exists # stash callable for service.create to run
|
35
|
-
|
36
|
-
return data
|
37
|
-
|
38
|
-
def _user_pre_update(data: Dict[str, Any]) -> Dict[str, Any]:
|
39
|
-
data = dict(data)
|
40
|
-
if "password" in data:
|
41
|
-
data["password_hash"] = _pwd.hash(data.pop("password"))
|
42
|
-
if "metadata" in data:
|
43
|
-
data["extra"] = data.pop("metadata")
|
44
|
-
return data
|
45
|
-
|
46
|
-
def make_default_user_service(repo: Repository):
|
47
|
-
class _Svc(ServiceWithHooks):
|
48
|
-
async def create(self, session, data):
|
49
|
-
exists_cb = data.pop("_precreate_exists_check", None)
|
50
|
-
if exists_cb and await exists_cb(session):
|
51
|
-
raise HTTPException(status_code=409, detail="User with this email already exists.")
|
52
|
-
return await super().create(session, data)
|
53
|
-
return _Svc(repo, pre_create=_user_pre_create, pre_update=_user_pre_update)
|
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.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/middleware/errors/__init__.py
RENAMED
File without changes
|
{svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/api/fastapi/middleware/errors/catchall.py
RENAMED
File without changes
|
File without changes
|
{svc_infra-0.1.183 → svc_infra-0.1.185}/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
|
File without changes
|
{svc_infra-0.1.183 → svc_infra-0.1.185}/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
|
File without changes
|
File without changes
|
{svc_infra-0.1.183 → svc_infra-0.1.185}/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.183 → svc_infra-0.1.185}/src/svc_infra/observability/templates/grafana_dashboard.json
RENAMED
File without changes
|
{svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/templates/otel-collector.yaml
RENAMED
File without changes
|
{svc_infra-0.1.183 → svc_infra-0.1.185}/src/svc_infra/observability/templates/prometheus_rules.yml
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|