svc-infra 0.1.184__tar.gz → 0.1.186__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 (83) hide show
  1. {svc_infra-0.1.184 → svc_infra-0.1.186}/PKG-INFO +1 -1
  2. {svc_infra-0.1.184 → svc_infra-0.1.186}/pyproject.toml +1 -1
  3. svc_infra-0.1.186/src/svc_infra/auth/user_default.py +92 -0
  4. svc_infra-0.1.184/src/svc_infra/auth/user_default.py +0 -53
  5. {svc_infra-0.1.184 → svc_infra-0.1.186}/README.md +0 -0
  6. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/__init__.py +0 -0
  7. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/__init__.py +0 -0
  8. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/__init__.py +0 -0
  9. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/README.md +0 -0
  10. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
  11. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/add.py +0 -0
  12. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/crud_router.py +0 -0
  13. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/health.py +0 -0
  14. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/http.py +0 -0
  15. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/management.py +0 -0
  16. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/repository.py +0 -0
  17. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/resource.py +0 -0
  18. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/service.py +0 -0
  19. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/service_hooks.py +0 -0
  20. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/session.py +0 -0
  21. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/db/uniq.py +0 -0
  22. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
  23. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
  24. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
  25. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/middleware/errors/error_handlers.py +0 -0
  26. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
  27. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
  28. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
  29. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/api/fastapi/settings.py +0 -0
  30. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/app/__init__.py +0 -0
  31. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/app/env.py +0 -0
  32. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/app/logging.py +0 -0
  33. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/app/root.py +0 -0
  34. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/app/settings.py +0 -0
  35. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/auth/__init__.py +0 -0
  36. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/auth/integration.py +0 -0
  37. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/auth/oauth_router.py +0 -0
  38. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/auth/providers.py +0 -0
  39. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/auth/settings.py +0 -0
  40. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/auth/users.py +0 -0
  41. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/cli/__init__.py +0 -0
  42. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/cli/cmds/__init__.py +0 -0
  43. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/cli/cmds/alembic_cmds.py +0 -0
  44. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/cli/cmds/help.py +0 -0
  45. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/cli/cmds/scaffold_cmds.py +0 -0
  46. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/cli/foundation/__init__.py +0 -0
  47. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/cli/foundation/runner.py +0 -0
  48. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
  49. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/README.md +0 -0
  50. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/__init__.py +0 -0
  51. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/base.py +0 -0
  52. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/constants.py +0 -0
  53. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/core.py +0 -0
  54. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/scaffold.py +0 -0
  55. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/templates/__init__.py +0 -0
  56. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/templates/models_schemas/__init__.py +0 -0
  57. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/templates/models_schemas/auth/models.py.tmpl +0 -0
  58. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/templates/models_schemas/auth/schemas.py.tmpl +0 -0
  59. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/templates/models_schemas/entity/models.py.tmpl +0 -0
  60. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/templates/models_schemas/entity/schemas.py.tmpl +0 -0
  61. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/templates/setup/__init__.py +0 -0
  62. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/templates/setup/alembic.ini.tmpl +0 -0
  63. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/templates/setup/env_async.py.tmpl +0 -0
  64. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/templates/setup/env_sync.py.tmpl +0 -0
  65. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/templates/setup/script.py.mako.tmpl +0 -0
  66. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/uniq.py +0 -0
  67. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/db/utils.py +0 -0
  68. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/mcp/__init__.py +0 -0
  69. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
  70. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/README.md +0 -0
  71. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/__init__.py +0 -0
  72. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/metrics/__init__.py +0 -0
  73. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/metrics/asgi.py +0 -0
  74. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/metrics/base.py +0 -0
  75. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/metrics/http.py +0 -0
  76. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/metrics/sqlalchemy.py +0 -0
  77. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/settings.py +0 -0
  78. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/templates/grafana_dashboard.json +0 -0
  79. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/templates/otel-collector.yaml +0 -0
  80. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/templates/prometheus_rules.yml +0 -0
  81. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/tracing/__init__.py +0 -0
  82. {svc_infra-0.1.184 → svc_infra-0.1.186}/src/svc_infra/observability/tracing/setup.py +0 -0
  83. {svc_infra-0.1.184 → svc_infra-0.1.186}/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.184
