svc-infra 0.1.602__py3-none-any.whl → 0.1.603__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.

@@ -46,6 +46,18 @@ def make_crud_router_plus_sql(
46
46
  redirect_slashes=False,
47
47
  )
48
48
 
49
+ def _coerce_id(v: Any) -> Any:
50
+ """Best-effort coercion of path ids: cast digit-only strings to int.
51
+
52
+ Keeps original type otherwise to avoid breaking non-integer IDs.
53
+ """
54
+ if isinstance(v, str) and v.isdigit():
55
+ try:
56
+ return int(v)
57
+ except Exception:
58
+ return v
59
+ return v
60
+
49
61
  def _parse_ordering_to_fields(order_spec: Optional[str]) -> list[str]:
50
62
  if not order_spec:
51
63
  return []
@@ -87,9 +99,7 @@ def make_crud_router_plus_sql(
87
99
  else:
88
100
  items = await service.list(session, limit=lp.limit, offset=lp.offset, order_by=order_by)
89
101
  total = await service.count(session)
90
- return Page[read_schema].from_items(
91
- total=total, items=items, limit=lp.limit, offset=lp.offset
92
- )
102
+ return Page[Any].from_items(total=total, items=items, limit=lp.limit, offset=lp.offset)
93
103
 
94
104
  # -------- GET by id --------
95
105
  @router.get(
@@ -98,7 +108,7 @@ def make_crud_router_plus_sql(
98
108
  description=f"Get item of type {model.__name__}",
99
109
  )
100
110
  async def get_item(item_id: Any, session: SqlSessionDep): # type: ignore[name-defined]
101
- row = await service.get(session, item_id)
111
+ row = await service.get(session, _coerce_id(item_id))
102
112
  if not row:
103
113
  raise HTTPException(404, "Not found")
104
114
  return row
@@ -139,7 +149,7 @@ def make_crud_router_plus_sql(
139
149
  data = payload
140
150
  else:
141
151
  raise HTTPException(422, "invalid_payload")
142
- row = await service.update(session, item_id, data)
152
+ row = await service.update(session, _coerce_id(item_id), data)
143
153
  if not row:
144
154
  raise HTTPException(404, "Not found")
145
155
  return row
@@ -149,7 +159,7 @@ def make_crud_router_plus_sql(
149
159
  "/{item_id}", status_code=204, description=f"Delete item of type {model.__name__}"
150
160
  )
151
161
  async def delete_item(item_id: Any, session: SqlSessionDep): # type: ignore[name-defined]
152
- ok = await service.delete(session, item_id)
162
+ ok = await service.delete(session, _coerce_id(item_id))
153
163
  if not ok:
154
164
  raise HTTPException(404, "Not found")
155
165
  return
@@ -180,6 +190,25 @@ def make_tenant_crud_router_plus_sql(
180
190
  redirect_slashes=False,
181
191
  )
182
192
 
193
+ # Evaluate the base service once to preserve in-memory state across requests in tests/local.
194
+ # Consumers may pass either an instance or a zero-arg factory function.
195
+ try:
196
+ _base_instance = service_factory() if callable(service_factory) else service_factory # type: ignore[misc]
197
+ except TypeError:
198
+ # If the callable requires args, assume it's already an instance
199
+ _base_instance = service_factory # type: ignore[assignment]
200
+
201
+ def _coerce_id(v: Any) -> Any:
202
+ """Best-effort coercion of path ids: cast digit-only strings to int.
203
+ Keeps original type otherwise.
204
+ """
205
+ if isinstance(v, str) and v.isdigit():
206
+ try:
207
+ return int(v)
208
+ except Exception:
209
+ return v
210
+ return v
211
+
183
212
  def _parse_ordering_to_fields(order_spec: Optional[str]) -> list[str]:
184
213
  if not order_spec:
185
214
  return []
@@ -194,17 +223,8 @@ def make_tenant_crud_router_plus_sql(
194
223
 
195
224
  # create per-request service with tenant scoping
196
225
  async def _svc(session: SqlSessionDep, tenant_id: TenantId): # type: ignore[name-defined]
197
- base = service_factory # consumer-provided factory or instance
198
- svc = base # assume already a SqlService by default
199
- if callable(base):
200
- svc = base # the consumer likely closed over repo
201
- # if callable returns a service, call it now
202
- try:
203
- svc = base() # type: ignore[misc]
204
- except TypeError:
205
- svc = base # already instance
206
- if not isinstance(svc, TenantSqlService):
207
- svc = TenantSqlService(getattr(svc, "repo", svc), tenant_id=tenant_id, tenant_field=tenant_field) # type: ignore[arg-type]
226
+ repo_or_service = getattr(_base_instance, "repo", _base_instance)
227
+ svc: Any = TenantSqlService(repo_or_service, tenant_id=tenant_id, tenant_field=tenant_field) # type: ignore[arg-type]
208
228
  return svc # type: ignore[return-value]
209
229
 
210
230
  @router.get("", response_model=cast(Any, Page[Any]))
@@ -232,14 +252,12 @@ def make_tenant_crud_router_plus_sql(
232
252
  else:
233
253
  items = await svc.list(session, limit=lp.limit, offset=lp.offset, order_by=order_by)
234
254
  total = await svc.count(session)
235
- return Page[read_schema].from_items(
236
- total=total, items=items, limit=lp.limit, offset=lp.offset
237
- )
255
+ return Page[Any].from_items(total=total, items=items, limit=lp.limit, offset=lp.offset)
238
256
 
239
257
  @router.get("/{item_id}", response_model=cast(Any, Any))
240
258
  async def get_item(item_id: Any, session: SqlSessionDep, tenant_id: TenantId): # type: ignore[name-defined]
241
259
  svc = await _svc(session, tenant_id)
242
- row = await svc.get(session, item_id)
260
+ row = await svc.get(session, _coerce_id(item_id))
243
261
  if not row:
244
262
  raise HTTPException(404, "Not found")
245
263
  return row
@@ -273,7 +291,7 @@ def make_tenant_crud_router_plus_sql(
273
291
  data = payload
274
292
  else:
275
293
  raise HTTPException(422, "invalid_payload")
276
- row = await svc.update(session, item_id, data)
294
+ row = await svc.update(session, _coerce_id(item_id), data)
277
295
  if not row:
278
296
  raise HTTPException(404, "Not found")
279
297
  return row
@@ -281,7 +299,7 @@ def make_tenant_crud_router_plus_sql(
281
299
  @router.delete("/{item_id}", status_code=204)
282
300
  async def delete_item(item_id: Any, session: SqlSessionDep, tenant_id: TenantId): # type: ignore[name-defined]
283
301
  svc = await _svc(session, tenant_id)
284
- ok = await svc.delete(session, item_id)
302
+ ok = await svc.delete(session, _coerce_id(item_id))
285
303
  if not ok:
286
304
  raise HTTPException(404, "Not found")
287
305
  return
@@ -5,6 +5,8 @@ from pathlib import Path
5
5
  from typing import Optional
6
6
 
7
7
  from fastapi import FastAPI
8
+ from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
9
+ from fastapi.responses import HTMLResponse, JSONResponse
8
10
 
9
11
 
10
12
  def add_docs(
@@ -15,21 +17,41 @@ def add_docs(
15
17
  openapi_url: str = "/openapi.json",
16
18
  export_openapi_to: Optional[str] = None,
17
19
  ) -> None:
18
- """Enable docs endpoints and optionally export OpenAPI schema to disk on startup."""
19
- # Configure FastAPI docs URLs
20
- app.docs_url = swagger_url
21
- app.redoc_url = redoc_url
22
- app.openapi_url = openapi_url
20
+ """Enable docs endpoints and optionally export OpenAPI schema to disk on startup.
23
21
 
22
+ We mount docs and OpenAPI routes explicitly so this works even when configured post-init.
23
+ """
24
+
25
+ # OpenAPI JSON route
26
+ async def openapi_handler() -> JSONResponse: # noqa: ANN201
27
+ return JSONResponse(app.openapi())
28
+
29
+ app.add_api_route(openapi_url, openapi_handler, methods=["GET"], include_in_schema=False)
30
+
31
+ # Swagger UI route
32
+ async def swagger_ui() -> HTMLResponse: # noqa: ANN201
33
+ return get_swagger_ui_html(openapi_url=openapi_url, title="API Docs")
34
+
35
+ app.add_api_route(swagger_url, swagger_ui, methods=["GET"], include_in_schema=False)
36
+
37
+ # Redoc route
38
+ async def redoc_ui() -> HTMLResponse: # noqa: ANN201
39
+ return get_redoc_html(openapi_url=openapi_url, title="API ReDoc")
40
+
41
+ app.add_api_route(redoc_url, redoc_ui, methods=["GET"], include_in_schema=False)
42
+
43
+ # Optional export to disk on startup
24
44
  if export_openapi_to:
25
45
  export_path = Path(export_openapi_to)
26
46
 
27
- @app.on_event("startup")
28
- async def _export_spec() -> None: # noqa: ANN202
47
+ async def _export_docs() -> None:
48
+ # Startup export
29
49
  spec = app.openapi()
30
50
  export_path.parent.mkdir(parents=True, exist_ok=True)
31
51
  export_path.write_text(json.dumps(spec, indent=2))
32
52
 
53
+ app.add_event_handler("startup", _export_docs)
54
+
33
55
 
34
56
  def add_sdk_generation_stub(
35
57
  app: FastAPI,
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ from importlib import import_module
4
5
  from typing import List, Optional
5
6
 
6
7
  import typer
@@ -209,3 +210,28 @@ def register(app: typer.Typer) -> None:
209
210
  app.command("sql-stamp")(cmd_stamp)
210
211
  app.command("sql-merge-heads")(cmd_merge_heads)
211
212
  app.command("sql-setup-and-migrate")(cmd_setup_and_migrate)
213
+ app.command("sql-seed")(cmd_seed)
214
+
215
+
216
+ def _import_callable(path: str):
217
+ mod_name, _, fn_name = path.partition(":")
218
+ if not mod_name or not fn_name:
219
+ raise typer.BadParameter("Expected format 'module.path:callable'")
220
+ mod = import_module(mod_name)
221
+ fn = getattr(mod, fn_name, None)
222
+ if not callable(fn):
223
+ raise typer.BadParameter(f"Callable '{fn_name}' not found in module '{mod_name}'")
224
+ return fn
225
+
226
+
227
+ def cmd_seed(
228
+ target: str = typer.Argument(..., help="Seed callable path 'module:func'"),
229
+ database_url: Optional[str] = typer.Option(
230
+ None,
231
+ help="Database URL; overrides env for this command.",
232
+ ),
233
+ ):
234
+ """Run a user-provided seed function to load fixtures/reference data."""
235
+ apply_database_url(database_url)
236
+ fn = _import_callable(target)
237
+ fn()
svc_infra/data/add.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import inspect
3
4
  from typing import Callable, Iterable, Optional
4
5
 
5
6
  from fastapi import FastAPI
@@ -30,10 +31,9 @@ def add_data_lifecycle(
30
31
  and offers extension points. Jobs should be scheduled using svc_infra.jobs helpers.
31
32
  """
32
33
 
33
- @app.on_event("startup")
34
- async def _data_lifecycle_startup() -> None: # noqa: D401, ANN202
34
+ async def _run_lifecycle() -> None:
35
+ # Startup
35
36
  if auto_migrate:
36
- # Use existing CLI function to perform end-to-end setup and migrate.
37
37
  cmd_setup_and_migrate(
38
38
  database_url=database_url,
39
39
  overwrite_scaffold=False,
@@ -44,10 +44,12 @@ def add_data_lifecycle(
44
44
  discover_packages=discover_packages,
45
45
  with_payments=with_payments,
46
46
  )
47
-
48
47
  if on_load_fixtures:
49
- # Run user-provided fixture loader (idempotent expected).
50
- on_load_fixtures()
48
+ res = on_load_fixtures()
49
+ if inspect.isawaitable(res):
50
+ await res # type: ignore[misc]
51
+
52
+ app.add_event_handler("startup", _run_lifecycle)
51
53
 
52
54
  # Store optional jobs on app.state for external schedulers to discover/register.
53
55
  if retention_jobs is not None:
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+ from typing import Callable, Optional
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class BackupHealthReport:
10
+ ok: bool
11
+ last_success: Optional[datetime]
12
+ retention_days: Optional[int]
13
+ message: str = ""
14
+
15
+
16
+ def verify_backups(
17
+ *, last_success: Optional[datetime] = None, retention_days: Optional[int] = None
18
+ ) -> BackupHealthReport:
19
+ """Return a basic backup health report.
20
+
21
+ In production, callers should plug a provider-specific checker and translate into this report.
22
+ """
23
+ if last_success is None:
24
+ return BackupHealthReport(
25
+ ok=False, last_success=None, retention_days=retention_days, message="no_backup_seen"
26
+ )
27
+ now = datetime.now(timezone.utc)
28
+ age_days = (now - last_success).total_seconds() / 86400.0
29
+ ok = retention_days is None or age_days <= max(1, retention_days)
30
+ return BackupHealthReport(ok=ok, last_success=last_success, retention_days=retention_days)
31
+
32
+
33
+ __all__ = ["BackupHealthReport", "verify_backups"]
34
+
35
+
36
+ def make_backup_verification_job(
37
+ checker: Callable[[], BackupHealthReport],
38
+ *,
39
+ on_report: Optional[Callable[[BackupHealthReport], None]] = None,
40
+ ):
41
+ """Return a callable suitable for scheduling in a job runner.
42
+
43
+ The checker should perform provider-specific checks and return a BackupHealthReport.
44
+ If on_report is provided, it will be invoked with the report.
45
+ """
46
+
47
+ def _job() -> BackupHealthReport:
48
+ rep = checker()
49
+ if on_report:
50
+ on_report(rep)
51
+ return rep
52
+
53
+ return _job
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Awaitable, Callable, Iterable, Optional, Protocol
5
+
6
+
7
+ class SqlSession(Protocol): # minimal protocol for tests/integration
8
+ async def execute(self, stmt: Any) -> Any:
9
+ pass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ErasureStep:
14
+ name: str
15
+ run: Callable[[SqlSession, str], Awaitable[int] | int]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ErasurePlan:
20
+ steps: Iterable[ErasureStep]
21
+
22
+
23
+ async def run_erasure(
24
+ session: SqlSession,
25
+ principal_id: str,
26
+ plan: ErasurePlan,
27
+ *,
28
+ on_audit: Optional[Callable[[str, dict[str, Any]], None]] = None,
29
+ ) -> int:
30
+ """Run an erasure plan and optionally emit an audit event.
31
+
32
+ Returns total affected rows across steps.
33
+ """
34
+ total = 0
35
+ for s in plan.steps:
36
+ res = s.run(session, principal_id)
37
+ if hasattr(res, "__await__"):
38
+ res = await res # type: ignore[misc]
39
+ total += int(res or 0)
40
+ if on_audit:
41
+ on_audit("erasure.completed", {"principal_id": principal_id, "affected": total})
42
+ return total
43
+
44
+
45
+ __all__ = ["ErasureStep", "ErasurePlan", "run_erasure"]
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from pathlib import Path
5
+ from typing import Awaitable, Callable, Iterable, Optional
6
+
7
+
8
+ async def run_fixtures(
9
+ loaders: Iterable[Callable[[], None | Awaitable[None]]], *, run_once_file: Optional[str] = None
10
+ ) -> None:
11
+ """Run a sequence of fixture loaders (sync or async).
12
+
13
+ - If run_once_file is provided and exists, does nothing.
14
+ - On success, creates the run_once_file sentinel (parent dirs included).
15
+ """
16
+ if run_once_file:
17
+ sentinel = Path(run_once_file)
18
+ if sentinel.exists():
19
+ return
20
+ for fn in loaders:
21
+ res = fn()
22
+ if inspect.isawaitable(res): # type: ignore[arg-type]
23
+ await res # type: ignore[misc]
24
+ if run_once_file:
25
+ sentinel.parent.mkdir(parents=True, exist_ok=True)
26
+ Path(run_once_file).write_text("ok")
27
+
28
+
29
+ def make_on_load_fixtures(
30
+ *loaders: Callable[[], None | Awaitable[None]], run_once_file: Optional[str] = None
31
+ ) -> Callable[[], Awaitable[None]]:
32
+ """Return an async callable suitable for add_data_lifecycle(on_load_fixtures=...)."""
33
+
34
+ async def _runner() -> None:
35
+ await run_fixtures(loaders, run_once_file=run_once_file)
36
+
37
+ return _runner
38
+
39
+
40
+ __all__ = ["run_fixtures", "make_on_load_fixtures"]
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timedelta, timezone
5
+ from typing import Any, Iterable, Optional, Protocol, Sequence
6
+
7
+
8
+ class SqlSession(Protocol): # minimal protocol for tests/integration
9
+ async def execute(self, stmt: Any) -> Any:
10
+ pass
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class RetentionPolicy:
15
+ name: str
16
+ model: Any # SQLAlchemy model or test double exposing columns
17
+ older_than_days: int
18
+ soft_delete_field: Optional[str] = "deleted_at"
19
+ extra_where: Optional[Sequence[Any]] = None
20
+ hard_delete: bool = False
21
+
22
+
23
+ async def purge_policy(session: SqlSession, policy: RetentionPolicy) -> int:
24
+ """Execute a single retention purge according to policy.
25
+
26
+ If hard_delete is False and soft_delete_field exists on model, set timestamp; else DELETE.
27
+ Returns number of affected rows (best-effort; test doubles may return an int directly).
28
+ """
29
+ cutoff = datetime.now(timezone.utc) - timedelta(days=policy.older_than_days)
30
+ m = policy.model
31
+ where = list(policy.extra_where or [])
32
+ created_col = getattr(m, "created_at", None)
33
+ if created_col is not None and hasattr(created_col, "__le__"):
34
+ where.append(created_col <= cutoff) # type: ignore[operator]
35
+
36
+ # Soft-delete path when available and requested
37
+ if not policy.hard_delete and policy.soft_delete_field and hasattr(m, policy.soft_delete_field):
38
+ stmt = m.update().where(*where).values({policy.soft_delete_field: cutoff}) # type: ignore[attr-defined]
39
+ res = await session.execute(stmt)
40
+ return getattr(res, "rowcount", 0)
41
+
42
+ # Hard delete fallback
43
+ stmt = m.delete().where(*where) # type: ignore[attr-defined]
44
+ res = await session.execute(stmt)
45
+ return getattr(res, "rowcount", 0)
46
+
47
+
48
+ async def run_retention_purge(session: SqlSession, policies: Iterable[RetentionPolicy]) -> int:
49
+ total = 0
50
+ for p in policies:
51
+ total += await purge_policy(session, p)
52
+ return total
53
+
54
+
55
+ __all__ = ["RetentionPolicy", "purge_policy", "run_retention_purge"]
@@ -124,9 +124,10 @@ class SqlRepository:
124
124
  return False
125
125
  if self.soft_delete:
126
126
  # Prefer timestamp, also optionally set flag to False
127
- if hasattr(self.model, self.soft_delete_field):
127
+ # Check attributes on the instance to support test doubles without class-level fields
128
+ if hasattr(obj, self.soft_delete_field):
128
129
  setattr(obj, self.soft_delete_field, func.now())
129
- if self.soft_delete_flag_field and hasattr(self.model, self.soft_delete_flag_field):
130
+ if self.soft_delete_flag_field and hasattr(obj, self.soft_delete_flag_field):
130
131
  setattr(obj, self.soft_delete_flag_field, False)
131
132
  await session.flush()
132
133
  return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.602
3
+ Version: 0.1.603
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
@@ -93,6 +93,7 @@ svc-infra packages the shared building blocks we use to ship production FastAPI
93
93
  | Observability | Prometheus middleware, Grafana automation, OTEL hooks | [Observability guide](docs/observability.md) |
94
94
  | Webhooks | Subscription store, signing, retry worker | [Webhooks framework](docs/webhooks.md) |
95
95
  | Security | Password policy, lockout, signed cookies, headers | [Security hardening](docs/security.md) |
96
+ | Data Lifecycle | Fixtures, retention, erasure, backups | [Data lifecycle](docs/data-lifecycle.md) |
96
97
 
97
98
  ## Minimal FastAPI bootstrap
98
99
 
@@ -50,13 +50,13 @@ svc_infra/api/fastapi/db/nosql/mongo/health.py,sha256=OM4_Pig3IRSrBhz2yHweIhGlSB
50
50
  svc_infra/api/fastapi/db/sql/README.md,sha256=uF_k4wbNSeWR6JF_8evWkeSFidBEXMXcIiYyR6sEv48,3082
51
51
  svc_infra/api/fastapi/db/sql/__init__.py,sha256=R9P1Vy2Uqf9gFISxChMhO9tOGciIjEymVPhpXmsY3zc,255
52
52
  svc_infra/api/fastapi/db/sql/add.py,sha256=xGZnbGnP9PQtWvr5vuXA3_PXzTKldPM_cD3L0uztAvo,4768
53
- svc_infra/api/fastapi/db/sql/crud_router.py,sha256=t4SYCYfzLIAnCXFfR2xDmIHjAI8septLd_wUBKFF1Xg,10816
53
+ svc_infra/api/fastapi/db/sql/crud_router.py,sha256=H7212YK4BsmuuS5E-HWKnRXzgc4RQxVSFWT6wzb4IXQ,11575
54
54
  svc_infra/api/fastapi/db/sql/health.py,sha256=ELLgQerooHHnvZRhGueSAc4QJsb3C4RojUGIu_U-hA4,792
55
55
  svc_infra/api/fastapi/db/sql/session.py,sha256=DUBqKTRJAX4fqRz9B-w9eD9SpzZ8EUS862-GsjCL3ts,1869
56
56
  svc_infra/api/fastapi/db/sql/users.py,sha256=68HGJgYVTEjKJm4-DPPC8-6nwXJoCukmgrYIIOHEUjs,5346
57
57
  svc_infra/api/fastapi/dependencies/ratelimit.py,sha256=DiOC-MJfqTtSydM6RAaeAsiXXL_6oZQoBLvRSpdWzs4,3794
58
58
  svc_infra/api/fastapi/docs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
- svc_infra/api/fastapi/docs/add.py,sha256=qbGO6GHW7tXH34njnGjqQM8Php_jT3iLVe_yxDPmkpU,1671
59
+ svc_infra/api/fastapi/docs/add.py,sha256=Ytk4iqWrNqPg3-SELJ5OUNuTieltpTBmIFOPKGnHVag,2563
60
60
  svc_infra/api/fastapi/docs/landing.py,sha256=5JqJYCxQDCWy-BeDLfkv7OBlzWQKSGWUCYXQ51hojG8,4627
61
61
  svc_infra/api/fastapi/docs/scoped.py,sha256=AuN35Op-9fUvHQCLOBtRjd5eWSpB5C9EAW_7-Boxmfo,7540
62
62
  svc_infra/api/fastapi/dual/__init__.py,sha256=scHLcNFkGbgX_R21V702xnAv6GMCkQ4n7yUtNDNgliM,552
@@ -133,7 +133,7 @@ svc_infra/cli/cmds/db/nosql/mongo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeR
133
133
  svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py,sha256=83_I0-63aRyR2uRLhpG1DKavH8BJ6fwdL3qpCpksyBU,6109
134
134
  svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py,sha256=gsv7V3eGxyhQQm4J8IPYV9xQkdv0DoX7txcPLgbiejk,4277
135
135
  svc_infra/cli/cmds/db/sql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
136
- svc_infra/cli/cmds/db/sql/alembic_cmds.py,sha256=KjumtKSOZR1UxbpZUuqllpknerDLNcY-0kqqqxiOnL4,7664
136
+ svc_infra/cli/cmds/db/sql/alembic_cmds.py,sha256=HfnLsLJMug6llfitiwUvwdrXzcjRqrn-BVyJNcQDLFA,8519
137
137
  svc_infra/cli/cmds/db/sql/sql_export_cmds.py,sha256=6MxoQO-9upoXg0cl1RHIqz96yXFVGidiBYp_ewhB0E0,2700
138
138
  svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py,sha256=eNTCqHXOxgl9H3WTbGVn9BHXYwCpjIEJsDqhEFdrYMM,4613
139
139
  svc_infra/cli/cmds/help.py,sha256=wGfZFMYaR2ZPwW2JwKDU7M3m4AtdCd8GRQ412AmEBUM,758
@@ -144,7 +144,11 @@ svc_infra/cli/cmds/obs/obs_cmds.py,sha256=fltUZu5fcnZdl0_JPJBIxIaA1Xqpw1BXE-SWBP
144
144
  svc_infra/cli/foundation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
145
145
  svc_infra/cli/foundation/runner.py,sha256=RbfjKwb3aHk1Y0MYU8xMpKRpIqRVMVr8GuL2EDZ6n38,1862
146
146
  svc_infra/cli/foundation/typer_bootstrap.py,sha256=KapgH1R-qON9FuYH1KYlVx_5sJvjmAGl25pB61XCpm4,985
147
- svc_infra/data/add.py,sha256=GbTdPDDsHg9ahQuMlLn4M_YoBFcc16Mf20FeWv1p1sU,2252
147
+ svc_infra/data/add.py,sha256=IG_1kMthOcYwpBYfZEGr90XE5Z96z2TEnl1hs5w0jAs,2222
148
+ svc_infra/data/backup.py,sha256=LZcfw7aQGge0I0PG8P1dQWkgFHTTPKc5iXnsvf9oAUA,1619
149
+ svc_infra/data/erasure.py,sha256=VG4XKCE9XMc63qHW7jd-mivwzeITOJe-Ru2X1-7bE_I,1155
150
+ svc_infra/data/fixtures.py,sha256=2WKU6-HCKfA_uiwjI0cB_79F-aCbqoo3Ro05ksGbH3Y,1278
151
+ svc_infra/data/retention.py,sha256=8K_lG4vVLl5BaLVw3S2bm2Vu88oTZOpUL9_ifGtes6I,2062
148
152
  svc_infra/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
149
153
  svc_infra/db/crud_schema.py,sha256=-fv-Om1lHVt6lcNbie6A2kRcPex4SDByUPfks6SpmUc,2521
150
154
  svc_infra/db/inbox.py,sha256=drxLRLHaMRrCDgo_8wj12do80wDh5ssHV6LGkaM98no,1996
@@ -178,7 +182,7 @@ svc_infra/db/sql/base.py,sha256=yeOM4Xo2CtTzsfx0hN8KLFTgOPZp6BrbV7Omz_bZUMg,317
178
182
  svc_infra/db/sql/constants.py,sha256=FqtoBWqP13gW8avIWpr8E41PDW5aQ88tjGmnokCV9Lk,1656
179
183
  svc_infra/db/sql/core.py,sha256=2W7o1uyEZBDbqX1ptArP5Fa5obaVpZqHpczJCdf1NZk,10855
180
184
  svc_infra/db/sql/management.py,sha256=bVpzj8BapwcghxAwmBsTnntWf3kaNf7CYETndppaiUM,3620
181
- svc_infra/db/sql/repository.py,sha256=SG3cBf0BUnfds47BuOWjZJiFtrd2SmRkMINGioUhVn0,7047
185
+ svc_infra/db/sql/repository.py,sha256=B80mqW0Hjv1HmbnnDqHDliOHKSusRFoCtHiuaBC_9M0,7131
182
186
  svc_infra/db/sql/resource.py,sha256=CfBmRvMq_1-cgYBS9uc_G4i1W5e6wk6zo4MGdKTPT9I,1194
183
187
  svc_infra/db/sql/scaffold.py,sha256=aRC572W9Jqs1nLprR_zmmgERthMlJwoUjQ_xbRh53RU,9107
184
188
  svc_infra/db/sql/service.py,sha256=Jtb89FQgXWNRrh_bPLCvNyM7Up0_rn_wx1_CGWfiqNs,2790
@@ -278,7 +282,7 @@ svc_infra/webhooks/fastapi.py,sha256=BCNvGNxukf6dC2a4i-6en-PrjBGV19YvCWOot5lXWsA
278
282
  svc_infra/webhooks/router.py,sha256=6JvAVPMEth_xxHX-IsIOcyMgHX7g1H0OVxVXKLuMp9w,1596
279
283
  svc_infra/webhooks/service.py,sha256=hWgiJRXKBwKunJOx91C7EcLUkotDtD3Xp0RT6vj2IC0,1797
280
284
  svc_infra/webhooks/signing.py,sha256=NCwdZzmravUe7HVIK_uXK0qqf12FG-_MVsgPvOw6lsM,784
281
- svc_infra-0.1.602.dist-info/METADATA,sha256=GarD5h35kMI5JWTb1QpqupgtKdpT8GIsv71yTKOEdgA,7839
282
- svc_infra-0.1.602.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
283
- svc_infra-0.1.602.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
284
- svc_infra-0.1.602.dist-info/RECORD,,
285
+ svc_infra-0.1.603.dist-info/METADATA,sha256=Y_24yijEGFoG0M5wtJE7EjMaf5BMobwLNwxJoR4ACtk,7941
286
+ svc_infra-0.1.603.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
287
+ svc_infra-0.1.603.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
288
+ svc_infra-0.1.603.dist-info/RECORD,,