modwire 3.0.0__tar.gz → 3.1.1__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 (95) hide show
  1. {modwire-3.0.0 → modwire-3.1.1}/.github/workflows/release.yml +11 -1
  2. {modwire-3.0.0 → modwire-3.1.1}/PKG-INFO +24 -1
  3. {modwire-3.0.0 → modwire-3.1.1}/README.md +22 -0
  4. {modwire-3.0.0 → modwire-3.1.1}/pyproject.toml +7 -0
  5. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/_version.py +3 -3
  6. modwire-3.1.1/src/modwire/modules/__init__.py +3 -0
  7. modwire-3.1.1/src/modwire/modules/generate.py +74 -0
  8. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/copier.yml +38 -0
  9. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/php/src/Application/{{ command_name }}.php.jinja +14 -0
  10. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/php/src/Application/{{ service_name }}.php.jinja +24 -0
  11. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/php/src/Domain/{{ aggregate_name }}.php.jinja +27 -0
  12. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/php/src/Infrastructure/InMemory{{ repository_name }}.php.jinja +24 -0
  13. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/php/src/Ports/{{ repository_name }}.php.jinja +14 -0
  14. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/php/src/Ui/{{ controller_name }}.php.jinja +27 -0
  15. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/python/{{ module_name }}/__init__.py.jinja +5 -0
  16. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/python/{{ module_name }}/application.py.jinja +22 -0
  17. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/python/{{ module_name }}/domain.py.jinja +20 -0
  18. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/python/{{ module_name }}/infrastructure.py.jinja +17 -0
  19. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/python/{{ module_name }}/ports.py.jinja +16 -0
  20. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/python/{{ module_name }}/ui.py.jinja +21 -0
  21. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/typescript/{{ module_name }}/application.ts.jinja +20 -0
  22. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/typescript/{{ module_name }}/domain.ts.jinja +18 -0
  23. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/typescript/{{ module_name }}/infrastructure.ts.jinja +14 -0
  24. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/typescript/{{ module_name }}/ports.ts.jinja +6 -0
  25. modwire-3.1.1/src/modwire/modules/scaffoldings/hexagonal/template/typescript/{{ module_name }}/ui.ts.jinja +19 -0
  26. modwire-3.1.1/src/modwire/modules/scaffoldings/layered/copier.yml +23 -0
  27. modwire-3.1.1/src/modwire/modules/scaffoldings/layered/template/{{ module_name }}/application.py.jinja +18 -0
  28. modwire-3.1.1/src/modwire/modules/scaffoldings/layered/template/{{ module_name }}/domain.py.jinja +12 -0
  29. modwire-3.1.1/src/modwire/modules/scaffoldings/layered/template/{{ module_name }}/infrastructure.py.jinja +11 -0
  30. modwire-3.1.1/src/modwire/modules/scaffoldings/layered/template/{{ module_name }}/ui.py.jinja +20 -0
  31. {modwire-3.0.0 → modwire-3.1.1}/src/modwire.egg-info/PKG-INFO +24 -1
  32. {modwire-3.0.0 → modwire-3.1.1}/src/modwire.egg-info/SOURCES.txt +27 -3
  33. {modwire-3.0.0 → modwire-3.1.1}/src/modwire.egg-info/requires.txt +1 -0
  34. modwire-3.1.1/tests/modules/test_generate.py +56 -0
  35. {modwire-3.0.0 → modwire-3.1.1}/uv.lock +264 -0
  36. modwire-3.0.0/src/modwire.egg-info/scm_file_list.json +0 -63
  37. modwire-3.0.0/src/modwire.egg-info/scm_version.json +0 -8
  38. {modwire-3.0.0 → modwire-3.1.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  39. {modwire-3.0.0 → modwire-3.1.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  40. {modwire-3.0.0 → modwire-3.1.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  41. {modwire-3.0.0 → modwire-3.1.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  42. {modwire-3.0.0 → modwire-3.1.1}/.github/workflows/ci.yml +0 -0
  43. {modwire-3.0.0 → modwire-3.1.1}/.gitignore +0 -0
  44. {modwire-3.0.0 → modwire-3.1.1}/CHANGELOG.md +0 -0
  45. {modwire-3.0.0 → modwire-3.1.1}/CONTRIBUTING.md +0 -0
  46. {modwire-3.0.0 → modwire-3.1.1}/LICENSE +0 -0
  47. {modwire-3.0.0 → modwire-3.1.1}/docs/wiki/Development-checks.md +0 -0
  48. {modwire-3.0.0 → modwire-3.1.1}/docs/wiki/Home.md +0 -0
  49. {modwire-3.0.0 → modwire-3.1.1}/docs/wiki/Reporting-bugs.md +0 -0
  50. {modwire-3.0.0 → modwire-3.1.1}/docs/wiki/Requesting-features.md +0 -0
  51. {modwire-3.0.0 → modwire-3.1.1}/setup.cfg +0 -0
  52. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/__init__.py +0 -0
  53. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/__init__.py +0 -0
  54. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/__init__.py +0 -0
  55. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/analyzers/__init__.py +0 -0
  56. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/analyzers/backward.py +0 -0
  57. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/analyzers/no_cycles.py +0 -0
  58. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/analyzers/no_reentry.py +0 -0
  59. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/base.py +0 -0
  60. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/config.py +0 -0
  61. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/map.py +0 -0
  62. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/pipeline.py +0 -0
  63. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/tags/__init__.py +0 -0
  64. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/tags/matcher.py +0 -0
  65. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/boundaries/tags/tag_map.py +0 -0
  66. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/config.py +0 -0
  67. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/insight/__init__.py +0 -0
  68. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/insight/base.py +0 -0
  69. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/insight/pipeline.py +0 -0
  70. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/insight/reports/__init__.py +0 -0
  71. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/insight/reports/callables.py +0 -0
  72. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/insight/reports/clusters.py +0 -0
  73. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/insight/reports/coherence.py +0 -0
  74. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/insight/reports/exports.py +0 -0
  75. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/insight/reports/hotspots.py +0 -0
  76. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/report.py +0 -0
  77. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/root.py +0 -0
  78. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/__init__.py +0 -0
  79. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/base.py +0 -0
  80. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/config.py +0 -0
  81. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/pipeline.py +0 -0
  82. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/rules/__init__.py +0 -0
  83. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/rules/abstract_class_resolver.py +0 -0
  84. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/rules/callable_resolver.py +0 -0
  85. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/rules/class_resolver.py +0 -0
  86. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/rules/file_resolver.py +0 -0
  87. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/rules/import_resolver.py +0 -0
  88. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/rules/property_resolver.py +0 -0
  89. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/rules/signature_resolver.py +0 -0
  90. {modwire-3.0.0 → modwire-3.1.1}/src/modwire/architecture/shape/rules/symbol_resolver.py +0 -0
  91. {modwire-3.0.0 → modwire-3.1.1}/src/modwire.egg-info/dependency_links.txt +0 -0
  92. {modwire-3.0.0 → modwire-3.1.1}/src/modwire.egg-info/top_level.txt +0 -0
  93. {modwire-3.0.0 → modwire-3.1.1}/tests/architecture/boundaries/test_map.py +0 -0
  94. {modwire-3.0.0 → modwire-3.1.1}/tests/architecture/boundaries/test_pipeline.py +0 -0
  95. {modwire-3.0.0 → modwire-3.1.1}/tests/architecture/test_report.py +0 -0
@@ -46,6 +46,15 @@ jobs:
46
46
  exit 1
47
47
  fi
48
48
 
49
+ - name: Use release tag as package version
50
+ if: github.event_name == 'release'
51
+ run: |
52
+ tag_name="${{ github.event.release.tag_name }}"
53
+ release_version="${tag_name#v}"
54
+
55
+ echo "MODWIRE_RELEASE_VERSION=$release_version" >> "$GITHUB_ENV"
56
+ echo "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MODWIRE=$release_version" >> "$GITHUB_ENV"
57
+
49
58
  - name: Set up Python
50
59
  uses: actions/setup-python@v5
51
60
  with:
@@ -90,12 +99,13 @@ jobs:
90
99
  if: github.event_name == 'release'
91
100
  run: |
92
101
  python - <<'PY'
102
+ import os
93
103
  import sys
94
104
  import zipfile
95
105
  from email.parser import Parser
96
106
  from pathlib import Path
97
107
 
98
- expected_version = "${{ github.event.release.tag_name }}".removeprefix("v")
108
+ expected_version = os.environ["MODWIRE_RELEASE_VERSION"]
99
109
  wheels = sorted(Path("dist").glob("*.whl"))
100
110
  versions: dict[str, str] = {}
101
111
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modwire
3
- Version: 3.0.0
3
+ Version: 3.1.1
4
4
  Summary: Map architecture boundaries and analyze extracted code maps.
5
5
  Author: Tomasz Szpak
6
6
  License-Expression: MIT
@@ -23,6 +23,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
23
23
  Requires-Python: >=3.11
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
+ Requires-Dist: copier>=9.16.0
26
27
  Requires-Dist: modwire-extraction>=1.0.1
27
28
  Requires-Dist: pydantic>=2.8
28
29
  Provides-Extra: dev
@@ -113,6 +114,28 @@ Reports are Pydantic models, so they can be serialized with `model_dump()` or
113
114
  payload = report.model_dump(mode="json")
114
115
  ```
115
116
 
117
+ ## Module Generation
118
+
119
+ `modwire.modules` can generate a module from any Copier template. A caller only
120
+ needs to create a normal Copier template directory and pass it to
121
+ `generate_module`.
122
+
123
+ ```python
124
+ from pathlib import Path
125
+
126
+ from modwire.modules import generate_module
127
+
128
+ generate_module(
129
+ "billing",
130
+ Path("src/features"),
131
+ Path("templates/modwire-module"),
132
+ data={"service_name": "BillingService"},
133
+ )
134
+ ```
135
+
136
+ If no template path is provided, `generate_module` uses the bundled `layered`
137
+ scaffolding. Bundled scaffoldings are packaged with the distribution wheel.
138
+
116
139
  ## Configuration
117
140
 
118
141
  Boundary tags classify source IDs. Flow rules then use those tags to detect
@@ -79,6 +79,28 @@ Reports are Pydantic models, so they can be serialized with `model_dump()` or
79
79
  payload = report.model_dump(mode="json")
80
80
  ```
81
81
 
82
+ ## Module Generation
83
+
84
+ `modwire.modules` can generate a module from any Copier template. A caller only
85
+ needs to create a normal Copier template directory and pass it to
86
+ `generate_module`.
87
+
88
+ ```python
89
+ from pathlib import Path
90
+
91
+ from modwire.modules import generate_module
92
+
93
+ generate_module(
94
+ "billing",
95
+ Path("src/features"),
96
+ Path("templates/modwire-module"),
97
+ data={"service_name": "BillingService"},
98
+ )
99
+ ```
100
+
101
+ If no template path is provided, `generate_module` uses the bundled `layered`
102
+ scaffolding. Bundled scaffoldings are packaged with the distribution wheel.
103
+
82
104
  ## Configuration
83
105
 
84
106
  Boundary tags classify source IDs. Flow rules then use those tags to detect
@@ -33,6 +33,7 @@ classifiers = [
33
33
  "Topic :: Software Development :: Quality Assurance",
34
34
  ]
35
35
  dependencies = [
36
+ "copier>=9.16.0",
36
37
  "modwire-extraction>=1.0.1",
37
38
  "pydantic>=2.8",
38
39
  ]
@@ -72,6 +73,12 @@ cache-dir = ".dev/ruff_cache"
72
73
  package-dir = {"" = "src"}
73
74
  include-package-data = true
74
75
 
76
+ [tool.setuptools.package-data]
77
+ "modwire.modules" = [
78
+ "scaffoldings/*/copier.yml",
79
+ "scaffoldings/*/template/**/*.jinja",
80
+ ]
81
+
75
82
  [tool.setuptools.packages.find]
76
83
  where = ["src"]
77
84
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '3.0.0'
22
- __version_tuple__ = version_tuple = (3, 0, 0)
21
+ __version__ = version = '3.1.1'
22
+ __version_tuple__ = version_tuple = (3, 1, 1)
23
23
 
24
- __commit_id__ = commit_id = 'g84d5eeaca'
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,3 @@
1
+ from .generate import generate_module
2
+
3
+ __all__ = ["generate_module"]
@@ -0,0 +1,74 @@
1
+ from collections.abc import Iterator, Mapping
2
+ from contextlib import contextmanager
3
+ from importlib import resources
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import copier
8
+
9
+ PathLike = str | Path
10
+
11
+
12
+ @contextmanager
13
+ def _bundled_scaffolding_path(scaffolding: str) -> Iterator[Path]:
14
+ scaffoldings = resources.files("modwire.modules") / "scaffoldings" / scaffolding
15
+ with resources.as_file(scaffoldings) as path:
16
+ yield path
17
+
18
+
19
+ def generate_module(
20
+ module_name: str,
21
+ target_root: PathLike,
22
+ template_root: PathLike | None = None,
23
+ *,
24
+ scaffolding: str = "layered",
25
+ data: Mapping[str, Any] | None = None,
26
+ overwrite: bool = False,
27
+ skip_tasks: bool = True,
28
+ ) -> None:
29
+ if template_root is not None:
30
+ _run_copy(
31
+ Path(template_root),
32
+ target_root,
33
+ module_name,
34
+ data=data,
35
+ overwrite=overwrite,
36
+ skip_tasks=skip_tasks,
37
+ )
38
+ return
39
+
40
+ with _bundled_scaffolding_path(scaffolding) as resolved_template_root:
41
+ _run_copy(
42
+ resolved_template_root,
43
+ target_root,
44
+ module_name,
45
+ data=data,
46
+ overwrite=overwrite,
47
+ skip_tasks=skip_tasks,
48
+ )
49
+
50
+
51
+ def _run_copy(
52
+ template_root: Path,
53
+ target_root: PathLike,
54
+ module_name: str,
55
+ *,
56
+ data: Mapping[str, Any] | None,
57
+ overwrite: bool,
58
+ skip_tasks: bool,
59
+ ) -> None:
60
+ resolved_template_root = template_root
61
+ if not resolved_template_root.exists():
62
+ raise FileNotFoundError(f"Copier template does not exist: {resolved_template_root}")
63
+
64
+ template_data = dict(data or {})
65
+ template_data["module_name"] = module_name
66
+
67
+ copier.run_copy(
68
+ str(resolved_template_root),
69
+ dst_path=target_root,
70
+ data=template_data,
71
+ defaults=True,
72
+ overwrite=overwrite,
73
+ skip_tasks=skip_tasks,
74
+ )
@@ -0,0 +1,38 @@
1
+ _min_copier_version: "9.0.0"
2
+ _templates_suffix: ".jinja"
3
+ _subdirectory: template
4
+
5
+ module_name:
6
+ type: str
7
+ default: sample_module
8
+ help: Importable module or package directory name.
9
+
10
+ aggregate_name:
11
+ type: str
12
+ default: SampleAggregate
13
+ help: Domain aggregate class name.
14
+
15
+ command_name:
16
+ type: str
17
+ default: CreateSample
18
+ help: Application command class name.
19
+
20
+ service_name:
21
+ type: str
22
+ default: SampleService
23
+ help: Application service class name.
24
+
25
+ repository_name:
26
+ type: str
27
+ default: SampleRepository
28
+ help: Repository port and adapter class suffix.
29
+
30
+ controller_name:
31
+ type: str
32
+ default: SampleController
33
+ help: UI/controller class name.
34
+
35
+ php_namespace:
36
+ type: str
37
+ default: App\\SampleModule
38
+ help: PHP namespace root for the generated classes.
@@ -0,0 +1,14 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace {{ php_namespace }}\Application;
6
+
7
+ final readonly class {{ command_name }}
8
+ {
9
+ public function __construct(
10
+ public string $id,
11
+ public string $name,
12
+ ) {
13
+ }
14
+ }
@@ -0,0 +1,24 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace {{ php_namespace }}\Application;
6
+
7
+ use {{ php_namespace }}\Domain\{{ aggregate_name }};
8
+ use {{ php_namespace }}\Ports\{{ repository_name }};
9
+
10
+ final class {{ service_name }}
11
+ {
12
+ public function __construct(
13
+ private readonly {{ repository_name }} $repository,
14
+ ) {
15
+ }
16
+
17
+ public function handle({{ command_name }} $command): {{ aggregate_name }}
18
+ {
19
+ $aggregate = {{ aggregate_name }}::create($command->id, $command->name);
20
+ $this->repository->save($aggregate);
21
+
22
+ return $aggregate;
23
+ }
24
+ }
@@ -0,0 +1,27 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace {{ php_namespace }}\Domain;
6
+
7
+ final readonly class {{ aggregate_name }}
8
+ {
9
+ private function __construct(
10
+ public string $id,
11
+ public string $name,
12
+ ) {
13
+ }
14
+
15
+ public static function create(string $id, string $name): self
16
+ {
17
+ if ($id === '') {
18
+ throw new \InvalidArgumentException('Aggregate id cannot be empty.');
19
+ }
20
+
21
+ if ($name === '') {
22
+ throw new \InvalidArgumentException('Aggregate name cannot be empty.');
23
+ }
24
+
25
+ return new self($id, $name);
26
+ }
27
+ }
@@ -0,0 +1,24 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace {{ php_namespace }}\Infrastructure;
6
+
7
+ use {{ php_namespace }}\Domain\{{ aggregate_name }};
8
+ use {{ php_namespace }}\Ports\{{ repository_name }};
9
+
10
+ final class InMemory{{ repository_name }} implements {{ repository_name }}
11
+ {
12
+ /** @var array<string, {{ aggregate_name }}> */
13
+ private array $items = [];
14
+
15
+ public function save({{ aggregate_name }} $aggregate): void
16
+ {
17
+ $this->items[$aggregate->id] = $aggregate;
18
+ }
19
+
20
+ public function get(string $id): ?{{ aggregate_name }}
21
+ {
22
+ return $this->items[$id] ?? null;
23
+ }
24
+ }
@@ -0,0 +1,14 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace {{ php_namespace }}\Ports;
6
+
7
+ use {{ php_namespace }}\Domain\{{ aggregate_name }};
8
+
9
+ interface {{ repository_name }}
10
+ {
11
+ public function save({{ aggregate_name }} $aggregate): void;
12
+
13
+ public function get(string $id): ?{{ aggregate_name }};
14
+ }
@@ -0,0 +1,27 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace {{ php_namespace }}\Ui;
6
+
7
+ use {{ php_namespace }}\Application\{{ command_name }};
8
+ use {{ php_namespace }}\Application\{{ service_name }};
9
+
10
+ final class {{ controller_name }}
11
+ {
12
+ public function __construct(
13
+ private readonly {{ service_name }} $service,
14
+ ) {
15
+ }
16
+
17
+ /** @return array{id: string, name: string} */
18
+ public function create(string $id, string $name): array
19
+ {
20
+ $aggregate = $this->service->handle(new {{ command_name }}($id, $name));
21
+
22
+ return [
23
+ 'id' => $aggregate->id,
24
+ 'name' => $aggregate->name,
25
+ ];
26
+ }
27
+ }
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from . import application, domain, infrastructure, ports, ui
4
+
5
+ __all__ = ["application", "domain", "infrastructure", "ports", "ui"]
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from . import domain, ports
4
+
5
+
6
+ class {{ command_name }}:
7
+ def __init__(self, aggregate_id: str, name: str) -> None:
8
+ self.aggregate_id = aggregate_id
9
+ self.name = name
10
+
11
+
12
+ class {{ service_name }}:
13
+ def __init__(self, repository: ports.{{ repository_name }}) -> None:
14
+ self._repository = repository
15
+
16
+ def handle(self, command: {{ command_name }}) -> domain.{{ aggregate_name }}:
17
+ aggregate = domain.{{ aggregate_name }}.create(command.aggregate_id, command.name)
18
+ self._repository.save(aggregate)
19
+ return aggregate
20
+
21
+
22
+ __all__ = ["{{ command_name }}", "{{ service_name }}"]
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class {{ aggregate_name }}:
8
+ aggregate_id: str
9
+ name: str
10
+
11
+ @classmethod
12
+ def create(cls, aggregate_id: str, name: str) -> "{{ aggregate_name }}":
13
+ if not aggregate_id:
14
+ raise ValueError("Aggregate id cannot be empty")
15
+ if not name:
16
+ raise ValueError("Aggregate name cannot be empty")
17
+ return cls(aggregate_id=aggregate_id, name=name)
18
+
19
+
20
+ __all__ = ["{{ aggregate_name }}"]
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from . import domain
4
+
5
+
6
+ class InMemory{{ repository_name }}:
7
+ def __init__(self) -> None:
8
+ self._items: dict[str, domain.{{ aggregate_name }}] = {}
9
+
10
+ def save(self, aggregate: domain.{{ aggregate_name }}) -> None:
11
+ self._items[aggregate.aggregate_id] = aggregate
12
+
13
+ def get(self, aggregate_id: str) -> domain.{{ aggregate_name }} | None:
14
+ return self._items.get(aggregate_id)
15
+
16
+
17
+ __all__ = ["InMemory{{ repository_name }}"]
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from . import domain
6
+
7
+
8
+ class {{ repository_name }}(Protocol):
9
+ def save(self, aggregate: domain.{{ aggregate_name }}) -> None:
10
+ raise NotImplementedError
11
+
12
+ def get(self, aggregate_id: str) -> domain.{{ aggregate_name }} | None:
13
+ raise NotImplementedError
14
+
15
+
16
+ __all__ = ["{{ repository_name }}"]
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from . import application, infrastructure
4
+
5
+
6
+ class {{ controller_name }}:
7
+ def __init__(self, service: application.{{ service_name }}) -> None:
8
+ self._service = service
9
+
10
+ def create(self, aggregate_id: str, name: str) -> dict[str, str]:
11
+ aggregate = self._service.handle(application.{{ command_name }}(aggregate_id, name))
12
+ return {"id": aggregate.aggregate_id, "name": aggregate.name}
13
+
14
+
15
+ def bootstrap() -> {{ controller_name }}:
16
+ repository = infrastructure.InMemory{{ repository_name }}()
17
+ service = application.{{ service_name }}(repository)
18
+ return {{ controller_name }}(service)
19
+
20
+
21
+ __all__ = ["{{ controller_name }}", "bootstrap"]
@@ -0,0 +1,20 @@
1
+ import { {{ aggregate_name }} } from "./domain";
2
+ import { {{ repository_name }} } from "./ports";
3
+
4
+ export class {{ command_name }} {
5
+ constructor(
6
+ public readonly aggregateId: string,
7
+ public readonly name: string,
8
+ ) {}
9
+ }
10
+
11
+ export class {{ service_name }} {
12
+ constructor(private readonly repository: {{ repository_name }}) {}
13
+
14
+ handle(command: {{ command_name }}): {{ aggregate_name }} {
15
+ const aggregate = {{ aggregate_name }}.create(command.aggregateId, command.name);
16
+ this.repository.save(aggregate);
17
+
18
+ return aggregate;
19
+ }
20
+ }
@@ -0,0 +1,18 @@
1
+ export class {{ aggregate_name }} {
2
+ private constructor(
3
+ public readonly aggregateId: string,
4
+ public readonly name: string,
5
+ ) {}
6
+
7
+ static create(aggregateId: string, name: string): {{ aggregate_name }} {
8
+ if (aggregateId.length === 0) {
9
+ throw new Error("Aggregate id cannot be empty.");
10
+ }
11
+
12
+ if (name.length === 0) {
13
+ throw new Error("Aggregate name cannot be empty.");
14
+ }
15
+
16
+ return new {{ aggregate_name }}(aggregateId, name);
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ import { {{ aggregate_name }} } from "./domain";
2
+ import { {{ repository_name }} } from "./ports";
3
+
4
+ export class InMemory{{ repository_name }} implements {{ repository_name }} {
5
+ private readonly items = new Map<string, {{ aggregate_name }}>();
6
+
7
+ save(aggregate: {{ aggregate_name }}): void {
8
+ this.items.set(aggregate.aggregateId, aggregate);
9
+ }
10
+
11
+ get(aggregateId: string): {{ aggregate_name }} | undefined {
12
+ return this.items.get(aggregateId);
13
+ }
14
+ }
@@ -0,0 +1,6 @@
1
+ import { {{ aggregate_name }} } from "./domain";
2
+
3
+ export interface {{ repository_name }} {
4
+ save(aggregate: {{ aggregate_name }}): void;
5
+ get(aggregateId: string): {{ aggregate_name }} | undefined;
6
+ }
@@ -0,0 +1,19 @@
1
+ import { {{ command_name }}, {{ service_name }} } from "./application";
2
+ import { InMemory{{ repository_name }} } from "./infrastructure";
3
+
4
+ export class {{ controller_name }} {
5
+ constructor(private readonly service: {{ service_name }}) {}
6
+
7
+ create(aggregateId: string, name: string): { id: string; name: string } {
8
+ const aggregate = this.service.handle(new {{ command_name }}(aggregateId, name));
9
+
10
+ return { id: aggregate.aggregateId, name: aggregate.name };
11
+ }
12
+ }
13
+
14
+ export function bootstrap(): {{ controller_name }} {
15
+ const repository = new InMemory{{ repository_name }}();
16
+ const service = new {{ service_name }}(repository);
17
+
18
+ return new {{ controller_name }}(service);
19
+ }
@@ -0,0 +1,23 @@
1
+ _min_copier_version: "9.0.0"
2
+ _templates_suffix: ".jinja"
3
+ _subdirectory: template
4
+
5
+ module_name:
6
+ type: str
7
+ default: sample_package
8
+ help: Importable package name that will own the four layer files.
9
+
10
+ model_name:
11
+ type: str
12
+ default: SampleItem
13
+ help: Pydantic domain model class name.
14
+
15
+ repository_name:
16
+ type: str
17
+ default: SampleRepository
18
+ help: Infrastructure repository class name.
19
+
20
+ service_name:
21
+ type: str
22
+ default: SampleApplication
23
+ help: Application service class name.
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from . import domain, infrastructure
4
+
5
+
6
+ class {{ service_name }}:
7
+ def __init__(self, repository: infrastructure.{{ repository_name }}) -> None:
8
+ self._repository = repository
9
+
10
+ def handle(self, name: str) -> domain.{{ model_name }}:
11
+ return self._repository.find(name)
12
+
13
+
14
+ def bootstrap() -> {{ service_name }}:
15
+ return {{ service_name }}(infrastructure.{{ repository_name }}())
16
+
17
+
18
+ __all__ = ["{{ service_name }}", "bootstrap"]
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class {{ model_name }}(BaseModel):
7
+ model_config = ConfigDict(frozen=True)
8
+
9
+ name: str
10
+
11
+
12
+ __all__ = ["{{ model_name }}"]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from . import domain
4
+
5
+
6
+ class {{ repository_name }}:
7
+ def find(self, name: str) -> domain.{{ model_name }}:
8
+ return domain.{{ model_name }}(name=name)
9
+
10
+
11
+ __all__ = ["{{ repository_name }}"]