svc-infra 0.1.600__py3-none-any.whl → 0.1.664__py3-none-any.whl

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.

Potentially problematic release.


This version of svc-infra might be problematic. Click here for more details.

Files changed (140) hide show
  1. svc_infra/api/fastapi/admin/__init__.py +3 -0
  2. svc_infra/api/fastapi/admin/add.py +231 -0
  3. svc_infra/api/fastapi/apf_payments/setup.py +0 -2
  4. svc_infra/api/fastapi/auth/add.py +0 -4
  5. svc_infra/api/fastapi/auth/routers/oauth_router.py +19 -4
  6. svc_infra/api/fastapi/billing/router.py +64 -0
  7. svc_infra/api/fastapi/billing/setup.py +19 -0
  8. svc_infra/api/fastapi/cache/add.py +9 -5
  9. svc_infra/api/fastapi/db/nosql/mongo/add.py +33 -27
  10. svc_infra/api/fastapi/db/sql/add.py +40 -18
  11. svc_infra/api/fastapi/db/sql/crud_router.py +176 -14
  12. svc_infra/api/fastapi/db/sql/session.py +16 -0
  13. svc_infra/api/fastapi/dependencies/ratelimit.py +57 -7
  14. svc_infra/api/fastapi/docs/add.py +160 -0
  15. svc_infra/api/fastapi/docs/landing.py +1 -1
  16. svc_infra/api/fastapi/docs/scoped.py +41 -6
  17. svc_infra/api/fastapi/middleware/errors/handlers.py +45 -7
  18. svc_infra/api/fastapi/middleware/graceful_shutdown.py +87 -0
  19. svc_infra/api/fastapi/middleware/ratelimit.py +59 -1
  20. svc_infra/api/fastapi/middleware/ratelimit_store.py +12 -6
  21. svc_infra/api/fastapi/middleware/timeout.py +148 -0
  22. svc_infra/api/fastapi/openapi/mutators.py +114 -0
  23. svc_infra/api/fastapi/ops/add.py +73 -0
  24. svc_infra/api/fastapi/pagination.py +3 -1
  25. svc_infra/api/fastapi/routers/ping.py +1 -0
  26. svc_infra/api/fastapi/setup.py +21 -13
  27. svc_infra/api/fastapi/tenancy/add.py +19 -0
  28. svc_infra/api/fastapi/tenancy/context.py +112 -0
  29. svc_infra/api/fastapi/versioned.py +101 -0
  30. svc_infra/app/README.md +5 -5
  31. svc_infra/billing/__init__.py +23 -0
  32. svc_infra/billing/async_service.py +147 -0
  33. svc_infra/billing/jobs.py +230 -0
  34. svc_infra/billing/models.py +131 -0
  35. svc_infra/billing/quotas.py +101 -0
  36. svc_infra/billing/schemas.py +33 -0
  37. svc_infra/billing/service.py +115 -0
  38. svc_infra/bundled_docs/README.md +5 -0
  39. svc_infra/bundled_docs/__init__.py +1 -0
  40. svc_infra/bundled_docs/getting-started.md +6 -0
  41. svc_infra/cache/__init__.py +4 -0
  42. svc_infra/cache/add.py +158 -0
  43. svc_infra/cache/backend.py +5 -2
  44. svc_infra/cache/decorators.py +19 -1
  45. svc_infra/cache/keys.py +24 -4
  46. svc_infra/cli/__init__.py +28 -8
  47. svc_infra/cli/cmds/__init__.py +8 -0
  48. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +4 -3
  49. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +4 -4
  50. svc_infra/cli/cmds/db/sql/alembic_cmds.py +80 -11
  51. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
  52. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
  53. svc_infra/cli/cmds/docs/docs_cmds.py +140 -0
  54. svc_infra/cli/cmds/dx/__init__.py +12 -0
  55. svc_infra/cli/cmds/dx/dx_cmds.py +99 -0
  56. svc_infra/cli/cmds/help.py +4 -0
  57. svc_infra/cli/cmds/obs/obs_cmds.py +4 -3
  58. svc_infra/cli/cmds/sdk/__init__.py +0 -0
  59. svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
  60. svc_infra/data/add.py +61 -0
  61. svc_infra/data/backup.py +53 -0
  62. svc_infra/data/erasure.py +45 -0
  63. svc_infra/data/fixtures.py +40 -0
  64. svc_infra/data/retention.py +55 -0
  65. svc_infra/db/nosql/mongo/README.md +13 -13
  66. svc_infra/db/sql/repository.py +51 -11
  67. svc_infra/db/sql/resource.py +5 -0
  68. svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
  69. svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
  70. svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
  71. svc_infra/db/sql/tenant.py +79 -0
  72. svc_infra/db/sql/utils.py +18 -4
  73. svc_infra/docs/acceptance-matrix.md +88 -0
  74. svc_infra/docs/acceptance.md +44 -0
  75. svc_infra/docs/admin.md +425 -0
  76. svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +40 -0
  77. svc_infra/docs/adr/0003-webhooks-framework.md +24 -0
  78. svc_infra/docs/adr/0004-tenancy-model.md +42 -0
  79. svc_infra/docs/adr/0005-data-lifecycle.md +86 -0
  80. svc_infra/docs/adr/0006-ops-slos-and-metrics.md +47 -0
  81. svc_infra/docs/adr/0007-docs-and-sdks.md +83 -0
  82. svc_infra/docs/adr/0008-billing-primitives.md +143 -0
  83. svc_infra/docs/adr/0009-acceptance-harness.md +40 -0
  84. svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +54 -0
  85. svc_infra/docs/adr/0011-admin-scope-and-impersonation.md +73 -0
  86. svc_infra/docs/adr/0012-generic-file-storage.md +498 -0
  87. svc_infra/docs/api.md +186 -0
  88. svc_infra/docs/auth.md +11 -0
  89. svc_infra/docs/billing.md +190 -0
  90. svc_infra/docs/cache.md +76 -0
  91. svc_infra/docs/cli.md +74 -0
  92. svc_infra/docs/contributing.md +34 -0
  93. svc_infra/docs/data-lifecycle.md +52 -0
  94. svc_infra/docs/database.md +14 -0
  95. svc_infra/docs/docs-and-sdks.md +62 -0
  96. svc_infra/docs/environment.md +114 -0
  97. svc_infra/docs/getting-started.md +63 -0
  98. svc_infra/docs/idempotency.md +111 -0
  99. svc_infra/docs/jobs.md +67 -0
  100. svc_infra/docs/observability.md +16 -0
  101. svc_infra/docs/ops.md +37 -0
  102. svc_infra/docs/rate-limiting.md +125 -0
  103. svc_infra/docs/repo-review.md +48 -0
  104. svc_infra/docs/security.md +176 -0
  105. svc_infra/docs/storage.md +982 -0
  106. svc_infra/docs/tenancy.md +35 -0
  107. svc_infra/docs/timeouts-and-resource-limits.md +147 -0
  108. svc_infra/docs/versioned-integrations.md +146 -0
  109. svc_infra/docs/webhooks.md +112 -0
  110. svc_infra/dx/add.py +63 -0
  111. svc_infra/dx/changelog.py +74 -0
  112. svc_infra/dx/checks.py +67 -0
  113. svc_infra/http/__init__.py +13 -0
  114. svc_infra/http/client.py +72 -0
  115. svc_infra/jobs/builtins/webhook_delivery.py +14 -2
  116. svc_infra/jobs/queue.py +9 -1
  117. svc_infra/jobs/runner.py +75 -0
  118. svc_infra/jobs/worker.py +17 -1
  119. svc_infra/mcp/svc_infra_mcp.py +85 -28
  120. svc_infra/obs/add.py +54 -7
  121. svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
  122. svc_infra/security/headers.py +15 -2
  123. svc_infra/security/hibp.py +6 -2
  124. svc_infra/security/models.py +27 -7
  125. svc_infra/security/oauth_models.py +59 -0
  126. svc_infra/security/permissions.py +1 -0
  127. svc_infra/storage/__init__.py +93 -0
  128. svc_infra/storage/add.py +250 -0
  129. svc_infra/storage/backends/__init__.py +11 -0
  130. svc_infra/storage/backends/local.py +331 -0
  131. svc_infra/storage/backends/memory.py +214 -0
  132. svc_infra/storage/backends/s3.py +329 -0
  133. svc_infra/storage/base.py +239 -0
  134. svc_infra/storage/easy.py +182 -0
  135. svc_infra/storage/settings.py +192 -0
  136. svc_infra/webhooks/service.py +10 -2
  137. {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/METADATA +45 -14
  138. {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/RECORD +140 -52
  139. {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/WHEEL +0 -0
  140. {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,182 @@
1
+ """
2
+ Easy storage backend builder with auto-detection.
3
+
4
+ Simplifies storage backend initialization with sensible defaults.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ from typing import Optional
10
+
11
+ from .backends import LocalBackend, MemoryBackend, S3Backend
12
+ from .base import StorageBackend
13
+ from .settings import StorageSettings
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def easy_storage(
19
+ backend: Optional[str] = None,
20
+ **kwargs,
21
+ ) -> StorageBackend:
22
+ """
23
+ Create a storage backend with auto-detection or explicit selection.
24
+
25
+ This is the recommended way to initialize storage in most applications.
26
+ It handles environment-based configuration and provides sensible defaults.
27
+
28
+ Args:
29
+ backend: Explicit backend type ("local", "s3", "gcs", "cloudinary", "memory")
30
+ If None, auto-detects from environment variables
31
+ **kwargs: Backend-specific configuration overrides
32
+
33
+ Returns:
34
+ Initialized storage backend
35
+
36
+ Auto-Detection Order:
37
+ 1. Explicit backend parameter
38
+ 2. STORAGE_BACKEND environment variable
39
+ 3. Railway volume (RAILWAY_VOLUME_MOUNT_PATH) → LocalBackend
40
+ 4. S3 credentials (AWS_ACCESS_KEY_ID or STORAGE_S3_BUCKET) → S3Backend
41
+ 5. GCS credentials (GOOGLE_APPLICATION_CREDENTIALS) → GCSBackend
42
+ 6. Cloudinary credentials (CLOUDINARY_URL) → CloudinaryBackend
43
+ 7. Default: MemoryBackend (with warning)
44
+
45
+ Examples:
46
+ >>> # Auto-detect backend from environment
47
+ >>> storage = easy_storage()
48
+ >>>
49
+ >>> # Explicit local backend
50
+ >>> storage = easy_storage(
51
+ ... backend="local",
52
+ ... base_path="/data/uploads"
53
+ ... )
54
+ >>>
55
+ >>> # Explicit S3 backend
56
+ >>> storage = easy_storage(
57
+ ... backend="s3",
58
+ ... bucket="my-uploads",
59
+ ... region="us-west-2"
60
+ ... )
61
+ >>>
62
+ >>> # DigitalOcean Spaces
63
+ >>> storage = easy_storage(
64
+ ... backend="s3",
65
+ ... bucket="my-uploads",
66
+ ... region="nyc3",
67
+ ... endpoint="https://nyc3.digitaloceanspaces.com"
68
+ ... )
69
+
70
+ Environment Variables:
71
+ See StorageSettings for full list of environment variables.
72
+
73
+ Raises:
74
+ ValueError: If backend type is unsupported or configuration is invalid
75
+ ImportError: If required backend dependencies are not installed
76
+
77
+ Note:
78
+ For production deployments, it's recommended to set STORAGE_BACKEND
79
+ explicitly to avoid unexpected auto-detection behavior.
80
+ """
81
+ # Load settings
82
+ settings = StorageSettings()
83
+
84
+ # Determine backend type
85
+ backend_type = backend or settings.detect_backend()
86
+
87
+ logger.info(f"Initializing {backend_type} storage backend")
88
+
89
+ # Create backend instance
90
+ if backend_type == "memory":
91
+ # Memory backend
92
+ if backend_type == settings.detect_backend() and not backend:
93
+ logger.warning(
94
+ "Using MemoryBackend (in-memory storage). "
95
+ "Data will be lost on restart. "
96
+ "Set STORAGE_BACKEND environment variable for production."
97
+ )
98
+
99
+ max_size = kwargs.get("max_size", 100_000_000)
100
+ return MemoryBackend(max_size=max_size)
101
+
102
+ elif backend_type == "local":
103
+ # Local filesystem backend
104
+ base_path = kwargs.get("base_path") or settings.storage_base_path
105
+
106
+ # Check for Railway volume
107
+ railway_volume = os.getenv("RAILWAY_VOLUME_MOUNT_PATH")
108
+ if railway_volume and not kwargs.get("base_path"):
109
+ base_path = railway_volume
110
+ logger.info(f"Detected Railway volume at {base_path}")
111
+
112
+ base_url = kwargs.get("base_url") or settings.storage_base_url
113
+ signing_secret = kwargs.get("signing_secret") or settings.storage_signing_secret
114
+
115
+ return LocalBackend(
116
+ base_path=base_path,
117
+ base_url=base_url,
118
+ signing_secret=signing_secret,
119
+ )
120
+
121
+ elif backend_type == "s3":
122
+ # S3-compatible backend
123
+ bucket = kwargs.get("bucket") or settings.storage_s3_bucket
124
+ if not bucket:
125
+ raise ValueError(
126
+ "S3 bucket is required. "
127
+ "Set STORAGE_S3_BUCKET environment variable or pass bucket parameter."
128
+ )
129
+
130
+ region = kwargs.get("region") or settings.storage_s3_region
131
+ endpoint = kwargs.get("endpoint") or settings.storage_s3_endpoint
132
+
133
+ # Get credentials with fallback
134
+ access_key = kwargs.get("access_key")
135
+ secret_key = kwargs.get("secret_key")
136
+
137
+ if not access_key or not secret_key:
138
+ access_key_from_settings, secret_key_from_settings = settings.get_s3_credentials()
139
+ access_key = access_key or access_key_from_settings
140
+ secret_key = secret_key or secret_key_from_settings
141
+
142
+ # Log provider detection
143
+ if endpoint:
144
+ if "digitalocean" in endpoint:
145
+ logger.info("Detected DigitalOcean Spaces")
146
+ elif "wasabi" in endpoint:
147
+ logger.info("Detected Wasabi")
148
+ elif "backblaze" in endpoint:
149
+ logger.info("Detected Backblaze B2")
150
+ else:
151
+ logger.info(f"Using custom S3 endpoint: {endpoint}")
152
+ else:
153
+ logger.info("Using AWS S3")
154
+
155
+ return S3Backend(
156
+ bucket=bucket,
157
+ region=region,
158
+ endpoint=endpoint,
159
+ access_key=access_key,
160
+ secret_key=secret_key,
161
+ )
162
+
163
+ elif backend_type == "gcs":
164
+ # Google Cloud Storage backend
165
+ raise NotImplementedError(
166
+ "GCS backend not yet implemented. " "Use 'local' or 's3' backend for now."
167
+ )
168
+
169
+ elif backend_type == "cloudinary":
170
+ # Cloudinary backend
171
+ raise NotImplementedError(
172
+ "Cloudinary backend not yet implemented. " "Use 'local' or 's3' backend for now."
173
+ )
174
+
175
+ else:
176
+ raise ValueError(
177
+ f"Unsupported storage backend: {backend_type}. "
178
+ f"Supported: local, s3, memory (gcs, cloudinary coming soon)"
179
+ )
180
+
181
+
182
+ __all__ = ["easy_storage"]
@@ -0,0 +1,192 @@
1
+ """
2
+ Storage configuration and settings.
3
+
4
+ Handles environment-based configuration and auto-detection of storage backends.
5
+ """
6
+
7
+ import os
8
+ from typing import Literal, Optional
9
+
10
+ from pydantic import Field
11
+ from pydantic_settings import BaseSettings
12
+
13
+
14
+ class StorageSettings(BaseSettings):
15
+ """
16
+ Storage system configuration.
17
+
18
+ Supports multiple backends with auto-detection from environment variables.
19
+
20
+ Environment Variables:
21
+ STORAGE_BACKEND: Explicit backend selection ("local", "s3", "gcs", "cloudinary", "memory")
22
+
23
+ Local Backend:
24
+ STORAGE_BASE_PATH: Base directory for file storage (default: /data/uploads)
25
+ STORAGE_BASE_URL: Base URL for file serving (default: http://localhost:8000/files)
26
+ STORAGE_SIGNING_SECRET: Secret key for URL signing (auto-generated if not set)
27
+
28
+ S3 Backend:
29
+ STORAGE_S3_BUCKET: S3 bucket name (required for S3)
30
+ STORAGE_S3_REGION: AWS region (default: us-east-1)
31
+ STORAGE_S3_ENDPOINT: Custom endpoint for S3-compatible services (optional)
32
+ STORAGE_S3_ACCESS_KEY: AWS access key (optional, uses AWS_ACCESS_KEY_ID if not set)
33
+ STORAGE_S3_SECRET_KEY: AWS secret key (optional, uses AWS_SECRET_ACCESS_KEY if not set)
34
+
35
+ GCS Backend:
36
+ STORAGE_GCS_BUCKET: GCS bucket name (required for GCS)
37
+ STORAGE_GCS_PROJECT: GCP project ID (optional)
38
+ STORAGE_GCS_CREDENTIALS_PATH: Path to service account JSON (optional)
39
+
40
+ Cloudinary Backend:
41
+ STORAGE_CLOUDINARY_CLOUD_NAME: Cloudinary cloud name (required)
42
+ STORAGE_CLOUDINARY_API_KEY: Cloudinary API key (required)
43
+ STORAGE_CLOUDINARY_API_SECRET: Cloudinary API secret (required)
44
+
45
+ Example:
46
+ >>> # Auto-detect backend from environment
47
+ >>> settings = StorageSettings()
48
+ >>> backend = settings.detect_backend()
49
+ >>>
50
+ >>> # Explicit backend selection
51
+ >>> settings = StorageSettings(storage_backend="s3")
52
+ """
53
+
54
+ # Backend selection
55
+ storage_backend: Optional[Literal["local", "s3", "gcs", "cloudinary", "memory"]] = Field(
56
+ default=None,
57
+ description="Storage backend type (auto-detected if not set)",
58
+ )
59
+
60
+ # Local backend settings
61
+ storage_base_path: str = Field(
62
+ default="/data/uploads",
63
+ description="Base directory for local file storage",
64
+ )
65
+ storage_base_url: str = Field(
66
+ default="http://localhost:8000/files",
67
+ description="Base URL for serving files",
68
+ )
69
+ storage_signing_secret: Optional[str] = Field(
70
+ default=None,
71
+ description="Secret key for URL signing (auto-generated if not set)",
72
+ )
73
+
74
+ # S3 backend settings
75
+ storage_s3_bucket: Optional[str] = Field(
76
+ default=None,
77
+ description="S3 bucket name",
78
+ )
79
+ storage_s3_region: str = Field(
80
+ default="us-east-1",
81
+ description="AWS region",
82
+ )
83
+ storage_s3_endpoint: Optional[str] = Field(
84
+ default=None,
85
+ description="Custom S3 endpoint (for DigitalOcean Spaces, Wasabi, etc.)",
86
+ )
87
+ storage_s3_access_key: Optional[str] = Field(
88
+ default=None,
89
+ description="S3 access key (falls back to AWS_ACCESS_KEY_ID)",
90
+ )
91
+ storage_s3_secret_key: Optional[str] = Field(
92
+ default=None,
93
+ description="S3 secret key (falls back to AWS_SECRET_ACCESS_KEY)",
94
+ )
95
+
96
+ # GCS backend settings
97
+ storage_gcs_bucket: Optional[str] = Field(
98
+ default=None,
99
+ description="Google Cloud Storage bucket name",
100
+ )
101
+ storage_gcs_project: Optional[str] = Field(
102
+ default=None,
103
+ description="GCP project ID",
104
+ )
105
+ storage_gcs_credentials_path: Optional[str] = Field(
106
+ default=None,
107
+ description="Path to GCP service account JSON",
108
+ )
109
+
110
+ # Cloudinary backend settings
111
+ storage_cloudinary_cloud_name: Optional[str] = Field(
112
+ default=None,
113
+ description="Cloudinary cloud name",
114
+ )
115
+ storage_cloudinary_api_key: Optional[str] = Field(
116
+ default=None,
117
+ description="Cloudinary API key",
118
+ )
119
+ storage_cloudinary_api_secret: Optional[str] = Field(
120
+ default=None,
121
+ description="Cloudinary API secret",
122
+ )
123
+
124
+ model_config = {
125
+ "env_file": ".env",
126
+ "case_sensitive": False,
127
+ }
128
+
129
+ def detect_backend(self) -> str:
130
+ """
131
+ Auto-detect storage backend from environment.
132
+
133
+ Detection order:
134
+ 1. Explicit STORAGE_BACKEND setting
135
+ 2. Railway volume (RAILWAY_VOLUME_MOUNT_PATH) → local
136
+ 3. S3 credentials (AWS_ACCESS_KEY_ID or STORAGE_S3_BUCKET) → s3
137
+ 4. GCS credentials (GOOGLE_APPLICATION_CREDENTIALS or STORAGE_GCS_BUCKET) → gcs
138
+ 5. Cloudinary credentials (CLOUDINARY_URL or STORAGE_CLOUDINARY_CLOUD_NAME) → cloudinary
139
+ 6. Default → memory (with warning)
140
+
141
+ Returns:
142
+ Backend type string
143
+
144
+ Example:
145
+ >>> settings = StorageSettings()
146
+ >>> backend_type = settings.detect_backend()
147
+ >>> print(f"Using {backend_type} backend")
148
+ """
149
+ # Explicit setting takes precedence
150
+ if self.storage_backend:
151
+ return self.storage_backend
152
+
153
+ # Check for Railway volume
154
+ railway_volume = os.getenv("RAILWAY_VOLUME_MOUNT_PATH")
155
+ if railway_volume:
156
+ return "local"
157
+
158
+ # Check for S3
159
+ has_s3_key = os.getenv("AWS_ACCESS_KEY_ID") or self.storage_s3_access_key
160
+ if has_s3_key or self.storage_s3_bucket:
161
+ return "s3"
162
+
163
+ # Check for GCS
164
+ has_gcs_creds = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
165
+ if has_gcs_creds or self.storage_gcs_bucket:
166
+ return "gcs"
167
+
168
+ # Check for Cloudinary
169
+ has_cloudinary = os.getenv("CLOUDINARY_URL")
170
+ if has_cloudinary or self.storage_cloudinary_cloud_name:
171
+ return "cloudinary"
172
+
173
+ # Default to memory (for development/testing)
174
+ return "memory"
175
+
176
+ def get_s3_credentials(self) -> tuple[Optional[str], Optional[str]]:
177
+ """
178
+ Get S3 credentials with fallback to AWS environment variables.
179
+
180
+ Returns:
181
+ Tuple of (access_key, secret_key)
182
+
183
+ Example:
184
+ >>> settings = StorageSettings()
185
+ >>> access_key, secret_key = settings.get_s3_credentials()
186
+ """
187
+ access_key = self.storage_s3_access_key or os.getenv("AWS_ACCESS_KEY_ID")
188
+ secret_key = self.storage_s3_secret_key or os.getenv("AWS_SECRET_ACCESS_KEY")
189
+ return access_key, secret_key
190
+
191
+
192
+ __all__ = ["StorageSettings"]
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass, field
4
4
  from datetime import datetime, timezone
5
5
  from typing import Dict, List
6
-
7
6
  from uuid import uuid4
8
7
 
9
8
  from svc_infra.db.outbox import OutboxStore
@@ -22,7 +21,16 @@ class InMemoryWebhookSubscriptions:
22
21
  self._subs: Dict[str, List[WebhookSubscription]] = {}
23
22
 
24
23
  def add(self, topic: str, url: str, secret: str) -> None:
25
- self._subs.setdefault(topic, []).append(WebhookSubscription(topic, url, secret))
24
+ # Upsert semantics per (topic, url): if a subscription already exists
25
+ # for this topic and URL, rotate its secret instead of adding a new row.
26
+ # This mirrors typical real-world secret rotation flows where the
27
+ # endpoint remains the same but the signing secret changes.
28
+ lst = self._subs.setdefault(topic, [])
29
+ for sub in lst:
30
+ if sub.url == url:
31
+ sub.secret = secret
32
+ return
33
+ lst.append(WebhookSubscription(topic, url, secret))
26
34
 
27
35
  def get_for_topic(self, topic: str) -> List[WebhookSubscription]:
28
36
  return list(self._subs.get(topic, []))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.600
3
+ Version: 0.1.664
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
@@ -24,10 +24,13 @@ Provides-Extra: mysql
24
24
  Provides-Extra: pg
25
25
  Provides-Extra: pg2
26
26
  Provides-Extra: redshift
27
+ Provides-Extra: s3
27
28
  Provides-Extra: snowflake
28
29
  Provides-Extra: sqlite
29
30
  Requires-Dist: adyen (>=13.4.0,<14.0.0)
30
31
  Requires-Dist: ai-infra (>=0.1.63,<0.2.0)
32
+ Requires-Dist: aioboto3 (>=13.0.0,<14.0.0) ; extra == "s3"
33
+ Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
31
34
  Requires-Dist: aiosqlite (>=0.20.0,<0.21.0) ; extra == "sqlite"
32
35
  Requires-Dist: alembic (>=1.13.2,<2.0.0)
33
36
  Requires-Dist: asyncpg (>=0.30.0,<0.31.0) ; extra == "pg"
@@ -77,22 +80,49 @@ Description-Content-Type: text/markdown
77
80
  # svc-infra
78
81
 
79
82
  [![PyPI](https://img.shields.io/pypi/v/svc-infra.svg)](https://pypi.org/project/svc-infra/)
80
- [![Docs](https://img.shields.io/badge/docs-reference-blue)](docs/)
83
+ [![Docs](https://img.shields.io/badge/docs-reference-blue)](.)
81
84
 
82
85
  svc-infra packages the shared building blocks we use to ship production FastAPI services fast—HTTP APIs with secure auth, durable persistence, background execution, cache, observability, and webhook plumbing that all share the same batteries-included defaults.
83
86
 
84
87
  ## Helper index
85
88
 
86
- | Helper | What it covers | Guide |
89
+ | Area | What it covers | Guide |
87
90
  | --- | --- | --- |
88
- | API | FastAPI bootstrap, envelopes, middleware, docs wiring | [FastAPI guide](docs/api.md) |
89
- | Auth | Sessions, OAuth/OIDC, MFA, SMTP delivery | [Auth settings](docs/auth.md) |
90
- | Database | SQL + Mongo wiring, Alembic helpers, inbox/outbox patterns | [Database guide](docs/database.md) |
91
- | Jobs | JobQueue, scheduler, CLI worker | [Jobs quickstart](docs/jobs.md) |
92
- | Cache | cashews decorators, namespace management, TTL helpers | [Cache guide](docs/cache.md) |
93
- | Observability | Prometheus middleware, Grafana automation, OTEL hooks | [Observability guide](docs/observability.md) |
94
- | Webhooks | Subscription store, signing, retry worker | [Webhooks framework](docs/webhooks.md) |
95
- | Security | Password policy, lockout, signed cookies, headers | [Security hardening](docs/security.md) |
91
+ | Getting Started | Overview and entry points | [This page](src/svc_infra/docs/getting-started.md) |
92
+ | Environment | Feature switches and env vars | [Environment](src/svc_infra/docs/environment.md) |
93
+ | API | FastAPI bootstrap, middleware, docs wiring | [API guide](src/svc_infra/docs/api.md) |
94
+ | Auth | Sessions, OAuth/OIDC, MFA, SMTP delivery | [Auth](src/svc_infra/docs/auth.md) |
95
+ | Security | Password policy, lockout, signed cookies, headers | [Security](src/svc_infra/docs/security.md) |
96
+ | Database | SQL + Mongo wiring, Alembic helpers, inbox/outbox patterns | [Database](src/svc_infra/docs/database.md) |
97
+ | Storage | File storage with S3, local, memory backends | [Storage](src/svc_infra/docs/storage.md) |
98
+ | Tenancy | Multi-tenant boundaries and helpers | [Tenancy](src/svc_infra/docs/tenancy.md) |
99
+ | Idempotency | Idempotent endpoints and middleware | [Idempotency](src/svc_infra/docs/idempotency.md) |
100
+ | Rate Limiting | Middleware, dependency limiter, headers | [Rate limiting](src/svc_infra/docs/rate-limiting.md) |
101
+ | Cache | cashews decorators, namespace management, TTL helpers | [Cache](src/svc_infra/docs/cache.md) |
102
+ | Jobs | JobQueue, scheduler, CLI worker | [Jobs](src/svc_infra/docs/jobs.md) |
103
+ | Observability | Prometheus, Grafana, OpenTelemetry | [Observability](src/svc_infra/docs/observability.md) |
104
+ | Ops | Probes, breakers, SLOs & dashboards | [Ops](src/svc_infra/docs/ops.md) |
105
+ | Webhooks | Subscription store, signing, retry worker | [Webhooks](src/svc_infra/docs/webhooks.md) |
106
+ | CLI | Command groups for sql/mongo/obs/docs/dx/sdk/jobs | [CLI](src/svc_infra/docs/cli.md) |
107
+ | Docs & SDKs | Publishing docs, generating SDKs | [Docs & SDKs](src/svc_infra/docs/docs-and-sdks.md) |
108
+ | Acceptance | Acceptance harness and flows | [Acceptance](src/svc_infra/docs/acceptance.md), [Matrix](src/svc_infra/docs/acceptance-matrix.md) |
109
+ | Contributing | Dev setup and quality gates | [Contributing](src/svc_infra/docs/contributing.md) |
110
+ | Repo Review | Checklist for releasing/PRs | [Repo review](src/svc_infra/docs/repo-review.md) |
111
+ | Data Lifecycle | Fixtures, retention, erasure, backups | [Data lifecycle](src/svc_infra/docs/data-lifecycle.md) |
112
+
113
+ ## Quick Start with Template Example
114
+
115
+ See **ALL** svc-infra features working together in a complete example:
116
+
117
+ ```bash
118
+ # One-time setup (from repo root)
119
+ make setup-template # Scaffolds models, runs migrations
120
+
121
+ # Run the example server
122
+ make run-template # Starts at http://localhost:8001
123
+ ```
124
+
125
+ See [`examples/README.md`](examples/README.md) for full documentation and manual setup options.
96
126
 
97
127
  ## Minimal FastAPI bootstrap
98
128
 
@@ -120,9 +150,10 @@ async def handle_webhook(payload = Depends(require_signature(lambda: ["current",
120
150
  - **API** – toggle logging/observability and docs exposure with `ENABLE_LOGGING`, `LOG_LEVEL`, `LOG_FORMAT`, `ENABLE_OBS`, `METRICS_PATH`, `OBS_SKIP_PATHS`, and `CORS_ALLOW_ORIGINS`. 【F:src/svc_infra/api/fastapi/ease.py†L67-L111】【F:src/svc_infra/api/fastapi/setup.py†L47-L88】
121
151
  - **Auth** – configure JWT secrets, SMTP, cookies, and policy using the `AUTH_…` settings family (e.g., `AUTH_JWT__SECRET`, `AUTH_SMTP_HOST`, `AUTH_SESSION_COOKIE_SECURE`). 【F:src/svc_infra/api/fastapi/auth/settings.py†L23-L91】
122
152
  - **Database** – set connection URLs or components via `SQL_URL`/`SQL_URL_FILE`, `DB_DIALECT`, `DB_HOST`, `DB_USER`, `DB_PASSWORD`, plus Mongo knobs like `MONGO_URL`, `MONGO_DB`, and `MONGO_URL_FILE`. 【F:src/svc_infra/api/fastapi/db/sql/add.py†L55-L114】【F:src/svc_infra/db/sql/utils.py†L85-L206】【F:src/svc_infra/db/nosql/mongo/settings.py†L9-L13】【F:src/svc_infra/db/nosql/utils.py†L56-L113】
123
- - **Jobs** – choose the queue backend with `JOBS_DRIVER` and provide Redis via `REDIS_URL`; interval schedules can be declared with `JOBS_SCHEDULE_JSON`. 【F:src/svc_infra/jobs/easy.py†L11-L27】【F:docs/jobs.md†L11-L48
153
+ - **Storage** – choose backend with `STORAGE_BACKEND` (local, s3, memory) and configure with `STORAGE_S3_BUCKET`, `STORAGE_S3_REGION`, `STORAGE_BASE_PATH`, or auto-detect from `RAILWAY_VOLUME_MOUNT_PATH` / AWS credentials. 【F:src/svc_infra/storage/settings.py】【F:src/svc_infra/docs/storage.md】
154
+ - **Jobs** – choose the queue backend with `JOBS_DRIVER` and provide Redis via `REDIS_URL`; interval schedules can be declared with `JOBS_SCHEDULE_JSON`. 【F:src/svc_infra/jobs/easy.py†L11-L27】【F:src/svc_infra/docs/jobs.md†L11-L48】
124
155
  - **Cache** – namespace keys and lifetimes through `CACHE_PREFIX`, `CACHE_VERSION`, and TTL overrides `CACHE_TTL_DEFAULT`, `CACHE_TTL_SHORT`, `CACHE_TTL_LONG`. 【F:src/svc_infra/cache/README.md†L20-L173】【F:src/svc_infra/cache/ttl.py†L26-L55】
125
156
  - **Observability** – turn metrics on/off or adjust scrape paths with `ENABLE_OBS`, `METRICS_PATH`, `OBS_SKIP_PATHS`, and Prometheus/Grafana flags like `SVC_INFRA_DISABLE_PROMETHEUS`, `SVC_INFRA_RATE_WINDOW`, `SVC_INFRA_DASHBOARD_REFRESH`, `SVC_INFRA_DASHBOARD_RANGE`. 【F:src/svc_infra/api/fastapi/ease.py†L67-L111】【F:src/svc_infra/obs/metrics/asgi.py†L49-L206】【F:src/svc_infra/obs/cloud_dash.py†L85-L108】
126
- - **Webhooks** – reuse the jobs envs (`JOBS_DRIVER`, `REDIS_URL`) for the delivery worker and queue configuration. 【F:docs/webhooks.md†L32-L53】
127
- - **Security** – enforce password policy, MFA, and rotation with auth prefixes such as `AUTH_PASSWORD_MIN_LENGTH`, `AUTH_PASSWORD_REQUIRE_SYMBOL`, `AUTH_JWT__SECRET`, and `AUTH_JWT__OLD_SECRETS`. 【F:docs/security.md†L24-L70】
157
+ - **Webhooks** – reuse the jobs envs (`JOBS_DRIVER`, `REDIS_URL`) for the delivery worker and queue configuration. 【F:src/svc_infra/docs/webhooks.md†L32-L53】
158
+ - **Security** – enforce password policy, MFA, and rotation with auth prefixes such as `AUTH_PASSWORD_MIN_LENGTH`, `AUTH_PASSWORD_REQUIRE_SYMBOL`, `AUTH_JWT__SECRET`, and `AUTH_JWT__OLD_SECRETS`. 【F:src/svc_infra/docs/security.md†L24-L70】
128
159