3
+ Version: 0.1.186
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.184"
3
+ version = "0.1.186"
4
4
  description = "Infrastructure for building and deploying prod-ready services"
5
5
  authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
6
6
  license = "MIT"
@@ -0,0 +1,92 @@
1
+ from typing import Any, Dict
2
+ from fastapi import HTTPException
3
+ from fastapi_users.password import PasswordHelper
4
+ from sqlalchemy import func
5
+ from sqlalchemy.exc import IntegrityError
6
+
7
+ from svc_infra.api.fastapi.db.service_hooks import ServiceWithHooks
8
+ from svc_infra.api.fastapi.db.repository import Repository
9
+
10
+ _pwd = PasswordHelper()
11
+
12
+ def make_default_user_service(repo: Repository):
13
+ Model = repo.model # capture the mapped class
14
+
15
+ def _user_pre_create(data: Dict[str, Any]) -> Dict[str, Any]:
16
+ data = dict(data)
17
+ # normalize + map fields
18
+ if "password" in data:
19
+ data["password_hash"] = _pwd.hash(data.pop("password"))
20
+ if "metadata" in data:
21
+ data["extra"] = data.pop("metadata")
22
+ data.setdefault("roles", [])
23
+
24
+ # attach existence checker (case-insensitive email, tenant scoped)
25
+ email = data.get("email")
26
+ tenant_id = data.get("tenant_id")
27
+ if email is not None:
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
+ data["_precreate_exists_check"] = _exists
39
+ return data
40
+
41
+ def _user_pre_update(data: Dict[str, Any]) -> Dict[str, Any]:
42
+ data = dict(data)
43
+ if "password" in data:
44
+ data["password_hash"] = _pwd.hash(data.pop("password"))
45
+ if "metadata" in data:
46
+ data["extra"] = data.pop("metadata")
47
+
48
+ # optional: protect email change too
49
+ email = data.get("email")
50
+ tenant_id = data.get("tenant_id")
51
+ if email is not None:
52
+ where = [func.lower(Model.email) == func.lower(email)]
53
+ if hasattr(Model, "tenant_id"):
54
+ if tenant_id is None:
55
+ where.append(Model.tenant_id.is_(None))
56
+ else:
57
+ where.append(Model.tenant_id == tenant_id)
58
+
59
+ async def _exists(session):
60
+ return await repo.exists(session, where=where)
61
+
62
+ data["_preupdate_exists_check"] = _exists
63
+ return data
64
+
65
+ class _Svc(ServiceWithHooks):
66
+ async def create(self, session, data):
67
+ # IMPORTANT: run pre_create first
68
+ data = await self.pre_create(data)
69
+ exists_cb = data.pop("_precreate_exists_check", None)
70
+ if exists_cb and await exists_cb(session):
71
+ raise HTTPException(status_code=409, detail="User with this email already exists.")
72
+ try:
73
+ return await self.repo.create(session, data)
74
+ except IntegrityError as e:
75
+ # race-safety / fallback
76
+ if "uq_users_tenant_id" in str(e.orig) or "ci_email" in str(e.orig):
77
+ raise HTTPException(status_code=409, detail="User with this email already exists.") from e
78
+ raise
79
+
80
+ async def update(self, session, id_value, data):
81
+ data = await self.pre_update(data)
82
+ exists_cb = data.pop("_preupdate_exists_check", None)
83
+ if exists_cb and await exists_cb(session):
84
+ raise HTTPException(status_code=409, detail="User with this email already exists.")
85
+ try:
86
+ return await self.repo.update(session, id_value, data)
87
+ except IntegrityError as e:
88
+ if "uq_users_tenant_id" in str(e.orig) or "ci_email" in str(e.orig):
89
+ raise HTTPException(status_code=409, detail="User with this email already exists.") from e
90
+ raise
91
+
92
+ return _Svc(repo, pre_create=_user_pre_create, pre_update=_user_pre_update)
@@ -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