csrd-utils 0.3.54__tar.gz → 0.3.56__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 (87) hide show
  1. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/PKG-INFO +1 -1
  2. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/pyproject.toml +1 -1
  3. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/__main__.py +109 -5
  4. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/generator.py +14 -0
  5. csrd_utils-0.3.56/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/acceptance/__init__.py +0 -0
  6. csrd_utils-0.3.56/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/__init__.py +0 -0
  7. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/.gitignore +0 -0
  8. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/README.md +0 -0
  9. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/__init__.py +0 -0
  10. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/audit.py +0 -0
  11. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/augmentor.py +0 -0
  12. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/cluster.py +0 -0
  13. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/doctor.py +0 -0
  14. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
  15. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
  16. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/caching/manifest.yaml +0 -0
  17. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/caching/requirements.fragment +0 -0
  18. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
  19. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
  20. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
  21. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
  22. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/logging/manifest.yaml +0 -0
  23. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/logging/requirements.fragment +0 -0
  24. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
  25. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
  26. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
  27. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
  28. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
  29. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
  30. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
  31. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
  32. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
  33. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
  34. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
  35. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
  36. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
  37. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
  38. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
  39. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
  40. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
  41. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
  42. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/workers/manifest.yaml +0 -0
  43. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/workers/requirements.fragment +0 -0
  44. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
  45. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
  46. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
  47. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
  48. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
  49. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/fragments.py +0 -0
  50. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/resources.py +0 -0
  51. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
  52. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
  53. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
  54. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.cookiecutterignore +0 -0
  55. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
  56. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  57. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  58. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  59. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/cache.py +0 -0
  60. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/__init__.py +0 -0
  61. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/upstreams.py +0 -0
  62. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
  63. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/delegates.py +0 -0
  64. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/item_repository.py +0 -0
  65. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/worker_broker.py +0 -0
  66. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/docker-compose.yml +0 -0
  67. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/__init__.py +0 -0
  68. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/logging.py +0 -0
  69. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
  70. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/repositories/item_repository.py +0 -0
  71. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  72. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  73. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/__init__.py +0 -0
  74. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/acceptance/test_logging_middleware.py +0 -0
  75. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/conftest.py +0 -0
  76. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/test_items.py +0 -0
  77. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_cache.py +0 -0
  78. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_database_settings.py +0 -0
  79. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_delegates.py +0 -0
  80. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_settings_contracts.py +0 -0
  81. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_worker_broker.py +0 -0
  82. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
  83. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +0 -0
  84. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +0 -0
  85. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/items_view.py +0 -0
  86. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/workers/__init__.py +0 -0
  87. {csrd_utils-0.3.54 → csrd_utils-0.3.56}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/workers/tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csrd-utils
3
- Version: 0.3.54
3
+ Version: 0.3.56
4
4
  Summary: CLI utilities for csrd service generation and feature augmentation
5
5
  Project-URL: Repository, https://github.com/csrd-api/fastapi-common
6
6
  Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/utils
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "csrd-utils"
3
- version = "0.3.54"
3
+ version = "0.3.56"
4
4
  description = "CLI utilities for csrd service generation and feature augmentation"
5
5
  license = { text = "MIT" }
6
6
  requires-python = ">=3.12"
@@ -213,12 +213,19 @@ def _collect_new_service_config(args: argparse.Namespace) -> GenerateServiceArgs
213
213
  has_database = args.database != "none"
214
214
  selected_database = "sqlite" if not has_database else args.database
215
215
  normalized_name = _normalize_service_name(args.name)
