scaffold-ca-python 0.2.1__py3-none-any.whl → 0.3.0__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.
@@ -16,12 +16,12 @@ from scaffold_ca_python.commands._shared import (
16
16
  )
17
17
  from scaffold_ca_python.core.module_builder import ModuleBuilder
18
18
  from scaffold_ca_python.core.name_utils import validate_module_name
19
- from scaffold_ca_python.core.project_detector import load_project_context
19
+ from scaffold_ca_python.core.project_detector import load_project_context, resolve_tests_root
20
20
  from scaffold_ca_python.factory.simple.model_factory import ModelFactory
21
21
  from scaffold_ca_python.models.context import ModuleContext
22
22
  from scaffold_ca_python.models.layer import Layer
23
23
 
24
- _GM_HELP = "Scaffold a Pydantic v2 domain model and test stub. Alias 'gm'."
24
+ _GM_HELP = "Scaffold a Pydantic v2 domain model, its gateway, and test stubs. Alias 'gm'."
25
25
  _GM_EPILOG = "Example:\n\n * scaffold gm --name order\n\n * scaffold generate-model --name order\n\n"
26
26
 
27
27
 
@@ -31,12 +31,24 @@ def _generate_model_impl(name: str, dry_run: bool) -> None:
31
31
  project_ctx = load_project_context(project_root)
32
32
  module_ctx = ModuleContext(name=name, layer=Layer.DOMAIN_MODEL, project=project_ctx)
33
33
 
34
- src_path = project_root / "src" / project_ctx.python_package / "domain" / "model" / f"{module_ctx.module_name}.py"
34
+ model_root = project_root / "src" / project_ctx.python_package / "domain" / "model"
35
35
  ensure_absent(
36
- src_path,
36
+ model_root / module_ctx.module_name,
37
+ project_root,
38
+ kind="Directory",
39
+ hint="Choose a different name or remove the existing folder first.",
40
+ )
41
+ ensure_absent(
42
+ model_root / f"{module_ctx.module_name}.py",
37
43
  project_root,
38
44
  kind="File",
39
- hint="Choose a different name or remove the existing file first.",
45
+ hint="A model with this name already exists in the legacy flat layout. Remove it or choose a different name.",
46
+ )
47
+ ensure_absent(
48
+ resolve_tests_root(project_root) / "domain" / "model" / module_ctx.module_name,
49
+ project_root,
50
+ kind="Directory",
51
+ hint="Remove the leftover tests folder or choose a different name.",
40
52
  )
41
53
 
42
54
  builder = ModuleBuilder(project_root=project_root, project_ctx=project_ctx, module_ctx=module_ctx, dry_run=dry_run)
@@ -70,7 +82,7 @@ def register(app: typer.Typer) -> None:
70
82
  ),
71
83
  ] = False,
72
84
  ) -> None:
73
- """Scaffold a domain model in domain/model/."""
85
+ """Scaffold a domain model package with its gateway in domain/model/<name>/."""
74
86
  if name is None:
75
87
  typer.echo(ctx.get_help())
76
88
  raise typer.Exit(0)
@@ -10,7 +10,8 @@ from scaffold_ca_python.core.project_detector import resolve_tests_root
10
10
  def find_module_targets(project_root: Path, pkg: str, snake: str) -> list[tuple[Path, Path | None]]:
11
11
  """Return ``(src_path, test_path | None)`` pairs for every match of *snake*.
12
12
 
13
- Searches in order: domain/model, domain/usecase, driven_adapters,
13
+ Searches in order: domain/model/<snake>/ (package), domain/model/<snake>.py (legacy flat),
14
+ domain/usecase, driven_adapters,
14
15
  entry_points, helpers.
15
16
  """
16
17
  src_root = project_root / "src" / pkg
