svc-infra 0.1.181__tar.gz → 0.1.183__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.181 → svc_infra-0.1.183}/PKG-INFO +1 -1
- {svc_infra-0.1.181 → svc_infra-0.1.183}/pyproject.toml +1 -1
- svc_infra-0.1.183/src/svc_infra/auth/user_default.py +53 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/templates/models_schemas/auth/models.py.tmpl +17 -9
- svc_infra-0.1.183/src/svc_infra/db/templates/models_schemas/entity/models.py.tmpl +52 -0
- svc_infra-0.1.183/src/svc_infra/db/uniq.py +98 -0
- svc_infra-0.1.181/src/svc_infra/auth/user_default.py +0 -27
- svc_infra-0.1.181/src/svc_infra/db/templates/models_schemas/entity/models.py.tmpl +0 -29
- {svc_infra-0.1.181 → svc_infra-0.1.183}/README.md +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/README.md +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/add.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/crud_router.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/health.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/http.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/management.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/repository.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/resource.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/service.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/service_hooks.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/db/session.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/middleware/errors/error_handlers.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/settings.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/app/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/app/env.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/app/logging.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/app/root.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/app/settings.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/auth/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/auth/integration.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/auth/oauth_router.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/auth/providers.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/auth/settings.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/auth/users.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/cli/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/cli/cmds/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/cli/cmds/alembic_cmds.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/cli/cmds/help.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/cli/cmds/scaffold_cmds.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/cli/foundation/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/cli/foundation/runner.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/README.md +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/base.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/constants.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/core.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/scaffold.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/templates/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/templates/models_schemas/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/templates/models_schemas/auth/schemas.py.tmpl +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/templates/models_schemas/entity/schemas.py.tmpl +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/templates/setup/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/templates/setup/alembic.ini.tmpl +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/templates/setup/env_async.py.tmpl +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/templates/setup/env_sync.py.tmpl +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/templates/setup/script.py.mako.tmpl +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/db/utils.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/mcp/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/README.md +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/metrics/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/metrics/asgi.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/metrics/base.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/metrics/http.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/metrics/sqlalchemy.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/settings.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/templates/grafana_dashboard.json +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/templates/otel-collector.yaml +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/templates/prometheus_rules.yml +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/tracing/__init__.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/tracing/setup.py +0 -0
- {svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/py.typed +0 -0
@@ -0,0 +1,53 @@
|
|
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)
|
@@ -4,14 +4,16 @@ from typing import Optional
|
|
4
4
|
import uuid
|
5
5
|
|
6
6
|
from sqlalchemy import (
|
7
|
-
String, Boolean, DateTime, JSON, Text, func,
|
7
|
+
String, Boolean, DateTime, JSON, Text, func, Index
|
8
8
|
)
|
9
9
|
from sqlalchemy.dialects.postgresql import UUID
|
10
10
|
from sqlalchemy.orm import Mapped, mapped_column
|
11
|
-
from sqlalchemy.ext.mutable import MutableDict, MutableList
|
11
|
+
from sqlalchemy.ext.mutable import MutableDict, MutableList
|
12
12
|
|
13
13
|
from svc_infra.db.base import ModelBase
|
14
14
|
from svc_infra.auth.user_default import _pwd
|
15
|
+
from svc_infra.db.uniq import make_unique_indexes
|
16
|
+
|
15
17
|
|
16
18
|
class User(ModelBase):
|
17
19
|
__tablename__ = "users"
|
@@ -24,10 +26,12 @@ class User(ModelBase):
|
|
24
26
|
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
25
27
|
is_verified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
26
28
|
|
29
|
+
# auth state
|
27
30
|
password_hash: Mapped[str] = mapped_column(String(512), nullable=False)
|
28
31
|
|
32
|
+
# Write-only facade over password
|
29
33
|
@property
|
30
|
-
def password(self) -> str:
|
34
|
+
def password(self) -> str:
|
31
35
|
raise AttributeError("password is write-only")
|
32
36
|
|
33
37
|
@password.setter
|
@@ -53,12 +57,16 @@ class User(ModelBase):
|
|
53
57
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
54
58
|
)
|
55
59
|
|
56
|
-
__table_args__ = (
|
57
|
-
UniqueConstraint("tenant_id", "email", name="uq_users_tenant_email"),
|
58
|
-
)
|
59
|
-
|
60
60
|
def __repr__(self) -> str:
|
61
61
|
return f"<User id={self.id} email={self.email!r}>"
|
62
62
|
|
63
|
-
|
64
|
-
|
63
|
+
|
64
|
+
# Unique indexes, including case-insensitive and/or tenant-scoped.
|
65
|
+
for _ix in make_unique_indexes(
|
66
|
+
User,
|
67
|
+
unique_ci=["email"], # case-insensitive unique email
|
68
|
+
tenant_field="tenant_id", # scoped by tenant if provided
|
69
|
+
):
|
70
|
+
# Simply iterating keeps them registered with the Table metadata.
|
71
|
+
# (No further action needed if you use Alembic autogenerate or metadata.create_all.)
|
72
|
+
pass
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# models/${table_name}.py
|
2
|
+
from __future__ import annotations
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import Optional
|
5
|
+
import uuid
|
6
|
+
|
7
|
+
from sqlalchemy import String, Boolean, DateTime, JSON, Text, func
|
8
|
+
from sqlalchemy.dialects.postgresql import UUID
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
10
|
+
from sqlalchemy.ext.mutable import MutableDict
|
11
|
+
|
12
|
+
from svc_infra.db.base import ModelBase
|
13
|
+
from svc_infra.db.uniq import make_unique_indexes
|
14
|
+
|
15
|
+
|
16
|
+
class ${Entity}(ModelBase):
|
17
|
+
__tablename__ = "${table_name}"
|
18
|
+
|
19
|
+
# identity
|
20
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
21
|
+
|
22
|
+
# core fields
|
23
|
+
name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
24
|
+
description: Mapped[Optional[str]] = mapped_column(Text)
|
25
|
+
|
26
|
+
${tenant_field}\
|
27
|
+
${soft_delete_field}\
|
28
|
+
# misc (avoid attr name "metadata" clash)
|
29
|
+
extra: Mapped[dict] = mapped_column(MutableDict.as_mutable(JSON), default=dict)
|
30
|
+
|
31
|
+
# auditing (DB-side timestamps)
|
32
|
+
created_at: Mapped[datetime] = mapped_column(
|
33
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
34
|
+
)
|
35
|
+
updated_at: Mapped[datetime] = mapped_column(
|
36
|
+
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
37
|
+
)
|
38
|
+
|
39
|
+
def __repr__(self) -> str:
|
40
|
+
return f"<${Entity} id={self.id} name={self.name!r}>"
|
41
|
+
|
42
|
+
|
43
|
+
# --- Uniqueness policy --------------------------------------------------------
|
44
|
+
# Register functional unique indexes (case-insensitive on "name"),
|
45
|
+
# optionally scoped by tenant if present.
|
46
|
+
for _ix in make_unique_indexes(
|
47
|
+
${Entity},
|
48
|
+
unique_ci=["name"]${tenant_arg}
|
49
|
+
):
|
50
|
+
# Iteration is enough to attach them to the Table metadata
|
51
|
+
# (Alembic autogenerate / metadata.create_all will pick them up)
|
52
|
+
pass
|
@@ -0,0 +1,98 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import Iterable, Sequence, Tuple, Union, List, Optional
|
3
|
+
from sqlalchemy import Index, func
|
4
|
+
|
5
|
+
|
6
|
+
ColumnSpec = Union[str, Sequence[str]] # "email" or ("first_name","last_name")
|
7
|
+
|
8
|
+
|
9
|
+
def _as_tuple(spec: ColumnSpec) -> Tuple[str, ...]:
|
10
|
+
return (spec,) if isinstance(spec, str) else tuple(spec)
|
11
|
+
|
12
|
+
|
13
|
+
def make_unique_indexes(
|
14
|
+
model: type,
|
15
|
+
*,
|
16
|
+
# Case-sensitive uniqueness specs (exact match)
|
17
|
+
unique_cs: Iterable[ColumnSpec] = (),
|
18
|
+
# Case-insensitive uniqueness specs: lower() applied to string columns
|
19
|
+
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
|
+
tenant_field: Optional[str] = None,
|
24
|
+
# Prefix for index names
|
25
|
+
name_prefix: str = "uq",
|
26
|
+
) -> List[Index]:
|
27
|
+
"""
|
28
|
+
Returns a list of SQLAlchemy Index objects enforcing uniqueness.
|
29
|
+
|
30
|
+
Notes:
|
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
|
41
|
+
"""
|
42
|
+
idxs: List[Index] = []
|
43
|
+
|
44
|
+
def _col(name: str):
|
45
|
+
return getattr(model, name)
|
46
|
+
|
47
|
+
def _to_sa_cols(spec: Tuple[str, ...], *, ci: bool):
|
48
|
+
cols = []
|
49
|
+
for cname in spec:
|
50
|
+
c = _col(cname)
|
51
|
+
cols.append(func.lower(c) if ci else c)
|
52
|
+
return tuple(cols)
|
53
|
+
|
54
|
+
tenant_col = _col(tenant_field) if tenant_field else None
|
55
|
+
|
56
|
+
# Helper: name like uq_<table>_<tenant?>_<ci/cs>_<joined-columns>
|
57
|
+
def _name(ci: bool, spec: Tuple[str, ...], null_bucket: Optional[str] = None):
|
58
|
+
parts = [name_prefix, model.__tablename__]
|
59
|
+
if tenant_field:
|
60
|
+
parts.append(tenant_field)
|
61
|
+
if null_bucket:
|
62
|
+
parts.append(null_bucket)
|
63
|
+
parts.append("ci" if ci else "cs")
|
64
|
+
parts.extend(spec)
|
65
|
+
return "_".join(parts)
|
66
|
+
|
67
|
+
# Build indexes for both CS and CI specs
|
68
|
+
for ci, spec_list in ((False, unique_cs), (True, unique_ci)):
|
69
|
+
for spec in spec_list:
|
70
|
+
spec_t = _as_tuple(spec)
|
71
|
+
cols = _to_sa_cols(spec_t, ci=ci)
|
72
|
+
|
73
|
+
if tenant_col is None:
|
74
|
+
# simple global unique (with or without lower())
|
75
|
+
idxs.append(Index(_name(ci, spec_t), *cols, unique=True))
|
76
|
+
else:
|
77
|
+
# two partial unique indexes to treat NULL tenant as its own bucket
|
78
|
+
# 1) global bucket (tenant IS NULL)
|
79
|
+
idxs.append(
|
80
|
+
Index(
|
81
|
+
_name(ci, spec_t, "null"),
|
82
|
+
*cols,
|
83
|
+
unique=True,
|
84
|
+
postgresql_where=tenant_col.is_(None),
|
85
|
+
)
|
86
|
+
)
|
87
|
+
# 2) per-tenant bucket (tenant IS NOT NULL)
|
88
|
+
idxs.append(
|
89
|
+
Index(
|
90
|
+
_name(ci, spec_t, "notnull"),
|
91
|
+
tenant_col,
|
92
|
+
*cols,
|
93
|
+
unique=True,
|
94
|
+
postgresql_where=tenant_col.isnot(None),
|
95
|
+
)
|
96
|
+
)
|
97
|
+
|
98
|
+
return idxs
|
@@ -1,27 +0,0 @@
|
|
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)
|
@@ -1,29 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from datetime import datetime
|
3
|
-
from typing import Optional
|
4
|
-
import uuid
|
5
|
-
|
6
|
-
from sqlalchemy import String, Boolean, DateTime, JSON, Text, func, UniqueConstraint, Index
|
7
|
-
from sqlalchemy.dialects.postgresql import UUID
|
8
|
-
from sqlalchemy.orm import Mapped, mapped_column
|
9
|
-
from sqlalchemy.ext.mutable import MutableDict
|
10
|
-
|
11
|
-
from svc_infra.db.base import ModelBase
|
12
|
-
|
13
|
-
|
14
|
-
class ${Entity}(ModelBase):
|
15
|
-
__tablename__ = "${table_name}"
|
16
|
-
|
17
|
-
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
18
|
-
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
19
|
-
description: Mapped[Optional[str]] = mapped_column(Text)
|
20
|
-
|
21
|
-
${tenant_field}${soft_delete_field} extra: Mapped[dict] = mapped_column(MutableDict.as_mutable(JSON), default=dict)
|
22
|
-
|
23
|
-
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
24
|
-
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
25
|
-
|
26
|
-
${constraints} def __repr__(self) -> str:
|
27
|
-
return f"<${Entity} id={self.id} name={self.name!r}>"
|
28
|
-
|
29
|
-
${indexes}
|
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.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/middleware/errors/__init__.py
RENAMED
File without changes
|
{svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/api/fastapi/middleware/errors/catchall.py
RENAMED
File without changes
|
File without changes
|
{svc_infra-0.1.181 → svc_infra-0.1.183}/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.181 → svc_infra-0.1.183}/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.181 → svc_infra-0.1.183}/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.181 → svc_infra-0.1.183}/src/svc_infra/observability/templates/grafana_dashboard.json
RENAMED
File without changes
|
{svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/templates/otel-collector.yaml
RENAMED
File without changes
|
{svc_infra-0.1.181 → svc_infra-0.1.183}/src/svc_infra/observability/templates/prometheus_rules.yml
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|