216
+ # Auto-advance port when default (8080) is already taken in the workspace spec.
217
+ output_root = args.output.resolve()
218
+ effective_port = (
219
+ _next_free_port(output_root, args.port)
220
+ if _is_workspace(output_root) and args.port == 8080
221
+ else args.port
222
+ )
216
223
  return GenerateServiceArgs(
217
224
  {
218
225
  "name": normalized_name,
219
226
  "output_dir": args.output,
220
227
  "description": args.description,
221
- "port": args.port,
228
+ "port": effective_port,
222
229
  "model_name": args.model_name,
223
230
  "model_name_plural": args.model_name_plural,
224
231
  "has_database": has_database,
@@ -242,7 +249,15 @@ def _collect_new_service_config(args: argparse.Namespace) -> GenerateServiceArgs
242
249
  else:
243
250
  output_dir = Path(_prompt_text("Output directory", str(args.output)))
244
251
  description = _prompt_text("Service description", args.description)
245
- port = _prompt_int("HTTP port", args.port)
252
+
253
+ # Suggest the next available port based on what's already in the workspace spec.
254
+ workspace_root_candidate = output_dir.resolve()
255
+ default_port = (
256
+ _next_free_port(workspace_root_candidate, args.port)
257
+ if _is_workspace(workspace_root_candidate)
258
+ else args.port
259
+ )
260
+ port = _prompt_int("HTTP port", default_port)
246
261
  model_name = _prompt_text("Model name", args.model_name)
247
262
 
248
263
  plural_default = args.model_name_plural
@@ -331,7 +346,9 @@ def _collect_new_cluster_config(args: argparse.Namespace) -> GenerateClusterArgs
331
346
  )
332
347
 
333
348
  print("Interactive cluster mode: press Enter to accept defaults.")
334
- cluster_name = _prompt_text("Cluster name", args.name or "my-cluster")
349
+ cluster_name = (
350
+ _prompt_text("Cluster name", args.name or "my-cluster") if not args.name else args.name
351
+ )
335
352
  preset = _prompt_choice("Preset", list(PRESETS.keys()), args.preset)
336
353
  services: list[ServiceSpec] = []
337
354
 
@@ -421,6 +438,70 @@ def _create_workspace(config: GenerateWorkspaceArgs) -> Path:
421
438
  return workspace_root
422
439
 
423
440
 
441
+ def _generate_services_from_cluster_spec(
442
+ spec: ClusterSpec, workspace_root: Path, force: bool = False
443
+ ) -> list[Path]:
444
+ """Scaffold source trees for every service in a cluster spec that lacks one.
445
+
446
+ Derives generate_service arguments from each ServiceSpec so the resulting
447
+ workspace has src/<service>/ code alongside the compose wiring.
448
+ """
449
+
450
+ normalized = normalize_cluster_spec(spec)
451
+ services = normalized["services"]
452
+ port_map = {str(svc["name"]): int(svc["port"]) for svc in services}
453
+
454
+ generated: list[Path] = []
455
+ for service in services:
456
+ name = str(service["name"])
457
+ service_type = str(service["type"])
458
+ port = int(service["port"])
459
+ features = list(service.get("features", []))
460
+ delegates_list = list(service.get("delegates", []))
461
+ broker = str(service.get("broker", "redis"))
462
+
463
+ # Derive upstreams from delegate targets + their ports
464
+ upstreams = ",".join(f"http://{t}:{port_map[t]}" for t in delegates_list if t in port_map)
465
+
466
+ has_database = service_type == "db"
467
+ has_delegates = bool(delegates_list) or service_type == "delegate"
468
+ has_workers = service_type == "worker" or "workers" in features
469
+ has_caching = "caching" in features
470
+ has_structured_logging = "logging" in features
471
+ database = str(service.get("database", "sqlite")) if has_database else "sqlite"
472
+
473
+ src_dir = workspace_root / "src" / name.replace("-", "_")
474
+ if src_dir.exists() and any(src_dir.iterdir()) and not force:
475
+ print(f"Skipped existing service: {name}")
476
+ continue
477
+
478
+ try:
479
+ result = generate_service(
480
+ name=name,
481
+ output_dir=workspace_root,
482
+ description=f"{name} service",
483
+ port=port,
484
+ model_name="Item",
485
+ model_name_plural="items",
486
+ has_database=has_database,
487
+ database=database,
488
+ has_delegates=has_delegates,
489
+ upstreams=upstreams,
490
+ has_workers=has_workers,
491
+ broker=broker,
492
+ has_caching=has_caching,
493
+ cache_backend="redis",
494
+ has_structured_logging=has_structured_logging,
495
+ overwrite_if_exists=force,
496
+ )
497
+ generated.append(result)
498
+ print(f"Generated service: {name}")
499
+ except Exception as exc:
500
+ print(f"WARN: could not scaffold service '{name}': {exc}")
501
+
502
+ return generated
503
+
504
+
424
505
  def _default_service_namespace(output_dir: Path) -> argparse.Namespace:
425
506
  return argparse.Namespace(
426
507
  interactive=True,
@@ -446,7 +527,7 @@ def _default_service_namespace(output_dir: Path) -> argparse.Namespace:
446
527
  def _default_cluster_namespace(output_dir: Path) -> argparse.Namespace:
447
528
  return argparse.Namespace(
448
529
  spec=None,
449
- name=None,
530
+ name=output_dir.name, # Default cluster name to workspace folder name
450
531
  output=output_dir,
451
532
  preset="none",
452
533
  force=False,
@@ -461,6 +542,22 @@ def _is_workspace(path: Path) -> bool:
461
542
  return (path / WORKSPACE_MARKER).is_file()
462
543
 
463
544
 
545
+ def _next_free_port(workspace_root: Path, start: int = 8080) -> int:
546
+ """Return the lowest port >= start that is not already used in the workspace cluster spec."""
547
+ spec_path = workspace_root / WORKSPACE_CLUSTER_SPEC
548
+ if not spec_path.is_file():
549
+ return start
550
+ try:
551
+ raw = yaml.safe_load(spec_path.read_text(encoding="utf-8")) or {}
552
+ used = {int(svc["port"]) for svc in raw.get("services", []) if "port" in svc}
553
+ except Exception:
554
+ return start
555
+ port = start
556
+ while port in used:
557
+ port += 1
558
+ return port
559
+
560
+
464
561
  def _service_is_interactive(args: argparse.Namespace) -> bool:
465
562
  explicit_interactive = getattr(args, "interactive", None)
466
563
  return bool(explicit_interactive) if explicit_interactive is not None else args.name is None
@@ -1262,7 +1359,14 @@ def main() -> int:
1262
1359
  cluster_config["output_dir"],
1263
1360
  force=cluster_config["overwrite_if_exists"],
1264
1361
  )
1265
- print(f"Generated cluster at: {generated_cluster}")
1362
+ _save_workspace_cluster_spec(workspace_root, cluster_config["spec"])
1363
+ print(f"Generated cluster scaffold at: {generated_cluster}")
1364
+ scaffolded = _generate_services_from_cluster_spec(
1365
+ cluster_config["spec"],
1366
+ workspace_root,
1367
+ force=cluster_config["overwrite_if_exists"],
1368
+ )
1369
+ print(f"Scaffolded {len(scaffolded)} service(s) under src/.")
1266
1370
  except (FileExistsError, FileNotFoundError, ValueError) as exc:
1267
1371
  print(f"ERROR: {exc}")
1268
1372
  return 1
@@ -49,6 +49,7 @@ dev = [
49
49
  asyncio_mode = "auto"
50
50
  testpaths = ["tests"]
51
51
  pythonpath = ["src"]
52
+ consider_namespace_packages = true
52
53
 
53
54
  [tool.mypy]
54
55
  python_version = "3.12"
@@ -289,6 +290,19 @@ def _move_tests_to_workspace_root(
289
290
  unit_dst.mkdir(parents=True, exist_ok=True)
290
291
  acceptance_dst.mkdir(parents=True, exist_ok=True)
291
292
 
293
+ # Ensure every test directory level has an __init__.py so pytest uses
294
+ # package-based import paths and avoids module-name collisions across services.
295
+ def _ensure_init(dir_path: Path) -> None:
296
+ init = dir_path / "__init__.py"
297
+ if not init.exists():
298
+ init.write_text("", encoding="utf-8")
299
+
300
+ _ensure_init(tests_root)
301
+ _ensure_init(tests_root / "unit")
302
+ _ensure_init(tests_root / "acceptance")
303
+ _ensure_init(unit_dst)
304
+ _ensure_init(acceptance_dst)
305
+
292
306
  def _copy_tree(src_dir: Path, dst_dir: Path) -> None:
293
307
  if not src_dir.is_dir():
294
308
  return
File without changes
File without changes