@@ -18,6 +19,11 @@ def find_module_targets(project_root: Path, pkg: str, snake: str) -> list[tuple[
18
19
 
19
20
  candidates: list[tuple[Path, Path | None]] = []
20
21
 
22
+ model_pkg = src_root / "domain" / "model" / snake
23
+ if model_pkg.is_dir():
24
+ model_pkg_test = tests_root / "domain" / "model" / snake
25
+ candidates.append((model_pkg, model_pkg_test if model_pkg_test.exists() else None))
26
+
21
27
  for src_rel, test_rel in [
22
28
  (src_root / "domain" / "model" / f"{snake}.py", tests_root / "domain" / "model" / f"test_{snake}.py"),
23
29
  (src_root / "domain" / "usecase" / f"{snake}.py", tests_root / "domain" / "usecase" / f"test_{snake}.py"),
@@ -6,8 +6,8 @@ import shutil
6
6
  from pathlib import Path
7
7
  from typing import TYPE_CHECKING
8
8
 
9
+ from scaffold_ca_python.core.module_locator import find_module_targets
9
10
  from scaffold_ca_python.core.name_utils import to_snake_case
10
- from scaffold_ca_python.core.project_detector import resolve_tests_root
11
11
  from scaffold_ca_python.factory import ModuleFactory
12
12
 
13
13
  if TYPE_CHECKING:
@@ -21,7 +21,7 @@ class DeleteModuleFactory(ModuleFactory):
21
21
  """Build deletion operations for a module.
22
22
 
23
23
  Finds and deletes modules across:
24
- - domain/model/<snake>.py
24
+ - domain/model/<snake>/ (package) or domain/model/<snake>.py (legacy)
25
25
  - domain/usecase/<snake>.py
26
26
  - infrastructure/driven_adapters/<snake>/
27
27
  - infrastructure/entry_points/<snake>/
@@ -34,41 +34,7 @@ class DeleteModuleFactory(ModuleFactory):
34
34
  module_name = builder.module_ctx.name if builder.module_ctx else "Module"
35
35
  snake_name = to_snake_case(module_name)
36
36
 
37
- src_root = project_root / "src" / pkg
38
- tests_root = resolve_tests_root(project_root)
39
-
40
- # Build list of possible module locations
41
- candidates: list[tuple[Path, Path | None]] = []
42
-
43
- # Single-file layers (domain/model, domain/usecase)
44
- model_src = src_root / "domain" / "model" / f"{snake_name}.py"
45
- model_test = tests_root / "domain" / "model" / f"test_{snake_name}.py"
46
- usecase_src = src_root / "domain" / "usecase" / f"{snake_name}.py"
47
- usecase_test = tests_root / "domain" / "usecase" / f"test_{snake_name}.py"
48
- for src_rel, test_rel in [
49
- (model_src, model_test),
50
- (usecase_src, usecase_test),
51
- ]:
52
- if src_rel.exists():
53
- candidates.append((src_rel, test_rel if test_rel.exists() else None))
54
-
55
- # Directory-based layers (infrastructure sub-dirs)
56
- for src_rel, test_rel in [
57
- (
58
- src_root / "infrastructure" / "driven_adapters" / snake_name,
59
- tests_root / "infrastructure" / "driven_adapters" / snake_name,
60
- ),
61
- (
62
- src_root / "infrastructure" / "entry_points" / snake_name,
63
- tests_root / "infrastructure" / "entry_points" / snake_name,
64
- ),
65
- (
66
- src_root / "infrastructure" / "helpers" / snake_name,
67
- tests_root / "infrastructure" / "helpers" / snake_name,
68
- ),
69
- ]:
70
- if src_rel.exists():
71
- candidates.append((src_rel, test_rel if test_rel.exists() else None))
37
+ candidates = find_module_targets(project_root, pkg, snake_name)
72
38
 
73
39
  if not builder.dry_run:
74
40
  for src_path, test_path in candidates:
@@ -15,43 +15,48 @@ class ModelFactory(ModuleFactory):
15
15
  """Factory for domain model generation."""
16
16
 
17
17
  def build(self, builder: ModuleBuilder) -> None:
18
- """Build domain model files.
18
+ """Build domain model package files.
19
19
 
20
20
  Queues files:
21
- - src/domain/model/{module_name}.py
22
- - tests/domain/model/test_{module_name}.py
21
+ - src/domain/model/{module_name}/__init__.py
22
+ - src/domain/model/{module_name}/{module_name}.py
23
+ - src/domain/model/{module_name}/gateways/__init__.py
24
+ - src/domain/model/{module_name}/gateways/{module_name}_repository.py
25
+ - tests/domain/model/{module_name}/test_{module_name}.py
26
+ - tests/domain/model/{module_name}/gateways/test_{module_name}_repository.py
23
27
 
24
28
  No dependencies injected (pydantic is already a project dependency).
25
29
  """
26
30
  project_root = builder.project_root
27
31
  pkg = builder.project_ctx.python_package
28
32
 
29
- # Get module name from module context
30
33
  module_name = builder.module_ctx.module_name if builder.module_ctx else "model"
31
34
 
32
- # Setup directory paths
33
- src_dir = project_root / "src" / pkg / "domain" / "model"
34
- test_dir = resolve_tests_root(project_root) / "domain" / "model"
35
+ src_dir = project_root / "src" / pkg / "domain" / "model" / module_name
36
+ gateways_dir = src_dir / "gateways"
37
+ test_dir = resolve_tests_root(project_root) / "domain" / "model" / module_name
38
+ test_gateways_dir = test_dir / "gateways"
35
39
 
36
- # Template base path
37
40
  base = "model"
38
41
 
39
- # Build context dict from module context
40
42
  ctx_dict = {}
41
43
  if builder.module_ctx:
42
44
  ctx_dict.update(builder.module_ctx.model_dump())
43
45
 
44
- # Add source file
45
- builder.add_file(
46
- src_dir / f"{module_name}.py",
47
- builder.render(f"{base}/model.py.jinja2", ctx_dict),
48
- template_name=f"{base}/model.py.jinja2",
49
- )
50
-
51
- # Add test file
52
- builder.add_file(
53
- test_dir / f"test_{module_name}.py",
54
- builder.render(f"{base}/test_model.py.jinja2", ctx_dict),
55
- template_name=f"{base}/test_model.py.jinja2",
56
- is_test=True,
57
- )
46
+ # (target path, template file name, is_test)
47
+ files = [
48
+ (src_dir / "__init__.py", "__init__.py.jinja2", False),
49
+ (src_dir / f"{module_name}.py", "model.py.jinja2", False),
50
+ (gateways_dir / "__init__.py", "gateways_init.py.jinja2", False),
51
+ (gateways_dir / f"{module_name}_repository.py", "gateway.py.jinja2", False),
52
+ (test_dir / f"test_{module_name}.py", "test_model.py.jinja2", True),
53
+ (test_gateways_dir / f"test_{module_name}_repository.py", "test_gateway.py.jinja2", True),
54
+ ]
55
+ for path, template, is_test in files:
56
+ template_name = f"{base}/{template}"
57
+ builder.add_file(
58
+ path,
59
+ builder.render(template_name, ctx_dict),
60
+ template_name=template_name,
61
+ is_test=is_test,
62
+ )
@@ -0,0 +1 @@
1
+ """{{ class_name }} domain model package."""
@@ -0,0 +1,7 @@
1
+ """{{ class_name }} gateway."""
2
+
3
+ from abc import ABC
4
+
5
+
6
+ class {{ class_name }}Repository(ABC):
7
+ """Repository contract for {{ class_name }}. Add the operations your domain needs."""
@@ -0,0 +1 @@
1
+ """{{ class_name }} gateways."""
@@ -0,0 +1,9 @@
1
+ """Tests for {{ class_name }}Repository gateway."""
2
+
3
+ from abc import ABC
4
+
5
+ from {{ project.python_package }}.domain.model.{{ module_name }}.gateways.{{ module_name }}_repository import {{ class_name }}Repository
6
+
7
+
8
+ def test_{{ module_name }}_repository_is_abstract_contract() -> None:
9
+ assert issubclass({{ class_name }}Repository, ABC)
@@ -1,6 +1,6 @@
1
1
  """Tests for {{ class_name }} domain model."""
2
2
 
3
- from {{ project.python_package }}.domain.model.{{ module_name }} import {{ class_name }}
3
+ from {{ project.python_package }}.domain.model.{{ module_name }}.{{ module_name }} import {{ class_name }}
4
4
 
5
5
 
6
6
  def test_{{ module_name }}_can_be_instantiated() -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scaffold-ca-python
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Scaffolder to generate Clean Architecture apps
5
5
  Project-URL: Homepage, https://github.com/bancolombia/scaffold-clean-architecture-py
6
6
  Project-URL: Repository, https://github.com/bancolombia/scaffold-clean-architecture-py
@@ -105,12 +105,23 @@ scaffold ca --name my-project --dry-run
105
105
 
106
106
  ### scaffold gm
107
107
 
108
- Scaffold a Pydantic v2 domain model and its test stub.
108
+ Scaffold a Pydantic v2 domain model, its gateway (an empty abstract repository contract), and test stubs.
109
109
 
110
110
  ```bash
111
111
  scaffold gm --name order
112
112
  ```
113
113
 
114
+ Generated structure:
115
+
116
+ ```text
117
+ domain/model/order/
118
+ ├── __init__.py
119
+ ├── order.py
120
+ └── gateways/
121
+ ├── __init__.py
122
+ └── order_repository.py
123
+ ```
124
+
114
125
  ```bash
115
126
  scaffold gm --name order --dry-run
116
127
  ```
@@ -8,7 +8,7 @@ scaffold_ca_python/commands/delete_module.py,sha256=J-nkA_o9mY2AOOzQBg3kyTEWm6el
8
8
  scaffold_ca_python/commands/generate_driven_adapter.py,sha256=IRGXCinpa694_9W7WXL7k43hOm9V52Jk87DVgayzeEs,4215
9
9
  scaffold_ca_python/commands/generate_entry_point.py,sha256=NvHF1auzB0bfcx9KzaHCx3Z_pA0FLswwDB9yW3LdKG8,9449
10
10
  scaffold_ca_python/commands/generate_helper.py,sha256=O98ua5Fj3judGxF_vmTYbm_j-qEAxGd4eA0F1YYtPqM,2884
11
- scaffold_ca_python/commands/generate_model.py,sha256=1oKXtRz7JkPPNZkmE-Kw1sOBUQgWnKo6IXYHxPf4-60,2839
11
+ scaffold_ca_python/commands/generate_model.py,sha256=GBHPkoTYrABNdmdILWGLuQFM4ppzYbuHyunjhc-o4Dc,3387
12
12
  scaffold_ca_python/commands/generate_pipeline.py,sha256=Ip-rN4nSbwaUbtJ49awAs4LDURsrDTQsZdG5kBCLdyE,3773
13
13
  scaffold_ca_python/commands/generate_project.py,sha256=5KRsPI_WiWQVY1d_0AblUHKdDQCWpXJA7MaOCHae0fE,5312
14
14
  scaffold_ca_python/commands/generate_use_case.py,sha256=N72h2BAyNgbCLiphryrwF1aDXkDL5NNj14Lsq7D3YFE,2947
@@ -18,7 +18,7 @@ scaffold_ca_python/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
18
18
  scaffold_ca_python/core/file_writer.py,sha256=KdnNrq33Jsvqs1IQf0jbUKdXls4jBSWAizw4uJhMalU,5561
19
19
  scaffold_ca_python/core/mcp_tool_spec.py,sha256=mPLN7vOSgWFWxHQQxmr0fWEaimHWI-9A0oP7ob73-rU,10602
20
20
  scaffold_ca_python/core/module_builder.py,sha256=AHC3szEdfS3i3q74IM6USZTHWEUK6QizL6NEKcHCrrI,6837
21
- scaffold_ca_python/core/module_locator.py,sha256=VEZapvZqk11-5brsHJvPdIsKlEWSTdwS6T9XF9o0acE,1657
21
+ scaffold_ca_python/core/module_locator.py,sha256=VnvD3-g6NvSGJReYamBuoU1s72NdoHgau-SlpaB3xEg,1958
22
22
  scaffold_ca_python/core/name_utils.py,sha256=195Qb36CafP4CGRomzSAm3Hmm0C-Qld3I90boPAV-7A,2583
23
23
  scaffold_ca_python/core/openapi_routes.py,sha256=RCQS7mHufWJZgbLI1cxGli-xGbU7p7WI95fHJhDnIss,1098
24
24
  scaffold_ca_python/core/project_detector.py,sha256=vcDAFCGgQVDMQ2MivJVUmn5Yyj58vGpTbzKhZTBQNtw,3524
@@ -40,9 +40,9 @@ scaffold_ca_python/factory/entry_points/ep_restapi.py,sha256=PFkt9TH1aN6YO8w4zAe
40
40
  scaffold_ca_python/factory/pipelines/__init__.py,sha256=0YPpPGsZ6A3-udHTSZDyUHleLFLQQh1V5IL6AB0qJ6g,557
41
41
  scaffold_ca_python/factory/pipelines/pipeline_factory.py,sha256=s4fpuyhdxXiwko-LlM6Pv-QJdRGQJb17KVrC-DW6ojo,842
42
42
  scaffold_ca_python/factory/simple/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- scaffold_ca_python/factory/simple/delete_module_factory.py,sha256=lw4_XCJegvDqxlZ7o6kDJ792rugOiUijCH1tVsiwRJI,3119
43
+ scaffold_ca_python/factory/simple/delete_module_factory.py,sha256=hvDZNz8TULYx0zES3697-jnrzDQT6v6nxqPV0H9-mWo,1651
44
44
  scaffold_ca_python/factory/simple/helper_factory.py,sha256=nBVOu_kGBVrEGnXFa918GPo-UFlcW-Y5-Uk6bwYWuhQ,2295
45
- scaffold_ca_python/factory/simple/model_factory.py,sha256=cgI3TRCUNfNUBQY5mO-9mhNOwW5LWEVk6Y9rsx120Tg,1807
45
+ scaffold_ca_python/factory/simple/model_factory.py,sha256=YdUpBvGgUrz4zzXMG2p8d6PWit62B9esjLZG49dT5PU,2467
46
46
  scaffold_ca_python/factory/simple/use_case_factory.py,sha256=znCyynIGmWgc4P8icJXfb2xSiNZB2Cu4SGVacDrpSdY,1954
47
47
  scaffold_ca_python/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
48
  scaffold_ca_python/models/context.py,sha256=Itp-YyPgY-0yNYA9YlCXrmPfa5nEVklGAOJQGHfBdHE,1909
@@ -94,8 +94,12 @@ scaffold_ca_python/templates/entry_point/restapi/test_server.py.jinja2,sha256=7_
94
94
  scaffold_ca_python/templates/helper/__init__.py.jinja2,sha256=LA4hikmh30a_8LgZ8h18zdC3NEfDPNJeZOSBJ4RZBwU,39
95
95
  scaffold_ca_python/templates/helper/helper.py.jinja2,sha256=rPaSM5Y4U25Ht8nshnw9m_fc3FXkx-CHA_egJM7QDmc,110
96
96
  scaffold_ca_python/templates/helper/test_helper.py.jinja2,sha256=5nmkjej1FQngXh5JAoftdRvE8kdad0Knjxf25Lb--RI,268
97
+ scaffold_ca_python/templates/model/__init__.py.jinja2,sha256=fz54Iv6Z8sDtCMgIz8eFBXokdDz0SA2uhQMf8IGYUcs,45
98
+ scaffold_ca_python/templates/model/gateway.py.jinja2,sha256=o2FNIfFseauf_5REavVnM-Qe7k8rjoXRqSuqtOI0vIk,184
99
+ scaffold_ca_python/templates/model/gateways_init.py.jinja2,sha256=8bO2y7wFlMVRMURwxLD9ubAHuu71YXCRhGtfSjkWtcM,33
97
100
  scaffold_ca_python/templates/model/model.py.jinja2,sha256=pvzy78xH5-qWJ6IQb-9bY-dnqPj4FPBsQ-Kalh_mFr0,151
98
- scaffold_ca_python/templates/model/test_model.py.jinja2,sha256=55F0CUHb2A3D5ikSNgPveNZ1Jt11gwfzKqGKHYjuzLo,263
101
+ scaffold_ca_python/templates/model/test_gateway.py.jinja2,sha256=xedkiihpUCARhGSKEPLmAlbJ0yOxS2uvDwpIdLbmSFg,338
102
+ scaffold_ca_python/templates/model/test_model.py.jinja2,sha256=Wdz84c8Yt6aeUFQIINdfsq0K9Ihk7GT_ZVzpEGrQ4uM,281
99
103
  scaffold_ca_python/templates/pipeline/azure/azure_pipelines.yml.jinja2,sha256=ecbvubtMhoyqxiM69PunOYwOOBBLUFqSBsoqJsIqyDI,555
100
104
  scaffold_ca_python/templates/pipeline/github/ci.yml.jinja2,sha256=y20dpIoOHlZNCIXzyJdjsTgq2HiyCjHpcg2UPKZuQRg,672
101
105
  scaffold_ca_python/templates/project/README.jinja2,sha256=HazR7Ls3utOfMFjxDsVS1-WMCPG1OR4XR2ftn02Fnuo,696
@@ -117,8 +121,8 @@ scaffold_ca_python/templates/project/application/config/resource_container.py.ji
117
121
  scaffold_ca_python/templates/project/application/config/usecases_container.py.jinja2,sha256=3fEHe6mkXkDHlfO5nhLqzLB7ccTpqwtjHZfLyfysbro,527
118
122
  scaffold_ca_python/templates/use_case/test_use_case.py.jinja2,sha256=3K1bNKDBQc29rcv8UHUZnROCUmcFSCwJhbm0xP9Uako,364
119
123
  scaffold_ca_python/templates/use_case/use_case.py.jinja2,sha256=KPtaK1wvZ-7qHdDZyuVfqXTPYwJdNUMPXQZGuKZFA7Y,217
120
- scaffold_ca_python-0.2.1.dist-info/METADATA,sha256=VxmJhT8bnSFEI4UGZznCyJSHD5QqDaVDrdWtxstKtYc,6960
121
- scaffold_ca_python-0.2.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
122
- scaffold_ca_python-0.2.1.dist-info/entry_points.txt,sha256=4PyYg6mNCvAlt2yF_o1-QR2CBXiowYGpNyqcP88tu6I,104
123
- scaffold_ca_python-0.2.1.dist-info/licenses/LICENSE,sha256=3ZZEP7DK_h3lEjS8r-DvQCec2q7zAqqq7HmSq6cuL0E,1068
124
- scaffold_ca_python-0.2.1.dist-info/RECORD,,
124
+ scaffold_ca_python-0.3.0.dist-info/METADATA,sha256=u5KDkAfy1Wb0HVt1Jrb2MzZr2Nqgb9xnwOZhhmmwVKw,7187
125
+ scaffold_ca_python-0.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
126
+ scaffold_ca_python-0.3.0.dist-info/entry_points.txt,sha256=4PyYg6mNCvAlt2yF_o1-QR2CBXiowYGpNyqcP88tu6I,104
127
+ scaffold_ca_python-0.3.0.dist-info/licenses/LICENSE,sha256=3ZZEP7DK_h3lEjS8r-DvQCec2q7zAqqq7HmSq6cuL0E,1068
128
+ scaffold_ca_python-0.3.0.dist-info/RECORD,,