instant-python 0.20.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.
Files changed (202) hide show
  1. instant_python/__init__.py +1 -0
  2. instant_python/cli/__init__.py +0 -0
  3. instant_python/cli/cli.py +58 -0
  4. instant_python/cli/instant_python_typer.py +35 -0
  5. instant_python/config/__init__.py +0 -0
  6. instant_python/config/application/__init__.py +0 -0
  7. instant_python/config/application/config_generator.py +23 -0
  8. instant_python/config/delivery/__init__.py +0 -0
  9. instant_python/config/delivery/cli.py +19 -0
  10. instant_python/config/domain/__init__.py +0 -0
  11. instant_python/config/domain/question_wizard.py +7 -0
  12. instant_python/config/infra/__init__.py +0 -0
  13. instant_python/config/infra/question_wizard/__init__.py +0 -0
  14. instant_python/config/infra/question_wizard/questionary_console_wizard.py +25 -0
  15. instant_python/config/infra/question_wizard/step/__init__.py +0 -0
  16. instant_python/config/infra/question_wizard/step/dependencies_step.py +64 -0
  17. instant_python/config/infra/question_wizard/step/general_step.py +81 -0
  18. instant_python/config/infra/question_wizard/step/git_step.py +41 -0
  19. instant_python/config/infra/question_wizard/step/questionary.py +17 -0
  20. instant_python/config/infra/question_wizard/step/steps.py +21 -0
  21. instant_python/config/infra/question_wizard/step/template_step.py +63 -0
  22. instant_python/initialize/__init__.py +0 -0
  23. instant_python/initialize/application/__init__.py +0 -0
  24. instant_python/initialize/application/project_initializer.py +47 -0
  25. instant_python/initialize/delivery/__init__.py +0 -0
  26. instant_python/initialize/delivery/cli.py +48 -0
  27. instant_python/initialize/domain/__init__.py +0 -0
  28. instant_python/initialize/domain/env_manager.py +9 -0
  29. instant_python/initialize/domain/node.py +73 -0
  30. instant_python/initialize/domain/project_formatter.py +7 -0
  31. instant_python/initialize/domain/project_renderer.py +10 -0
  32. instant_python/initialize/domain/project_structure.py +77 -0
  33. instant_python/initialize/domain/project_writer.py +20 -0
  34. instant_python/initialize/domain/version_control_configurer.py +9 -0
  35. instant_python/initialize/infra/__init__.py +0 -0
  36. instant_python/initialize/infra/env_manager/__init__.py +0 -0
  37. instant_python/initialize/infra/env_manager/env_manager_factory.py +27 -0
  38. instant_python/initialize/infra/env_manager/pdm_env_manager.py +66 -0
  39. instant_python/initialize/infra/env_manager/system_console.py +65 -0
  40. instant_python/initialize/infra/env_manager/uv_env_manager.py +74 -0
  41. instant_python/initialize/infra/formatter/__init__.py +0 -0
  42. instant_python/initialize/infra/formatter/ruff_project_formatter.py +10 -0
  43. instant_python/initialize/infra/renderer/__init__.py +0 -0
  44. instant_python/initialize/infra/renderer/jinja_environment.py +71 -0
  45. instant_python/initialize/infra/renderer/jinja_project_renderer.py +57 -0
  46. instant_python/initialize/infra/version_control/__init__.py +0 -0
  47. instant_python/initialize/infra/version_control/git_configurer.py +29 -0
  48. instant_python/initialize/infra/writer/__init__.py +0 -0
  49. instant_python/initialize/infra/writer/file_system_project_writer.py +23 -0
  50. instant_python/shared/__init__.py +0 -0
  51. instant_python/shared/application_error.py +8 -0
  52. instant_python/shared/domain/__init__.py +0 -0
  53. instant_python/shared/domain/config_repository.py +18 -0
  54. instant_python/shared/domain/config_schema.py +113 -0
  55. instant_python/shared/domain/dependency_config.py +41 -0
  56. instant_python/shared/domain/general_config.py +71 -0
  57. instant_python/shared/domain/git_config.py +32 -0
  58. instant_python/shared/domain/template_config.py +76 -0
  59. instant_python/shared/infra/__init__.py +0 -0
  60. instant_python/shared/infra/persistence/__init__.py +0 -0
  61. instant_python/shared/infra/persistence/yaml_config_repository.py +39 -0
  62. instant_python/shared/supported_built_in_features.py +20 -0
  63. instant_python/shared/supported_licenses.py +11 -0
  64. instant_python/shared/supported_managers.py +10 -0
  65. instant_python/shared/supported_python_versions.py +12 -0
  66. instant_python/shared/supported_templates.py +12 -0
  67. instant_python/templates/boilerplate/.gitignore +164 -0
  68. instant_python/templates/boilerplate/.pre-commit-config.yml +73 -0
  69. instant_python/templates/boilerplate/.python-version +1 -0
  70. instant_python/templates/boilerplate/CITATION.cff +13 -0
  71. instant_python/templates/boilerplate/LICENSE +896 -0
  72. instant_python/templates/boilerplate/README.md +8 -0
  73. instant_python/templates/boilerplate/SECURITY.md +43 -0
  74. instant_python/templates/boilerplate/event_bus/__init__.py +0 -0
  75. instant_python/templates/boilerplate/event_bus/domain_event.py +15 -0
  76. instant_python/templates/boilerplate/event_bus/domain_event_json_deserializer.py +25 -0
  77. instant_python/templates/boilerplate/event_bus/domain_event_json_serializer.py +16 -0
  78. instant_python/templates/boilerplate/event_bus/domain_event_subscriber.py +33 -0
  79. instant_python/templates/boilerplate/event_bus/event_aggregate.py +19 -0
  80. instant_python/templates/boilerplate/event_bus/event_bus.py +9 -0
  81. instant_python/templates/boilerplate/event_bus/exchange_type.py +14 -0
  82. instant_python/templates/boilerplate/event_bus/mock_event_bus.py +16 -0
  83. instant_python/templates/boilerplate/event_bus/rabbit_mq_configurer.py +45 -0
  84. instant_python/templates/boilerplate/event_bus/rabbit_mq_connection.py +71 -0
  85. instant_python/templates/boilerplate/event_bus/rabbit_mq_consumer.py +56 -0
  86. instant_python/templates/boilerplate/event_bus/rabbit_mq_event_bus.py +26 -0
  87. instant_python/templates/boilerplate/event_bus/rabbit_mq_queue_formatter.py +21 -0
  88. instant_python/templates/boilerplate/event_bus/rabbit_mq_settings.py +8 -0
  89. instant_python/templates/boilerplate/exceptions/__init__.py +0 -0
  90. instant_python/templates/boilerplate/exceptions/base_error.py +13 -0
  91. instant_python/templates/boilerplate/exceptions/domain_error.py +6 -0
  92. instant_python/templates/boilerplate/exceptions/domain_event_type_not_found_error.py +6 -0
  93. instant_python/templates/boilerplate/exceptions/rabbit_mq_connection_not_established_error.py +7 -0
  94. instant_python/templates/boilerplate/exceptions/required_value_error.py +6 -0
  95. instant_python/templates/boilerplate/fastapi/__init__.py +0 -0
  96. instant_python/templates/boilerplate/fastapi/application.py +74 -0
  97. instant_python/templates/boilerplate/fastapi/error_handlers.py +88 -0
  98. instant_python/templates/boilerplate/fastapi/error_response.py +31 -0
  99. instant_python/templates/boilerplate/fastapi/fastapi_log_middleware.py +32 -0
  100. instant_python/templates/boilerplate/fastapi/lifespan.py +13 -0
  101. instant_python/templates/boilerplate/fastapi/success_response.py +13 -0
  102. instant_python/templates/boilerplate/github/action.yml +35 -0
  103. instant_python/templates/boilerplate/github/bug_report.yml +60 -0
  104. instant_python/templates/boilerplate/github/ci.yml +199 -0
  105. instant_python/templates/boilerplate/github/feature_request.yml +21 -0
  106. instant_python/templates/boilerplate/github/release.yml +94 -0
  107. instant_python/templates/boilerplate/logger/__init__.py +0 -0
  108. instant_python/templates/boilerplate/logger/file_logger.py +55 -0
  109. instant_python/templates/boilerplate/logger/file_rotating_handler.py +36 -0
  110. instant_python/templates/boilerplate/logger/json_formatter.py +16 -0
  111. instant_python/templates/boilerplate/mypy.ini +41 -0
  112. instant_python/templates/boilerplate/persistence/__init__.py +0 -0
  113. instant_python/templates/boilerplate/persistence/alembic_migrator.py +19 -0
  114. instant_python/templates/boilerplate/persistence/async/README.md +1 -0
  115. instant_python/templates/boilerplate/persistence/async/__init__.py +0 -0
  116. instant_python/templates/boilerplate/persistence/async/alembic.ini +124 -0
  117. instant_python/templates/boilerplate/persistence/async/async_engine_fixture.py +20 -0
  118. instant_python/templates/boilerplate/persistence/async/async_session.py +20 -0
  119. instant_python/templates/boilerplate/persistence/async/env.py +94 -0
  120. instant_python/templates/boilerplate/persistence/async/models_metadata.py +10 -0
  121. instant_python/templates/boilerplate/persistence/async/postgres_settings.py +15 -0
  122. instant_python/templates/boilerplate/persistence/async/script.py.mako +26 -0
  123. instant_python/templates/boilerplate/persistence/async/sqlalchemy_repository.py +28 -0
  124. instant_python/templates/boilerplate/persistence/base.py +4 -0
  125. instant_python/templates/boilerplate/persistence/synchronous/__init__.py +0 -0
  126. instant_python/templates/boilerplate/persistence/synchronous/session_maker.py +21 -0
  127. instant_python/templates/boilerplate/persistence/synchronous/sqlalchemy_repository.py +40 -0
  128. instant_python/templates/boilerplate/pyproject.toml +134 -0
  129. instant_python/templates/boilerplate/pytest.ini +10 -0
  130. instant_python/templates/boilerplate/scripts/add_dependency.py +45 -0
  131. instant_python/templates/boilerplate/scripts/create_aggregate.py +33 -0
  132. instant_python/templates/boilerplate/scripts/insert_template.py +90 -0
  133. instant_python/templates/boilerplate/scripts/integration.sh +39 -0
  134. instant_python/templates/boilerplate/scripts/local_setup.py +12 -0
  135. instant_python/templates/boilerplate/scripts/makefile +184 -0
  136. instant_python/templates/boilerplate/scripts/post-merge.py +40 -0
  137. instant_python/templates/boilerplate/scripts/pre-commit.py +15 -0
  138. instant_python/templates/boilerplate/scripts/pre-push.py +6 -0
  139. instant_python/templates/boilerplate/scripts/remove_dependency.py +40 -0
  140. instant_python/templates/boilerplate/scripts/unit.sh +40 -0
  141. instant_python/templates/project_structure/clean_architecture/layers/application.yml +3 -0
  142. instant_python/templates/project_structure/clean_architecture/layers/delivery.yml +8 -0
  143. instant_python/templates/project_structure/clean_architecture/layers/domain.yml +10 -0
  144. instant_python/templates/project_structure/clean_architecture/layers/infra.yml +12 -0
  145. instant_python/templates/project_structure/clean_architecture/layers/test_application.yml +3 -0
  146. instant_python/templates/project_structure/clean_architecture/layers/test_delivery.yml +3 -0
  147. instant_python/templates/project_structure/clean_architecture/layers/test_domain.yml +3 -0
  148. instant_python/templates/project_structure/clean_architecture/layers/test_infra.yml +9 -0
  149. instant_python/templates/project_structure/clean_architecture/main_structure.yml +38 -0
  150. instant_python/templates/project_structure/clean_architecture/source.yml +9 -0
  151. instant_python/templates/project_structure/clean_architecture/test.yml +9 -0
  152. instant_python/templates/project_structure/config_files/gitignore.yml +3 -0
  153. instant_python/templates/project_structure/config_files/mypy.yml +4 -0
  154. instant_python/templates/project_structure/config_files/pyproject.yml +4 -0
  155. instant_python/templates/project_structure/config_files/pytest.yml +4 -0
  156. instant_python/templates/project_structure/config_files/python_version.yml +3 -0
  157. instant_python/templates/project_structure/documentation/citation.yml +4 -0
  158. instant_python/templates/project_structure/documentation/license.yml +3 -0
  159. instant_python/templates/project_structure/documentation/readme.yml +4 -0
  160. instant_python/templates/project_structure/documentation/security.yml +4 -0
  161. instant_python/templates/project_structure/domain_driven_design/layers/bounded_context.yml +17 -0
  162. instant_python/templates/project_structure/domain_driven_design/layers/delivery.yml +8 -0
  163. instant_python/templates/project_structure/domain_driven_design/layers/shared.yml +18 -0
  164. instant_python/templates/project_structure/domain_driven_design/layers/shared_domain.yml +10 -0
  165. instant_python/templates/project_structure/domain_driven_design/layers/shared_infra.yml +12 -0
  166. instant_python/templates/project_structure/domain_driven_design/layers/test_shared.yml +8 -0
  167. instant_python/templates/project_structure/domain_driven_design/layers/test_shared_delivery.yml +3 -0
  168. instant_python/templates/project_structure/domain_driven_design/layers/test_shared_domain.yml +3 -0
  169. instant_python/templates/project_structure/domain_driven_design/layers/test_shared_infra.yml +9 -0
  170. instant_python/templates/project_structure/domain_driven_design/main_structure.yml +38 -0
  171. instant_python/templates/project_structure/domain_driven_design/source.yml +10 -0
  172. instant_python/templates/project_structure/domain_driven_design/test.yml +9 -0
  173. instant_python/templates/project_structure/errors.yml +12 -0
  174. instant_python/templates/project_structure/events/event_bus_domain.yml +48 -0
  175. instant_python/templates/project_structure/events/event_bus_infra.yml +40 -0
  176. instant_python/templates/project_structure/events/mock_event_bus.yml +4 -0
  177. instant_python/templates/project_structure/fastapi/fastapi_app.yml +32 -0
  178. instant_python/templates/project_structure/fastapi/fastapi_domain.yml +12 -0
  179. instant_python/templates/project_structure/fastapi/fastapi_infra.yml +12 -0
  180. instant_python/templates/project_structure/github/github_action.yml +24 -0
  181. instant_python/templates/project_structure/github/github_issues_template.yml +14 -0
  182. instant_python/templates/project_structure/github/makefile.yml +3 -0
  183. instant_python/templates/project_structure/github/precommit_hook.yml +4 -0
  184. instant_python/templates/project_structure/logger.yml +16 -0
  185. instant_python/templates/project_structure/macros.j2 +73 -0
  186. instant_python/templates/project_structure/persistence/alembic_migrator.yml +6 -0
  187. instant_python/templates/project_structure/persistence/async_alembic.yml +27 -0
  188. instant_python/templates/project_structure/persistence/async_engine_conftest.yml +4 -0
  189. instant_python/templates/project_structure/persistence/async_sqlalchemy.yml +14 -0
  190. instant_python/templates/project_structure/persistence/persistence.yml +8 -0
  191. instant_python/templates/project_structure/persistence/synchronous_sqlalchemy.yml +20 -0
  192. instant_python/templates/project_structure/standard_project/layers/source_features.yml +13 -0
  193. instant_python/templates/project_structure/standard_project/layers/test_event_bus.yml +6 -0
  194. instant_python/templates/project_structure/standard_project/layers/test_features.yml +6 -0
  195. instant_python/templates/project_structure/standard_project/main_structure.yml +38 -0
  196. instant_python/templates/project_structure/standard_project/source.yml +5 -0
  197. instant_python/templates/project_structure/standard_project/test.yml +5 -0
  198. instant_python-0.20.0.dist-info/METADATA +318 -0
  199. instant_python-0.20.0.dist-info/RECORD +202 -0
  200. instant_python-0.20.0.dist-info/WHEEL +4 -0
  201. instant_python-0.20.0.dist-info/entry_points.txt +2 -0
  202. instant_python-0.20.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,73 @@
