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.
Files changed (82) hide show
  1. {svc_infra-0.1.175 → svc_infra-0.1.177}/PKG-INFO +1 -1
  2. {svc_infra-0.1.175 → svc_infra-0.1.177}/pyproject.toml +1 -1
  3. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/add.py +18 -1
  4. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/repository.py +9 -3
  5. svc_infra-0.1.177/src/svc_infra/api/fastapi/db/resource.py +30 -0
  6. svc_infra-0.1.177/src/svc_infra/api/fastapi/db/service_hooks.py +17 -0
  7. svc_infra-0.1.177/src/svc_infra/auth/__init__.py +7 -0
  8. svc_infra-0.1.177/src/svc_infra/auth/user_default.py +27 -0
  9. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/models_schemas/auth/models.py.tmpl +9 -1
  10. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/models_schemas/auth/schemas.py.tmpl +1 -0
  11. svc_infra-0.1.175/src/svc_infra/api/fastapi/db/resource.py +0 -31
  12. svc_infra-0.1.175/src/svc_infra/auth/__init__.py +0 -3
  13. {svc_infra-0.1.175 → svc_infra-0.1.177}/README.md +0 -0
  14. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/__init__.py +0 -0
  15. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/__init__.py +0 -0
  16. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/__init__.py +0 -0
  17. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/README.md +0 -0
  18. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
  19. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/crud_router.py +0 -0
  20. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/health.py +0 -0
  21. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/http.py +0 -0
  22. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/management.py +0 -0
  23. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/service.py +0 -0
  24. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/db/session.py +0 -0
  25. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
  26. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
  27. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
  28. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/errors/error_handlers.py +0 -0
  29. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
  30. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
  31. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
  32. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/api/fastapi/settings.py +0 -0
  33. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/app/__init__.py +0 -0
  34. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/app/env.py +0 -0
  35. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/app/logging.py +0 -0
  36. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/app/root.py +0 -0
  37. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/app/settings.py +0 -0
  38. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/auth/integration.py +0 -0
  39. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/auth/oauth_router.py +0 -0
  40. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/auth/providers.py +0 -0
  41. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/auth/settings.py +0 -0
  42. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/auth/users.py +0 -0
  43. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/__init__.py +0 -0
  44. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/cmds/__init__.py +0 -0
  45. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/cmds/alembic_cmds.py +0 -0
  46. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/cmds/help.py +0 -0
  47. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/cmds/scaffold_cmds.py +0 -0
  48. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/foundation/__init__.py +0 -0
  49. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/foundation/runner.py +0 -0
  50. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
  51. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/README.md +0 -0
  52. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/__init__.py +0 -0
  53. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/base.py +0 -0
  54. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/constants.py +0 -0
  55. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/core.py +0 -0
  56. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/scaffold.py +0 -0
  57. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/__init__.py +0 -0
  58. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/models_schemas/__init__.py +0 -0
  59. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/models_schemas/entity/models.py.tmpl +0 -0
  60. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/models_schemas/entity/schemas.py.tmpl +0 -0
  61. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/setup/__init__.py +0 -0
  62. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/setup/alembic.ini.tmpl +0 -0
  63. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/setup/env_async.py.tmpl +0 -0
  64. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/setup/env_sync.py.tmpl +0 -0
  65. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/templates/setup/script.py.mako.tmpl +0 -0
  66. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/db/utils.py +0 -0
  67. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/mcp/__init__.py +0 -0
  68. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
  69. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/README.md +0 -0
  70. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/__init__.py +0 -0
  71. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/metrics/__init__.py +0 -0
  72. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/metrics/asgi.py +0 -0
  73. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/metrics/base.py +0 -0
  74. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/metrics/http.py +0 -0
  75. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/metrics/sqlalchemy.py +0 -0
  76. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/settings.py +0 -0
  77. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/templates/grafana_dashboard.json +0 -0
  78. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/templates/otel-collector.yaml +0 -0
  79. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/templates/prometheus_rules.yml +0 -0
  80. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/tracing/__init__.py +0 -0
  81. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/observability/tracing/setup.py +0 -0
  82. {svc_infra-0.1.175 → svc_infra-0.1.177}/src/svc_infra/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.175
3
+ Version: 0.1.177
4
4
  Summary: Infrastructure for building and deploying prod-ready services
5
5
  License: MIT
6
6
  Keywords: fastapi,sqlalchemy,alembic,auth,infra,async,pydantic
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "svc-infra"
3
- version = "0.1.175"
3
+ version = "0.1.177"
4
4
  description = "Infrastructure for building and deploying prod-ready services"
5
5
  authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
6
6
  license = "MIT"
@@ -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
- svc = Service(repo)
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
- obj = self.model(**data)
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,7 @@
1
+ from .integration import enable_auth
2
+ from .user_default import make_user_service
3
+
4
+ __all__ = [
5
+ "enable_auth",
6
+ "make_user_service"
7
+ ]
@@ -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",)
@@ -1,3 +0,0 @@
1
- from .integration import enable_auth
2
-
3
- __all__ = ["enable_auth"]
File without changes