1
+ from collections.abc import Iterator
2
+ from enum import Enum
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from instant_python.initialize.domain.project_writer import NodeWriter
8
+
9
+
10
+ from abc import ABC, abstractmethod
11
+
12
+
13
+ class Node(ABC):
14
+ @abstractmethod
15
+ def create(self, writer: "NodeWriter", destination: Path) -> None:
16
+ raise NotImplementedError
17
+
18
+
19
+ class File(Node):
20
+ def __init__(self, name: str, extension: str, content: str | None = None) -> None:
21
+ self._name = name
22
+ self._extension = extension
23
+ self._content = content
24
+
25
+ def __repr__(self) -> str:
26
+ return f"{self.__class__.__name__}(name={self._name}, extension={self._extension})"
27
+
28
+ def create(self, writer: "NodeWriter", destination: Path) -> None:
29
+ file_path = self._build_path_for(destination)
30
+ writer.create_file(file_path, self._content)
31
+
32
+ def is_empty(self) -> bool:
33
+ return self._content is None or self._content == ""
34
+
35
+ def is_pyproject_toml(self) -> bool:
36
+ return self._name == "pyproject" and self._extension == ".toml"
37
+
38
+ def _build_path_for(self, path: Path) -> Path:
39
+ return path / f"{self._name}{self._extension}"
40
+
41
+
42
+ class Directory(Node):
43
+ _INIT_FILE_NAME = "__init__.py"
44
+
45
+ def __init__(self, name: str, is_python_module: bool, children: list[Node]) -> None:
46
+ self._name = name
47
+ self._is_python_module = is_python_module
48
+ self._children = children
49
+
50
+ def __repr__(self) -> str:
51
+ return f"{self.__class__.__name__}(name={self._name}, is_python_module={self._is_python_module})"
52
+
53
+ def __iter__(self) -> Iterator["Node"]:
54
+ return iter(self._children)
55
+
56
+ def create(self, writer: "NodeWriter", destination: Path) -> None:
57
+ directory_path = self._build_path_for(destination)
58
+ writer.create_directory(directory_path)
59
+
60
+ if self._is_python_module:
61
+ init_file_path = directory_path / self._INIT_FILE_NAME
62
+ writer.create_file(init_file_path)
63
+
64
+ for child in self._children:
65
+ child.create(writer, directory_path)
66
+
67
+ def _build_path_for(self, path: Path) -> Path:
68
+ return path / self._name
69
+
70
+
71
+ class NodeType(str, Enum):
72
+ DIRECTORY = "directory"
73
+ FILE = "file"
@@ -0,0 +1,7 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class ProjectFormatter(ABC):
5
+ @abstractmethod
6
+ def format(self) -> None:
7
+ raise NotImplementedError
@@ -0,0 +1,10 @@
1
+ from abc import abstractmethod, ABC
2
+
3
+ from instant_python.shared.domain.config_schema import ConfigSchema
4
+ from instant_python.initialize.domain.project_structure import ProjectStructure
5
+
6
+
7
+ class ProjectRenderer(ABC):
8
+ @abstractmethod
9
+ def render(self, context_config: ConfigSchema) -> ProjectStructure:
10
+ raise NotImplementedError
@@ -0,0 +1,77 @@
1
+ from collections.abc import Iterator
2
+
3
+ from instant_python.initialize.domain.node import Node, NodeType, Directory, File
4
+ from instant_python.shared.application_error import ApplicationError
5
+
6
+
7
+ class ProjectStructure:
8
+ def __init__(self, nodes: list[Node]) -> None:
9
+ self._nodes = nodes
10
+
11
+ @classmethod
12
+ def from_raw_structure(cls, structure: list[dict]) -> "ProjectStructure":
13
+ nodes = cls._build_project_structure(structure)
14
+ cls._ensure_pyproject_file_is_present(nodes)
15
+ return cls(nodes=nodes)
16
+
17
+ def flatten(self) -> Iterator[Node]:
18
+ for node in self._nodes:
19
+ yield node
20
+ if isinstance(node, Directory):
21
+ yield from self._flatten_directory(node)
22
+
23
+ @classmethod
24
+ def _build_project_structure(cls, nodes: list[dict]) -> list[Node]:
25
+ return [cls._build_node(node) for node in nodes]
26
+
27
+ @classmethod
28
+ def _build_node(cls, node: dict) -> Node:
29
+ node_type = node["type"]
30
+ name = node["name"]
31
+
32
+ if node_type == NodeType.DIRECTORY:
33
+ children = node.get("children", [])
34
+ is_python_module = node.get("python", False)
35
+ directory_children = [cls._build_node(child) for child in children]
36
+ return Directory(name=name, is_python_module=is_python_module, children=directory_children)
37
+ elif node_type == NodeType.FILE:
38
+ extension = node.get("extension", "")
39
+ content = node.get("content", None)
40
+ return File(name=name, extension=extension, content=content)
41
+ else:
42
+ raise UnknownNodeTypeError(node_type)
43
+
44
+ @classmethod
45
+ def _ensure_pyproject_file_is_present(cls, nodes: list[Node]) -> None:
46
+ for node in nodes:
47
+ if isinstance(node, File) and node.is_pyproject_toml():
48
+ return
49
+ raise MissingPyprojectTomlError()
50
+
51
+ def __iter__(self) -> Iterator[Node]:
52
+ return iter(self._nodes)
53
+
54
+ def __len__(self) -> int:
55
+ return len(self._nodes)
56
+
57
+ def _flatten_directory(self, directory: Directory) -> Iterator[Node]:
58
+ for child in directory:
59
+ yield child
60
+ if isinstance(child, Directory):
61
+ yield from self._flatten_directory(child)
62
+
63
+
64
+ class UnknownNodeTypeError(ApplicationError):
65
+ def __init__(self, node_type: str) -> None:
66
+ super().__init__(message=f"Unknown node type: {node_type}")
67
+
68
+
69
+ class MissingPyprojectTomlError(ApplicationError):
70
+ def __init__(self) -> None:
71
+ super().__init__(
72
+ message="Missing pyproject.toml file in project structure. Add the following "
73
+ "to your project structure definition:\n"
74
+ "- name: pyproject\n"
75
+ " type: file\n"
76
+ " extension: .toml"
77
+ )
@@ -0,0 +1,20 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+
4
+ from instant_python.initialize.domain.project_structure import ProjectStructure
5
+
6
+
7
+ class ProjectWriter(ABC):
8
+ @abstractmethod
9
+ def write(self, project_structure: ProjectStructure, destination: Path) -> None:
10
+ raise NotImplementedError
11
+
12
+
13
+ class NodeWriter(ABC):
14
+ @abstractmethod
15
+ def create_directory(self, path: Path) -> None:
16
+ raise NotImplementedError
17
+
18
+ @abstractmethod
19
+ def create_file(self, path: Path, content: str | None = None) -> None:
20
+ raise NotImplementedError
@@ -0,0 +1,9 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from instant_python.shared.domain.git_config import GitConfig
4
+
5
+
6
+ class VersionControlConfigurer(ABC):
7
+ @abstractmethod
8
+ def setup(self, config: GitConfig) -> None:
9
+ raise NotImplementedError
File without changes
@@ -0,0 +1,27 @@
1
+ from instant_python.initialize.domain.env_manager import EnvManager
2
+ from instant_python.initialize.infra.env_manager.pdm_env_manager import PdmEnvManager
3
+ from instant_python.initialize.infra.env_manager.system_console import SystemConsole
4
+ from instant_python.initialize.infra.env_manager.uv_env_manager import UvEnvManager
5
+ from instant_python.shared.application_error import ApplicationError
6
+ from instant_python.shared.supported_managers import SupportedManagers
7
+
8
+
9
+ class EnvManagerFactory:
10
+ @staticmethod
11
+ def create(dependency_manager: str, console: SystemConsole) -> EnvManager:
12
+ managers = {
13
+ SupportedManagers.UV: UvEnvManager,
14
+ SupportedManagers.PDM: PdmEnvManager,
15
+ }
16
+ try:
17
+ return managers[SupportedManagers(dependency_manager)](console=console)
18
+ except KeyError:
19
+ raise UnknownDependencyManagerError(dependency_manager)
20
+
21
+
22
+ class UnknownDependencyManagerError(ApplicationError):
23
+ def __init__(self, manager: str) -> None:
24
+ supported_managers = ".".join(SupportedManagers.get_supported_managers())
25
+ super().__init__(
26
+ message=f"Unknown env manager: {manager}. Please use some of the supported managers: '{supported_managers}'."
27
+ )
@@ -0,0 +1,66 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ from instant_python.shared.domain.dependency_config import DependencyConfig
5
+ from instant_python.initialize.domain.env_manager import EnvManager
6
+ from instant_python.initialize.infra.env_manager.system_console import SystemConsole
7
+
8
+
9
+ class PdmEnvManager(EnvManager):
10
+ def __init__(self, console: SystemConsole | None = None) -> None:
11
+ self._console = console
12
+ self._system_os = sys.platform
13
+ self._pdm = self._set_pdm_executable_based_on_os()
14
+
15
+ def setup(self, python_version: str, dependencies: list[DependencyConfig]) -> None:
16
+ if self._pdm_is_not_installed():
17
+ self._install()
18
+ self._install_python(python_version)
19
+ self._install_dependencies(dependencies)
20
+
21
+ def _pdm_is_not_installed(self) -> bool:
22
+ result = self._console.execute(f"{self._pdm} --version")
23
+ return not result.success()
24
+
25
+ def _install(self) -> None:
26
+ print(">>> Installing pdm...")
27
+ self._console.execute_or_raise(self._get_installation_command_based_on_os())
28
+ print(">>> pdm installed successfully")
29
+
30
+ def _set_pdm_executable_based_on_os(self):
31
+ return (
32
+ f"{str(Path.home() / 'AppData' / 'Roaming' / 'Python' / 'Scripts' / 'pdm.exe')}"
33
+ if self._system_os.startswith("win")
34
+ else "~/.local/bin/pdm"
35
+ )
36
+
37
+ def _get_installation_command_based_on_os(self) -> str:
38
+ if self._system_os.startswith("win"):
39
+ return 'powershell -ExecutionPolicy ByPass -c "irm https://pdm-project.org/install-pdm.py | py -"'
40
+ return "curl -sSL https://pdm-project.org/install-pdm.py | python3 -"
41
+
42
+ def _install_python(self, version: str) -> None:
43
+ print(f">>> Installing Python {version}...")
44
+ self._console.execute_or_raise(f"{self._pdm} python install {version}")
45
+ print(f">>> Python {version} installed successfully")
46
+
47
+ def _install_dependencies(self, dependencies: list[DependencyConfig]) -> None:
48
+ self._create_virtual_environment()
49
+ print(">>> Installing dependencies...")
50
+ for dependency in dependencies:
51
+ self._install_dependency(dependency)
52
+ print(">>> Dependencies installed successfully")
53
+
54
+ def _install_dependency(self, dependency: DependencyConfig) -> None:
55
+ command = self._build_dependency_install_command(dependency)
56
+ self._console.execute_or_raise(command)
57
+
58
+ def _build_dependency_install_command(self, dependency: DependencyConfig) -> str:
59
+ command = [f"{self._pdm} add"]
60
+ command.extend(dependency.get_installation_flag())
61
+ command.append(dependency.get_specification())
62
+
63
+ return " ".join(command)
64
+
65
+ def _create_virtual_environment(self) -> None:
66
+ self._console.execute_or_raise(f"{self._pdm} install")
@@ -0,0 +1,65 @@
1
+ import subprocess
2
+ from dataclasses import dataclass
3
+
4
+ from instant_python.shared.application_error import ApplicationError
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class CommandExecutionResult:
9
+ exit_code: int
10
+ stdout: str
11
+ stderr: str
12
+
13
+ def success(self) -> bool:
14
+ return self.exit_code == 0
15
+
16
+
17
+ class SystemConsole:
18
+ def __init__(self, working_directory: str) -> None:
19
+ self._working_directory = working_directory
20
+
21
+ def execute(self, command: str) -> CommandExecutionResult:
22
+ try:
23
+ return self._run_command(command)
24
+ except Exception as error:
25
+ return self._unexpected_error_result(error)
26
+
27
+ def execute_or_raise(self, command: str) -> CommandExecutionResult:
28
+ result = self.execute(command)
29
+ if not result.success():
30
+ raise CommandExecutionError(
31
+ exit_code=result.exit_code,
32
+ stderr_output=result.stderr,
33
+ )
34
+ return result
35
+
36
+ def _run_command(self, command: str) -> CommandExecutionResult:
37
+ result = subprocess.run(
38
+ command,
39
+ shell=True,
40
+ check=False,
41
+ cwd=self._working_directory,
42
+ capture_output=True,
43
+ text=True,
44
+ )
45
+ return CommandExecutionResult(
46
+ exit_code=result.returncode,
47
+ stdout=result.stdout,
48
+ stderr=result.stderr,
49
+ )
50
+
51
+ @staticmethod
52
+ def _unexpected_error_result(error: Exception) -> CommandExecutionResult:
53
+ return CommandExecutionResult(
54
+ exit_code=-1,
55
+ stdout="",
56
+ stderr=str(error),
57
+ )
58
+
59
+
60
+ class CommandExecutionError(ApplicationError):
61
+ def __init__(self, exit_code: int, stderr_output: str = None) -> None:
62
+ message = f"Unexpected error when executing a command, exit code {exit_code}"
63
+ if stderr_output:
64
+ message += f": {stderr_output}"
65
+ super().__init__(message=message)
@@ -0,0 +1,74 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ from instant_python.shared.domain.dependency_config import DependencyConfig
5
+ from instant_python.initialize.domain.env_manager import EnvManager
6
+ from instant_python.initialize.infra.env_manager.system_console import SystemConsole
7
+
8
+
9
+ class UvEnvManager(EnvManager):
10
+ def __init__(self, console: SystemConsole | None = None) -> None:
11
+ self._console = console
12
+ self._system_os = sys.platform
13
+ self._uv = self._set_uv_executable_based_on_os()
14
+
15
+ def setup(self, python_version: str, dependencies: list[DependencyConfig]) -> None:
16
+ if self._uv_is_not_installed():
17
+ self._install()
18
+ self._install_python(python_version)
19
+ self._install_dependencies(dependencies)
20
+
21
+ def _install(self) -> None:
22
+ print(">>> Installing uv...")
23
+ self._console.execute_or_raise(self._get_installation_command_based_on_os())
24
+ print(">>> uv installed successfully")
25
+ if self._system_os.startswith("win"):
26
+ print(
27
+ ">>> Remember to add uv to your PATH environment variable. You can do this:\n"
28
+ " 1. Running the following command if you use cmd:\n"
29
+ " set Path=%Path%;%USERPROFILE%\\.local\\bin\n"
30
+ " 2. Running the following command if you use PowerShell:\n"
31
+ " $env:Path = '$env:USERPROFILE\\.local\\bin;$env:Path'\n"
32
+ " 3. Restarting your shell."
33
+ )
34
+
35
+ def _get_installation_command_based_on_os(self) -> str:
36
+ if self._system_os.startswith("win"):
37
+ return 'powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"'
38
+ return "curl -LsSf https://astral.sh/uv/install.sh | sh"
39
+
40
+ def _set_uv_executable_based_on_os(self):
41
+ return (
42
+ f"{str(Path.home() / '.local' / 'bin' / 'uv.exe')}"
43
+ if self._system_os.startswith("win")
44
+ else "~/.local/bin/uv"
45
+ )
46
+
47
+ def _install_python(self, version: str) -> None:
48
+ print(f">>> Installing Python {version}...")
49
+ self._console.execute_or_raise(f"{self._uv} python install {version}")
50
+ print(f">>> Python {version} installed successfully")
51
+
52
+ def _install_dependencies(self, dependencies: list[DependencyConfig]) -> None:
53
+ self._create_virtual_environment()
54
+ print(">>> Installing dependencies...")
55
+ for dependency in dependencies:
56
+ self._install_dependency(dependency)
57
+ print(">>> Dependencies installed successfully")
58
+
59
+ def _install_dependency(self, dependency: DependencyConfig) -> None:
60
+ command = self._build_dependency_install_command(dependency)
61
+ self._console.execute_or_raise(command)
62
+
63
+ def _build_dependency_install_command(self, dependency: DependencyConfig) -> str:
64
+ command = [f"{self._uv} add"]
65
+ command.extend(dependency.get_installation_flag())
66
+ command.append(dependency.get_specification())
67
+ return " ".join(command)
68
+
69
+ def _create_virtual_environment(self) -> None:
70
+ self._console.execute_or_raise(f"{self._uv} sync --all-groups")
71
+
72
+ def _uv_is_not_installed(self) -> bool:
73
+ result = self._console.execute(f"{self._uv} --version")
74
+ return not result.success()
File without changes
@@ -0,0 +1,10 @@
1
+ from instant_python.initialize.domain.project_formatter import ProjectFormatter
2
+ from instant_python.initialize.infra.env_manager.system_console import SystemConsole
3
+
4
+
5
+ class RuffProjectFormatter(ProjectFormatter):
6
+ def __init__(self, console: SystemConsole) -> None:
7
+ self._console = console
8
+
9
+ def format(self) -> None:
10
+ self._console.execute_or_raise(command="uvx ruff format")
File without changes
@@ -0,0 +1,71 @@
1
+ from collections.abc import Callable
2
+ from typing import Any
3
+
4
+ from instant_python.shared.application_error import ApplicationError
5
+ from instant_python.shared.supported_templates import SupportedTemplates
6
+ from jinja2 import Environment, FileSystemLoader, ChoiceLoader, PackageLoader
7
+
8
+
9
+ class JinjaEnvironment:
10
+ _EMPTY_CONTEXT = {}
11
+ _BASE_PACKAGE_NAME = "instant_python"
12
+ _PROJECT_STRUCTURE_TEMPLATE_PATH = "templates/project_structure"
13
+ _BOILERPLATE_TEMPLATE_PATH = "templates/boilerplate"
14
+
15
+ def __init__(self, user_template_path: str | None = None) -> None:
16
+ self._env = Environment(
17
+ loader=ChoiceLoader(
18
+ [
19
+ FileSystemLoader(user_template_path if user_template_path else []),
20
+ PackageLoader(
21
+ package_name=self._BASE_PACKAGE_NAME, package_path=self._PROJECT_STRUCTURE_TEMPLATE_PATH
22
+ ),
23
+ PackageLoader(package_name=self._BASE_PACKAGE_NAME, package_path=self._BOILERPLATE_TEMPLATE_PATH),
24
+ ]
25
+ ),
26
+ trim_blocks=True,
27
+ lstrip_blocks=True,
28
+ autoescape=True,
29
+ )
30
+ self.add_filter("is_in", _is_in)
31
+ self.add_filter("compute_base_path", _compute_base_path)
32
+ self.add_filter("has_dependency", _has_dependency)
33
+ self.add_filter("resolve_import_path", _resolve_import_path)
34
+
35
+ def render_template(self, name: str, context: dict[str, Any] | None = None) -> str:
36
+ template = self._env.get_template(name)
37
+ return template.render(**(context or self._EMPTY_CONTEXT))
38
+
39
+ def add_filter(self, name: str, filter_: Callable) -> None:
40
+ self._env.filters[name] = filter_
41
+
42
+
43
+ class UnknownTemplateError(ApplicationError):
44
+ def __init__(self, template_name: str) -> None:
45
+ super().__init__(message=f"Unknown template type: {template_name}")
46
+
47
+
48
+ def _is_in(values: list[str], container: list) -> bool:
49
+ return any(value in container for value in values)
50
+
51
+
52
+ def _has_dependency(dependencies: list[dict], dependency_name: str) -> bool:
53
+ return any(dep.get("name") == dependency_name for dep in dependencies)
54
+
55
+
56
+ def _compute_base_path(initial_path: str, template_type: str) -> str:
57
+ if template_type == SupportedTemplates.DDD:
58
+ return initial_path
59
+
60
+ path_components = initial_path.split(".")
61
+ if template_type == SupportedTemplates.CLEAN:
62
+ return ".".join(path_components[1:])
63
+ elif template_type == SupportedTemplates.STANDARD:
64
+ return ".".join(path_components[2:])
65
+ else:
66
+ raise UnknownTemplateError(template_type)
67
+
68
+
69
+ def _resolve_import_path(initial_path: str, template_type: str) -> str:
70
+ base_path = _compute_base_path(initial_path, template_type)
71
+ return f".{base_path}" if base_path else ""
@@ -0,0 +1,57 @@
1
+ from pathlib import Path
2
+
3
+ import yaml
4
+ from jinja2 import TemplateNotFound
5
+
6
+ from instant_python.shared.domain.config_schema import ConfigSchema
7
+ from instant_python.initialize.domain.node import NodeType
8
+ from instant_python.initialize.domain.project_renderer import ProjectRenderer
9
+ from instant_python.initialize.domain.project_structure import ProjectStructure
10
+ from instant_python.initialize.infra.renderer.jinja_environment import JinjaEnvironment
11
+ from instant_python.shared.supported_templates import SupportedTemplates
12
+
13
+
14
+ class JinjaProjectRenderer(ProjectRenderer):
15
+ _MAIN_STRUCTURE_TEMPLATE_FILE = "main_structure.yml"
16
+
17
+ def __init__(self, env: JinjaEnvironment) -> None:
18
+ self._env = env
19
+
20
+ def render(self, context_config: ConfigSchema) -> ProjectStructure:
21
+ template_name = self._get_project_main_structure_template(context_config)
22
+ basic_project_structure = self._render_project_structure_with_jinja(context_config, template_name)
23
+ project_structure_with_files_content = self._add_template_content_to_files(
24
+ context_config, basic_project_structure
25
+ )
26
+ return ProjectStructure.from_raw_structure(structure=project_structure_with_files_content)
27
+
28
+ def _render_project_structure_with_jinja(self, context_config: ConfigSchema, template_name: str) -> list[dict]:
29
+ raw_project_structure = self._env.render_template(name=template_name, context=context_config.to_primitives())
30
+ return yaml.safe_load(raw_project_structure)
31
+
32
+ def _get_project_main_structure_template(self, config: ConfigSchema) -> str:
33
+ return str(Path(config.calculate_project_structure_template_name()) / self._MAIN_STRUCTURE_TEMPLATE_FILE)
34
+
35
+ def _add_template_content_to_files(self, context_config: ConfigSchema, project_structure: list[dict]) -> list[dict]:
36
+ for node in project_structure:
37
+ self._populate_file_content(context_config, node)
38
+ return project_structure
39
+
40
+ def _populate_file_content(self, context_config: ConfigSchema, node: dict) -> None:
41
+ if node.get("type") == NodeType.FILE:
42
+ try:
43
+ template_name = node.get("template") or f"{node['name']}{node['extension']}"
44
+ file_content = self._env.render_template(
45
+ name=template_name,
46
+ context={
47
+ **context_config.to_primitives(),
48
+ "template_types": SupportedTemplates,
49
+ },
50
+ )
51
+ except (TemplateNotFound, KeyError):
52
+ print(f"Warning: Template not found for file {node.get('name')}, leaving content empty.")
53
+ file_content = None
54
+ node["content"] = file_content
55
+
56
+ for child in node.get("children", []):
57
+ self._populate_file_content(context_config, child)
@@ -0,0 +1,29 @@
1
+ from instant_python.shared.domain.git_config import GitConfig
2
+ from instant_python.initialize.domain.version_control_configurer import VersionControlConfigurer
3
+ from instant_python.initialize.infra.env_manager.system_console import SystemConsole
4
+
5
+
6
+ class GitConfigurer(VersionControlConfigurer):
7
+ def __init__(self, console: SystemConsole) -> None:
8
+ self._console = console
9
+
10
+ def setup(self, config: GitConfig) -> None:
11
+ print(">>> Setting up git repository...")
12
+ self._initialize_repository()
13
+ self._set_user_information(
14
+ username=config.username,
15
+ email=config.email,
16
+ )
17
+ self._make_initial_commit()
18
+ print(">>> Git repository created successfully")
19
+
20
+ def _initialize_repository(self) -> None:
21
+ self._console.execute_or_raise(command="git init")
22
+
23
+ def _set_user_information(self, username: str, email: str) -> None:
24
+ self._console.execute(command=f"git config user.name {username}")
25
+ self._console.execute(command=f"git config user.email {email}")
26
+
27
+ def _make_initial_commit(self) -> None:
28
+ self._console.execute(command="git add .")
29
+ self._console.execute(command='git commit -m "🎉 chore: initial commit"')
File without changes
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+
3
+ from instant_python.initialize.domain.project_structure import ProjectStructure
4
+ from instant_python.initialize.domain.project_writer import ProjectWriter, NodeWriter
5
+
6
+
7
+ class FileSystemNodeWriter(NodeWriter):
8
+ def create_directory(self, path: Path) -> None:
9
+ path.mkdir(parents=True, exist_ok=True)
10
+
11
+ def create_file(self, path: Path, content: str | None = None) -> None:
12
+ path.touch(exist_ok=True)
13
+ if content is not None:
14
+ path.write_text(content)
15
+
16
+
17
+ class FileSystemProjectWriter(ProjectWriter):
18
+ def __init__(self) -> None:
19
+ self._node_writer = FileSystemNodeWriter()
20
+
21
+ def write(self, project_structure: ProjectStructure, destination: Path) -> None:
22
+ for node in project_structure:
23
+ node.create(writer=self._node_writer, destination=destination)
File without changes
@@ -0,0 +1,8 @@
1
+ class ApplicationError(Exception):
2
+ def __init__(self, message: str) -> None:
3
+ self._message = message
4
+ super().__init__(self._message)
5
+
6
+ @property
7
+ def message(self) -> str:
8
+ return self._message
File without changes