instant-python 0.5.2__py3-none-any.whl → 0.6.1__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 (176) hide show
  1. instant_python/cli.py +5 -5
  2. instant_python/commands/config.py +28 -0
  3. instant_python/commands/init.py +53 -0
  4. instant_python/configuration/configuration_schema.py +85 -0
  5. instant_python/configuration/dependency/dependency_configuration.py +36 -0
  6. instant_python/configuration/dependency/not_dev_dependency_included_in_group.py +8 -0
  7. instant_python/configuration/general/general_configuration.py +60 -0
  8. instant_python/configuration/general/invalid_dependency_manager_value.py +8 -0
  9. instant_python/configuration/general/invalid_license_value.py +8 -0
  10. instant_python/configuration/general/invalid_python_version_value.py +8 -0
  11. instant_python/configuration/git/git_configuration.py +23 -0
  12. instant_python/configuration/git/git_user_or_email_not_present.py +8 -0
  13. instant_python/configuration/parser/config_key_not_present.py +8 -0
  14. instant_python/configuration/parser/configuration_file_not_found.py +8 -0
  15. instant_python/configuration/parser/empty_configuration_not_allowed.py +8 -0
  16. instant_python/configuration/parser/missing_mandatory_fields.py +10 -0
  17. instant_python/configuration/parser/parser.py +148 -0
  18. instant_python/configuration/question/boolean_question.py +12 -0
  19. instant_python/configuration/question/choice_question.py +19 -0
  20. instant_python/{question_prompter → configuration}/question/conditional_question.py +6 -3
  21. instant_python/configuration/question/free_text_question.py +14 -0
  22. instant_python/configuration/question/multiple_choice_question.py +12 -0
  23. instant_python/{question_prompter → configuration}/question/question.py +6 -2
  24. instant_python/configuration/question/questionary.py +17 -0
  25. instant_python/configuration/question_wizard.py +14 -0
  26. instant_python/configuration/step/__init__.py +0 -0
  27. instant_python/configuration/step/dependencies_step.py +70 -0
  28. instant_python/configuration/step/general_step.py +67 -0
  29. instant_python/configuration/step/git_step.py +41 -0
  30. instant_python/{question_prompter → configuration}/step/steps.py +6 -1
  31. instant_python/configuration/step/template_step.py +63 -0
  32. instant_python/configuration/template/__init__.py +0 -0
  33. instant_python/configuration/template/bounded_context_not_applicable.py +8 -0
  34. instant_python/configuration/template/bounded_context_not_especified.py +8 -0
  35. instant_python/configuration/template/invalid_built_in_features_values.py +10 -0
  36. instant_python/configuration/template/invalid_template_value.py +8 -0
  37. instant_python/configuration/template/template_configuration.py +59 -0
  38. instant_python/dependency_manager/__init__.py +0 -0
  39. instant_python/dependency_manager/command_execution_error.py +10 -0
  40. instant_python/dependency_manager/dependency_manager.py +23 -0
  41. instant_python/dependency_manager/dependency_manager_factory.py +18 -0
  42. instant_python/dependency_manager/pdm_dependency_manager.py +46 -0
  43. instant_python/dependency_manager/unknown_dependency_manager_error.py +8 -0
  44. instant_python/dependency_manager/uv_dependency_manager.py +45 -0
  45. instant_python/git/__init__.py +0 -0
  46. instant_python/git/git_configurer.py +42 -0
  47. instant_python/{intant_python_typer.py → instant_python_typer.py} +5 -4
  48. instant_python/project_creator/__init__.py +0 -0
  49. instant_python/project_creator/directory.py +26 -0
  50. instant_python/project_creator/file.py +31 -0
  51. instant_python/project_creator/file_has_not_been_created.py +8 -0
  52. instant_python/project_creator/file_system.py +46 -0
  53. instant_python/project_creator/unknown_node_typer_error.py +8 -0
  54. instant_python/render/__init__.py +0 -0
  55. instant_python/{project_generator/custom_template_manager.py → render/custom_project_renderer.py} +6 -5
  56. instant_python/render/jinja_custom_filters.py +23 -0
  57. instant_python/render/jinja_environment.py +28 -0
  58. instant_python/render/jinja_project_renderer.py +30 -0
  59. instant_python/render/template_file_not_found_error.py +10 -0
  60. instant_python/render/unknown_template_error.py +8 -0
  61. instant_python/shared/__init__.py +0 -0
  62. instant_python/shared/application_error.py +16 -0
  63. instant_python/shared/error_types.py +7 -0
  64. instant_python/shared/supported_built_in_features.py +16 -0
  65. instant_python/shared/supported_licenses.py +11 -0
  66. instant_python/shared/supported_managers.py +10 -0
  67. instant_python/shared/supported_python_versions.py +12 -0
  68. instant_python/shared/supported_templates.py +12 -0
  69. instant_python/templates/boilerplate/.python-version +1 -1
  70. instant_python/templates/boilerplate/LICENSE +6 -6
  71. instant_python/templates/boilerplate/README.md +5 -2
  72. instant_python/templates/boilerplate/event_bus/aggregate_root.py +2 -2
  73. instant_python/templates/boilerplate/event_bus/domain_event_json_deserializer.py +4 -4
  74. instant_python/templates/boilerplate/event_bus/domain_event_json_serializer.py +2 -2
  75. instant_python/templates/boilerplate/event_bus/domain_event_subscriber.py +2 -2
  76. instant_python/templates/boilerplate/event_bus/event_bus.py +2 -2
  77. instant_python/templates/boilerplate/event_bus/exchange_type.py +1 -1
  78. instant_python/templates/boilerplate/event_bus/mock_event_bus.py +3 -3
  79. instant_python/templates/boilerplate/event_bus/rabbit_mq_configurer.py +6 -6
  80. instant_python/templates/boilerplate/event_bus/rabbit_mq_connection.py +5 -5
  81. instant_python/templates/boilerplate/event_bus/rabbit_mq_consumer.py +7 -7
  82. instant_python/templates/boilerplate/event_bus/rabbit_mq_event_bus.py +6 -6
  83. instant_python/templates/boilerplate/event_bus/rabbit_mq_queue_formatter.py +3 -3
  84. instant_python/templates/boilerplate/exceptions/domain_error.py +17 -12
  85. instant_python/templates/boilerplate/exceptions/domain_event_type_not_found_error.py +3 -11
  86. instant_python/templates/boilerplate/exceptions/incorrect_value_type_error.py +3 -11
  87. instant_python/templates/boilerplate/exceptions/invalid_id_format_error.py +3 -11
  88. instant_python/templates/boilerplate/exceptions/invalid_negative_value_error.py +3 -11
  89. instant_python/templates/boilerplate/exceptions/rabbit_mq_connection_not_established_error.py +3 -11
  90. instant_python/templates/boilerplate/exceptions/required_value_error.py +3 -11
  91. instant_python/templates/boilerplate/fastapi/application.py +8 -8
  92. instant_python/templates/boilerplate/fastapi/http_response.py +7 -7
  93. instant_python/templates/boilerplate/fastapi/lifespan.py +2 -2
  94. instant_python/templates/boilerplate/github/action.yml +3 -3
  95. instant_python/templates/boilerplate/logger/logger.py +2 -2
  96. instant_python/templates/boilerplate/mypy.ini +1 -1
  97. instant_python/templates/boilerplate/persistence/alembic_migrator.py +2 -2
  98. instant_python/templates/boilerplate/persistence/async/async_engine_fixture.py +2 -2
  99. instant_python/templates/boilerplate/persistence/async/env.py +2 -2
  100. instant_python/templates/boilerplate/persistence/async/models_metadata.py +2 -2
  101. instant_python/templates/boilerplate/persistence/async/sqlalchemy_repository.py +4 -4
  102. instant_python/templates/boilerplate/persistence/synchronous/session_maker.py +2 -2
  103. instant_python/templates/boilerplate/persistence/synchronous/sqlalchemy_repository.py +7 -7
  104. instant_python/templates/boilerplate/pyproject.toml +21 -10
  105. instant_python/templates/boilerplate/scripts/add_dependency.sh +2 -2
  106. instant_python/templates/boilerplate/scripts/integration.sh +1 -1
  107. instant_python/templates/boilerplate/scripts/makefile +26 -26
  108. instant_python/templates/boilerplate/scripts/remove_dependency.sh +2 -2
  109. instant_python/templates/boilerplate/scripts/unit.sh +1 -1
  110. instant_python/templates/boilerplate/value_object/int_value_object.py +3 -3
  111. instant_python/templates/boilerplate/value_object/string_value_object.py +4 -4
  112. instant_python/templates/boilerplate/value_object/uuid.py +3 -3
  113. instant_python/templates/boilerplate/value_object/value_object.py +1 -1
  114. instant_python/templates/project_structure/clean_architecture/main_structure.yml.j2 +24 -25
  115. instant_python/templates/project_structure/clean_architecture/source.yml.j2 +12 -12
  116. instant_python/templates/project_structure/clean_architecture/test.yml.j2 +1 -1
  117. instant_python/templates/project_structure/domain_driven_design/bounded_context.yml.j2 +2 -2
  118. instant_python/templates/project_structure/domain_driven_design/main_structure.yml.j2 +24 -25
  119. instant_python/templates/project_structure/domain_driven_design/source.yml.j2 +14 -14
  120. instant_python/templates/project_structure/domain_driven_design/test.yml.j2 +2 -2
  121. instant_python/templates/project_structure/makefile.yml.j2 +1 -1
  122. instant_python/templates/project_structure/standard_project/main_structure.yml.j2 +24 -25
  123. instant_python/templates/project_structure/standard_project/source.yml.j2 +9 -9
  124. instant_python/templates/project_structure/standard_project/test.yml.j2 +1 -1
  125. {instant_python-0.5.2.dist-info → instant_python-0.6.1.dist-info}/METADATA +69 -36
  126. instant_python-0.6.1.dist-info/RECORD +186 -0
  127. instant_python/errors/application_error.py +0 -11
  128. instant_python/errors/command_execution_error.py +0 -20
  129. instant_python/errors/error_types.py +0 -6
  130. instant_python/errors/template_file_not_found_error.py +0 -18
  131. instant_python/errors/unknown_dependency_manager_error.py +0 -18
  132. instant_python/errors/unknown_node_typer_error.py +0 -16
  133. instant_python/errors/unknown_template_error.py +0 -16
  134. instant_python/folder_cli.py +0 -50
  135. instant_python/installer/dependency_manager.py +0 -15
  136. instant_python/installer/dependency_manager_factory.py +0 -18
  137. instant_python/installer/git_configurer.py +0 -50
  138. instant_python/installer/installer.py +0 -24
  139. instant_python/installer/managers.py +0 -6
  140. instant_python/installer/pdm_manager.py +0 -86
  141. instant_python/installer/uv_manager.py +0 -88
  142. instant_python/project_cli.py +0 -100
  143. instant_python/project_generator/boilerplate_file.py +0 -20
  144. instant_python/project_generator/directory.py +0 -28
  145. instant_python/project_generator/file.py +0 -16
  146. instant_python/project_generator/folder_tree.py +0 -39
  147. instant_python/project_generator/jinja_custom_filters.py +0 -19
  148. instant_python/project_generator/jinja_environment.py +0 -20
  149. instant_python/project_generator/jinja_template_manager.py +0 -37
  150. instant_python/project_generator/project_generator.py +0 -36
  151. instant_python/project_generator/template_manager.py +0 -7
  152. instant_python/question_prompter/question/boolean_question.py +0 -13
  153. instant_python/question_prompter/question/choice_question.py +0 -18
  154. instant_python/question_prompter/question/dependencies_question.py +0 -43
  155. instant_python/question_prompter/question/free_text_question.py +0 -13
  156. instant_python/question_prompter/question/multiple_choice_question.py +0 -13
  157. instant_python/question_prompter/question_wizard.py +0 -15
  158. instant_python/question_prompter/requirements_configuration.py +0 -40
  159. instant_python/question_prompter/step/dependencies_step.py +0 -20
  160. instant_python/question_prompter/step/general_custom_template_project_step.py +0 -45
  161. instant_python/question_prompter/step/general_project_step.py +0 -50
  162. instant_python/question_prompter/step/git_step.py +0 -23
  163. instant_python/question_prompter/step/template_step.py +0 -71
  164. instant_python/question_prompter/template_types.py +0 -7
  165. instant_python-0.5.2.dist-info/RECORD +0 -162
  166. /instant_python/{errors → commands}/__init__.py +0 -0
  167. /instant_python/{installer → configuration}/__init__.py +0 -0
  168. /instant_python/{project_generator → configuration/dependency}/__init__.py +0 -0
  169. /instant_python/{question_prompter → configuration/general}/__init__.py +0 -0
  170. /instant_python/{question_prompter/question → configuration/git}/__init__.py +0 -0
  171. /instant_python/{question_prompter/step → configuration/parser}/__init__.py +0 -0
  172. /instant_python/{templates → configuration/question}/__init__.py +0 -0
  173. /instant_python/{project_generator → project_creator}/node.py +0 -0
  174. {instant_python-0.5.2.dist-info → instant_python-0.6.1.dist-info}/WHEEL +0 -0
  175. {instant_python-0.5.2.dist-info → instant_python-0.6.1.dist-info}/entry_points.txt +0 -0
  176. {instant_python-0.5.2.dist-info → instant_python-0.6.1.dist-info}/licenses/LICENSE +0 -0
instant_python/cli.py CHANGED
@@ -1,15 +1,15 @@
1
1
  from rich.console import Console
2
2
  from rich.panel import Panel
3
3
 
4
- from instant_python import folder_cli, project_cli
5
- from instant_python.errors.application_error import ApplicationError
6
- from instant_python.intant_python_typer import InstantPythonTyper
4
+ from instant_python.commands import init, config
5
+ from instant_python.shared.application_error import ApplicationError
6
+ from instant_python.instant_python_typer import InstantPythonTyper
7
7
 
8
8
  app = InstantPythonTyper()
9
9
  console = Console()
10
10
 
11
- app.add_typer(folder_cli.app, name="folder", help="Generate only the folder structure for a new project")
12
- app.add_typer(project_cli.app, name="project", help="Generate a full project ready to be used")
11
+ app.add_typer(init.app)
12
+ app.add_typer(config.app)
13
13
 
14
14
 
15
15
  @app.error_handler(ApplicationError)
@@ -0,0 +1,28 @@
1
+ import typer
2
+
3
+ from instant_python.configuration.parser.parser import Parser
4
+ from instant_python.configuration.question.questionary import Questionary
5
+ from instant_python.configuration.question_wizard import QuestionWizard
6
+ from instant_python.configuration.step.dependencies_step import DependenciesStep
7
+ from instant_python.configuration.step.general_step import GeneralStep
8
+ from instant_python.configuration.step.git_step import GitStep
9
+ from instant_python.configuration.step.steps import Steps
10
+ from instant_python.configuration.step.template_step import TemplateStep
11
+
12
+ app = typer.Typer()
13
+
14
+
15
+ @app.command("config", help="Generate the configuration file for a new project")
16
+ def create_new_project() -> None:
17
+ questionary = Questionary()
18
+ steps = Steps(
19
+ GeneralStep(questionary=questionary),
20
+ TemplateStep(questionary=questionary),
21
+ GitStep(questionary=questionary),
22
+ DependenciesStep(questionary=questionary),
23
+ )
24
+
25
+ question_wizard = QuestionWizard(steps=steps)
26
+ configuration = question_wizard.run()
27
+ validated_configuration = Parser.parse_from_answers(configuration)
28
+ validated_configuration.save_on_current_directory()
@@ -0,0 +1,53 @@
1
+ import typer
2
+
3
+ from instant_python.configuration.parser.parser import Parser
4
+ from instant_python.dependency_manager.dependency_manager_factory import DependencyManagerFactory
5
+ from instant_python.git.git_configurer import GitConfigurer
6
+ from instant_python.project_creator.file_system import FileSystem
7
+ from instant_python.render.custom_project_renderer import CustomProjectRenderer
8
+ from instant_python.render.jinja_environment import JinjaEnvironment
9
+ from instant_python.render.jinja_project_renderer import JinjaProjectRenderer
10
+
11
+ app = typer.Typer()
12
+
13
+
14
+ @app.command("init", help="Create a new project")
15
+ def create_new_project(
16
+ config_file: str = typer.Option("ipy.yml", "--config", "-c", help="Path to yml configuration file"),
17
+ template: str | None = typer.Option(None, "--template", "-t", help="Path to custom template file"),
18
+ ) -> None:
19
+ configuration = Parser.parse_from_file(config_file_path=config_file)
20
+ environment = JinjaEnvironment(package_name="instant_python", template_directory="templates")
21
+
22
+ if template:
23
+ project_renderer = CustomProjectRenderer(template_path=template)
24
+ project_structure = project_renderer.render_project_structure()
25
+ else:
26
+ project_renderer = JinjaProjectRenderer(jinja_environment=environment)
27
+ project_structure = project_renderer.render_project_structure(
28
+ context_config=configuration,
29
+ template_base_dir="project_structure",
30
+ )
31
+
32
+ file_system = FileSystem(project_structure=project_structure)
33
+ file_system.write_on_disk(
34
+ file_renderer=environment,
35
+ context=configuration,
36
+ )
37
+
38
+ dependency_manager = DependencyManagerFactory.create(
39
+ dependency_manager=configuration.dependency_manager,
40
+ project_directory=configuration.project_folder_name,
41
+ )
42
+ dependency_manager.setup_environment(
43
+ python_version=configuration.python_version,
44
+ dependencies=configuration.dependencies,
45
+ )
46
+
47
+ git_configurer = GitConfigurer(project_directory=configuration.project_folder_name)
48
+ git_configurer.setup_repository(configuration.git)
49
+ configuration.save_on_project_folder()
50
+
51
+
52
+ if __name__ == "__main__":
53
+ app()
@@ -0,0 +1,85 @@
1
+ import shutil
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+ from typing import TypedDict, Self, Union
5
+
6
+ import yaml
7
+
8
+ from instant_python.configuration.dependency.dependency_configuration import (
9
+ DependencyConfiguration,
10
+ )
11
+ from instant_python.configuration.general.general_configuration import (
12
+ GeneralConfiguration,
13
+ )
14
+ from instant_python.configuration.git.git_configuration import GitConfiguration
15
+ from instant_python.configuration.template.template_configuration import (
16
+ TemplateConfiguration,
17
+ )
18
+
19
+
20
+ @dataclass
21
+ class ConfigurationSchema:
22
+ general: GeneralConfiguration
23
+ dependencies: list[DependencyConfiguration]
24
+ template: TemplateConfiguration
25
+ git: GitConfiguration
26
+ _config_file_path: Path = field(default_factory=lambda: Path("ipy.yml"))
27
+
28
+ @classmethod
29
+ def from_file(
30
+ cls,
31
+ config_file_path: str,
32
+ general: GeneralConfiguration,
33
+ dependencies: list[DependencyConfiguration],
34
+ template: TemplateConfiguration,
35
+ git: GitConfiguration,
36
+ ) -> Self:
37
+ return cls(
38
+ general=general,
39
+ dependencies=dependencies,
40
+ template=template,
41
+ git=git,
42
+ _config_file_path=Path(config_file_path),
43
+ )
44
+
45
+ def save_on_project_folder(self) -> None:
46
+ destination_folder = Path.cwd() / self.project_folder_name
47
+ destination_path = destination_folder / self._config_file_path.name
48
+
49
+ shutil.move(self._config_file_path, destination_path)
50
+
51
+ def save_on_current_directory(self) -> None:
52
+ destination_folder = Path.cwd() / self._config_file_path
53
+ with open(destination_folder, "w") as file:
54
+ yaml.dump(self.to_primitives(), file)
55
+
56
+ def to_primitives(self) -> "ConfigurationSchemaPrimitives":
57
+ return ConfigurationSchemaPrimitives(
58
+ general=self.general.to_primitives(),
59
+ dependencies=[dependency.to_primitives() for dependency in self.dependencies],
60
+ template=self.template.to_primitives(),
61
+ git=self.git.to_primitives(),
62
+ )
63
+
64
+ @property
65
+ def template_type(self) -> str:
66
+ return self.template.name
67
+
68
+ @property
69
+ def project_folder_name(self) -> str:
70
+ return self.general.slug
71
+
72
+ @property
73
+ def dependency_manager(self) -> str:
74
+ return self.general.dependency_manager
75
+
76
+ @property
77
+ def python_version(self) -> str:
78
+ return self.general.python_version
79
+
80
+
81
+ class ConfigurationSchemaPrimitives(TypedDict):
82
+ general: dict[str, str]
83
+ dependencies: list[dict[str, Union[str, bool]]]
84
+ template: dict[str, Union[str, list[str]]]
85
+ git: dict[str, Union[str, bool]]
@@ -0,0 +1,36 @@
1
+ from dataclasses import dataclass, field, asdict
2
+
3
+ from instant_python.configuration.dependency.not_dev_dependency_included_in_group import (
4
+ NotDevDependencyIncludedInGroup,
5
+ )
6
+
7
+
8
+ @dataclass
9
+ class DependencyConfiguration:
10
+ name: str
11
+ version: str
12
+ is_dev: bool = field(default=False)
13
+ group: str = field(default_factory=str)
14
+
15
+ def __post_init__(self) -> None:
16
+ self.version = str(self.version)
17
+ self._ensure_dependency_is_dev_if_group_is_set()
18
+
19
+ def to_primitives(self) -> dict[str, str | bool]:
20
+ return asdict(self)
21
+
22
+ def get_installation_flag(self) -> tuple[str, ...]:
23
+ if self.group:
24
+ return (f"--group {self.group}",)
25
+ elif self.is_dev:
26
+ return ("--dev",)
27
+ return tuple()
28
+
29
+ def get_specification(self) -> str:
30
+ if self.version == "latest":
31
+ return self.name
32
+ return f"{self.name}=={self.version}"
33
+
34
+ def _ensure_dependency_is_dev_if_group_is_set(self) -> None:
35
+ if self.group and not self.is_dev:
36
+ raise NotDevDependencyIncludedInGroup(self.name, self.group)
@@ -0,0 +1,8 @@
1
+ from instant_python.shared.application_error import ApplicationError
2
+ from instant_python.shared.error_types import ErrorTypes
3
+
4
+
5
+ class NotDevDependencyIncludedInGroup(ApplicationError):
6
+ def __init__(self, dependency_name: str, dependency_group: str) -> None:
7
+ message = f"Dependency '{dependency_name}' has been included in group '{dependency_group}' but it is not a development dependency. Please ensure that only development dependencies are included in groups."
8
+ super().__init__(message=message, error_type=ErrorTypes.CONFIGURATION.value)
@@ -0,0 +1,60 @@
1
+ from datetime import datetime
2
+ from dataclasses import dataclass, asdict, field
3
+ from typing import ClassVar
4
+
5
+ from instant_python.configuration.general.invalid_dependency_manager_value import (
6
+ InvalidDependencyManagerValue,
7
+ )
8
+ from instant_python.configuration.general.invalid_license_value import (
9
+ InvalidLicenseValue,
10
+ )
11
+ from instant_python.configuration.general.invalid_python_version_value import (
12
+ InvalidPythonVersionValue,
13
+ )
14
+ from instant_python.shared.supported_licenses import SupportedLicenses
15
+ from instant_python.shared.supported_managers import SupportedManagers
16
+ from instant_python.shared.supported_python_versions import SupportedPythonVersions
17
+
18
+
19
+ @dataclass
20
+ class GeneralConfiguration:
21
+ slug: str
22
+ source_name: str
23
+ description: str
24
+ version: str
25
+ author: str
26
+ license: str
27
+ python_version: str
28
+ dependency_manager: str
29
+ year: int = field(default=datetime.now().year)
30
+
31
+ _SUPPORTED_DEPENDENCY_MANAGERS: ClassVar[list[str]] = SupportedManagers.get_supported_managers()
32
+ _SUPPORTED_PYTHON_VERSIONS: ClassVar[list[str]] = SupportedPythonVersions.get_supported_versions()
33
+ _SUPPORTED_LICENSES: ClassVar[list[str]] = SupportedLicenses.get_supported_licenses()
34
+
35
+ def __post_init__(self) -> None:
36
+ self.version = str(self.version)
37
+ self.python_version = str(self.python_version)
38
+ self._remove_white_spaces_from_slug_if_present()
39
+ self._ensure_license_is_supported()
40
+ self._ensure_python_version_is_supported()
41
+ self._ensure_dependency_manager_is_supported()
42
+
43
+ def _remove_white_spaces_from_slug_if_present(self) -> None:
44
+ if " " in self.slug:
45
+ self.slug = self.slug.replace(" ", "")
46
+
47
+ def _ensure_license_is_supported(self) -> None:
48
+ if self.license not in self._SUPPORTED_LICENSES:
49
+ raise InvalidLicenseValue(self.license, self._SUPPORTED_LICENSES)
50
+
51
+ def _ensure_python_version_is_supported(self) -> None:
52
+ if self.python_version not in self._SUPPORTED_PYTHON_VERSIONS:
53
+ raise InvalidPythonVersionValue(self.python_version, self._SUPPORTED_PYTHON_VERSIONS)
54
+
55
+ def _ensure_dependency_manager_is_supported(self) -> None:
56
+ if self.dependency_manager not in self._SUPPORTED_DEPENDENCY_MANAGERS:
57
+ raise InvalidDependencyManagerValue(self.dependency_manager, self._SUPPORTED_DEPENDENCY_MANAGERS)
58
+
59
+ def to_primitives(self) -> dict[str, str]:
60
+ return asdict(self)
@@ -0,0 +1,8 @@
1
+ from instant_python.shared.application_error import ApplicationError
2
+ from instant_python.shared.error_types import ErrorTypes
3
+
4
+
5
+ class InvalidDependencyManagerValue(ApplicationError):
6
+ def __init__(self, value: str, supported_values: list[str]) -> None:
7
+ message = f"Invalid dependency manager: {value}. Allowed values are {', '.join(supported_values)}."
8
+ super().__init__(message=message, error_type=ErrorTypes.CONFIGURATION.value)
@@ -0,0 +1,8 @@
1
+ from instant_python.shared.application_error import ApplicationError
2
+ from instant_python.shared.error_types import ErrorTypes
3
+
4
+
5
+ class InvalidLicenseValue(ApplicationError):
6
+ def __init__(self, value: str, supported_values: list[str]) -> None:
7
+ message = f"Invalid license: {value}. Allowed values are {', '.join(supported_values)}."
8
+ super().__init__(message=message, error_type=ErrorTypes.CONFIGURATION.value)
@@ -0,0 +1,8 @@
1
+ from instant_python.shared.application_error import ApplicationError
2
+ from instant_python.shared.error_types import ErrorTypes
3
+
4
+
5
+ class InvalidPythonVersionValue(ApplicationError):
6
+ def __init__(self, value: str, supported_values: list[str]) -> None:
7
+ message = f"Invalid Python version: {value}. Allowed versions are {', '.join(supported_values)}."
8
+ super().__init__(message=message, error_type=ErrorTypes.CONFIGURATION.value)
@@ -0,0 +1,23 @@
1
+ from dataclasses import dataclass, field, asdict
2
+ from typing import Optional
3
+
4
+ from instant_python.configuration.git.git_user_or_email_not_present import (
5
+ GitUserOrEmailNotPresent,
6
+ )
7
+
8
+
9
+ @dataclass
10
+ class GitConfiguration:
11
+ initialize: bool
12
+ username: Optional[str] = field(default=None)
13
+ email: Optional[str] = field(default=None)
14
+
15
+ def __post_init__(self) -> None:
16
+ self._ensure_username_and_email_are_set_if_initializing()
17
+
18
+ def _ensure_username_and_email_are_set_if_initializing(self) -> None:
19
+ if self.initialize and (self.username is None or self.email is None):
20
+ raise GitUserOrEmailNotPresent()
21
+
22
+ def to_primitives(self) -> dict[str, str | bool]:
23
+ return asdict(self)
@@ -0,0 +1,8 @@
1
+ from instant_python.shared.application_error import ApplicationError
2
+ from instant_python.shared.error_types import ErrorTypes
3
+
4
+
5
+ class GitUserOrEmailNotPresent(ApplicationError):
6
+ def __init__(self) -> None:
7
+ message = "When initializing a git repository, both username and email must be provided."
8
+ super().__init__(message=message, error_type=ErrorTypes.CONFIGURATION.value)
@@ -0,0 +1,8 @@
1
+ from instant_python.shared.application_error import ApplicationError
2
+ from instant_python.shared.error_types import ErrorTypes
3
+
4
+
5
+ class ConfigKeyNotPresent(ApplicationError):
6
+ def __init__(self, missing_keys: list[str], required_keys: list[str]) -> None:
7
+ message = f"The following required keys are missing from the configuration file: {', '.join(missing_keys)}. Required keys are: {', '.join(required_keys)}."
8
+ super().__init__(message=message, error_type=ErrorTypes.CONFIGURATION.value)
@@ -0,0 +1,8 @@
1
+ from instant_python.shared.application_error import ApplicationError
2
+ from instant_python.shared.error_types import ErrorTypes
3
+
4
+
5
+ class ConfigurationFileNotFound(ApplicationError):
6
+ def __init__(self, path: str) -> None:
7
+ message = f"Configuration file not found at '{path}'."
8
+ super().__init__(message=message, error_type=ErrorTypes.CONFIGURATION.value)
@@ -0,0 +1,8 @@
1
+ from instant_python.shared.application_error import ApplicationError
2
+ from instant_python.shared.error_types import ErrorTypes
3
+
4
+
5
+ class EmptyConfigurationNotAllowed(ApplicationError):
6
+ def __init__(self) -> None:
7
+ message = "Configuration file cannot be empty."
8
+ super().__init__(message=message, error_type=ErrorTypes.CONFIGURATION.value)
@@ -0,0 +1,10 @@
1
+ from instant_python.shared.application_error import ApplicationError
2
+ from instant_python.shared.error_types import ErrorTypes
3
+
4
+
5
+ class MissingMandatoryFields(ApplicationError):
6
+ def __init__(self, missing_field: str, config_section: str) -> None:
7
+ message = (
8
+ f"Mandatory field '{missing_field}' is missing in the '{config_section}' section of the configuration file."
9
+ )
10
+ super().__init__(message=message, error_type=ErrorTypes.CONFIGURATION.value)
@@ -0,0 +1,148 @@
1
+ from typing import Union
2
+
3
+ import yaml
4
+
5
+ from instant_python.configuration.parser.config_key_not_present import ConfigKeyNotPresent
6
+ from instant_python.configuration.configuration_schema import ConfigurationSchema
7
+ from instant_python.configuration.dependency.dependency_configuration import (
8
+ DependencyConfiguration,
9
+ )
10
+ from instant_python.configuration.general.general_configuration import (
11
+ GeneralConfiguration,
12
+ )
13
+ from instant_python.configuration.git.git_configuration import GitConfiguration
14
+ from instant_python.configuration.parser.configuration_file_not_found import (
15
+ ConfigurationFileNotFound,
16
+ )
17
+ from instant_python.configuration.parser.empty_configuration_not_allowed import (
18
+ EmptyConfigurationNotAllowed,
19
+ )
20
+ from instant_python.configuration.parser.missing_mandatory_fields import (
21
+ MissingMandatoryFields,
22
+ )
23
+ from instant_python.configuration.template.template_configuration import TemplateConfiguration
24
+
25
+
26
+ class Parser:
27
+ REQUIRED_CONFIG_KEYS = ["general", "dependencies", "template", "git"]
28
+
29
+ @classmethod
30
+ def parse_from_file(cls, config_file_path: str) -> ConfigurationSchema:
31
+ """Parses the configuration file and validates its content.
32
+
33
+ Args:
34
+ config_file_path: The path to the configuration file to be parsed.
35
+ Returns:
36
+ ConfigurationSchema: An instance of ConfigurationSchema containing the parsed configuration.
37
+ Raises:
38
+ ConfigurationFileNotFound: If the configuration file does not exist in that path.
39
+ EmptyConfigurationNotAllowed: If the configuration file is empty.
40
+ ConfigKeyNotPresent: If any of the required keys are missing from the configuration.
41
+ MissingMandatoryFields: If any mandatory fields are missing in the configuration sections.
42
+ """
43
+ content = cls._get_config_file_content(config_file_path)
44
+ general_configuration, dependencies_configuration, template_configuration, git_configuration = (
45
+ cls._parse_configuration(content=content)
46
+ )
47
+ return ConfigurationSchema.from_file(
48
+ config_file_path=config_file_path,
49
+ general=general_configuration,
50
+ dependencies=dependencies_configuration,
51
+ template=template_configuration,
52
+ git=git_configuration,
53
+ )
54
+
55
+ @classmethod
56
+ def parse_from_answers(cls, content: dict[str, dict]) -> ConfigurationSchema:
57
+ general_configuration, dependencies_configuration, template_configuration, git_configuration = (
58
+ cls._parse_configuration(content=content)
59
+ )
60
+ return ConfigurationSchema(
61
+ general=general_configuration,
62
+ dependencies=dependencies_configuration,
63
+ template=template_configuration,
64
+ git=git_configuration,
65
+ )
66
+
67
+ @classmethod
68
+ def _parse_configuration(cls, content: dict[str, dict]) -> tuple:
69
+ general_configuration = cls._parse_general_configuration(content["general"])
70
+ dependencies_configuration = cls._parse_dependencies_configuration(content["dependencies"])
71
+ template_configuration = cls._parse_template_configuration(content["template"])
72
+ git_configuration = cls._parse_git_configuration(content["git"])
73
+ return general_configuration, dependencies_configuration, template_configuration, git_configuration
74
+
75
+ @classmethod
76
+ def _get_config_file_content(cls, config_file_path: str) -> dict[str, dict]:
77
+ content = cls._read_config_file(config_file_path)
78
+ cls._ensure_config_file_is_not_empty(content)
79
+ cls._ensure_all_required_keys_are_present(content)
80
+ return content
81
+
82
+ @staticmethod
83
+ def _ensure_config_file_is_not_empty(content: dict[str, dict]) -> None:
84
+ if not content:
85
+ raise EmptyConfigurationNotAllowed()
86
+
87
+ @staticmethod
88
+ def _read_config_file(config_file_path: str) -> dict[str, dict]:
89
+ try:
90
+ with open(config_file_path, "r") as file:
91
+ return yaml.safe_load(file)
92
+ except FileNotFoundError:
93
+ raise ConfigurationFileNotFound(config_file_path)
94
+
95
+ @staticmethod
96
+ def _ensure_all_required_keys_are_present(content: dict[str, dict]) -> None:
97
+ missing_keys = [key for key in Parser.REQUIRED_CONFIG_KEYS if key not in content]
98
+ if missing_keys:
99
+ raise ConfigKeyNotPresent(missing_keys, Parser.REQUIRED_CONFIG_KEYS)
100
+
101
+ @staticmethod
102
+ def _parse_general_configuration(fields: dict[str, str]) -> GeneralConfiguration:
103
+ try:
104
+ return GeneralConfiguration(**fields)
105
+ except TypeError as error:
106
+ _ensure_error_is_for_missing_fields(error)
107
+ raise MissingMandatoryFields(error.args[0], "general") from error
108
+
109
+ @staticmethod
110
+ def _parse_dependencies_configuration(
111
+ fields: list[dict[str, Union[str, bool]]],
112
+ ) -> list[DependencyConfiguration]:
113
+ dependencies = []
114
+
115
+ if not fields:
116
+ return dependencies
117
+
118
+ for dependency_fields in fields:
119
+ try:
120
+ dependency = DependencyConfiguration(**dependency_fields)
121
+ except TypeError as error:
122
+ _ensure_error_is_for_missing_fields(error)
123
+ raise MissingMandatoryFields(error.args[0], "dependencies") from error
124
+
125
+ dependencies.append(dependency)
126
+
127
+ return dependencies
128
+
129
+ @staticmethod
130
+ def _parse_template_configuration(fields: dict[str, Union[str, bool, list[str]]]) -> TemplateConfiguration:
131
+ try:
132
+ return TemplateConfiguration(**fields)
133
+ except TypeError as error:
134
+ _ensure_error_is_for_missing_fields(error)
135
+ raise MissingMandatoryFields(error.args[0], "template") from error
136
+
137
+ @staticmethod
138
+ def _parse_git_configuration(fields: dict[str, Union[str, bool]]) -> GitConfiguration:
139
+ try:
140
+ return GitConfiguration(**fields)
141
+ except TypeError as error:
142
+ _ensure_error_is_for_missing_fields(error)
143
+ raise MissingMandatoryFields(error.args[0], "git") from error
144
+
145
+
146
+ def _ensure_error_is_for_missing_fields(error: TypeError) -> None:
147
+ if ".__init__() missing" not in str(error):
148
+ raise error
@@ -0,0 +1,12 @@
1
+ from instant_python.configuration.question.question import Question
2
+ from instant_python.configuration.question.questionary import Questionary
3
+
4
+
5
+ class BooleanQuestion(Question[bool]):
6
+ def __init__(self, key: str, message: str, default: bool, questionary: Questionary) -> None:
7
+ super().__init__(key, message, questionary)
8
+ self._default = default
9
+
10
+ def ask(self) -> dict[str, bool]:
11
+ answer = self._questionary.boolean_question(self._message, default=self._default)
12
+ return {self._key: answer}
@@ -0,0 +1,19 @@
1
+ from typing import Optional
2
+
3
+ from instant_python.configuration.question.question import Question
4
+ from instant_python.configuration.question.questionary import Questionary
5
+
6
+
7
+ class ChoiceQuestion(Question[str]):
8
+ def __init__(self, key: str, message: str, questionary: Questionary, options: Optional[list[str]] = None) -> None:
9
+ super().__init__(key, message, questionary)
10
+ self._default = options[0] if options else ""
11
+ self._options = options if options else []
12
+
13
+ def ask(self) -> dict[str, str]:
14
+ answer = self._questionary.single_choice_question(
15
+ self._message,
16
+ options=self._options,
17
+ default=self._default,
18
+ )
19
+ return {self._key: answer}
@@ -1,11 +1,14 @@
1
1
  from typing import Union
2
2
 
3
- from instant_python.question_prompter.question.question import Question
3
+ from instant_python.configuration.question.question import Question
4
4
 
5
5
 
6
6
  class ConditionalQuestion:
7
7
  def __init__(
8
- self, base_question: Question, subquestions: Union[list[Question], "ConditionalQuestion"], condition: str | bool
8
+ self,
9
+ base_question: Question,
10
+ subquestions: Union[list[Question], "ConditionalQuestion"],
11
+ condition: Union[str, bool],
9
12
  ) -> None:
10
13
  self._base_question = base_question
11
14
  self._subquestions = subquestions
@@ -18,7 +21,7 @@ class ConditionalQuestion:
18
21
  return base_answer
19
22
 
20
23
  answers = base_answer
21
-
24
+
22
25
  if isinstance(self._subquestions, ConditionalQuestion):
23
26
  answers.update(self._subquestions.ask())
24
27
  else:
@@ -0,0 +1,14 @@
1
+ from typing import Optional
2
+
3
+ from instant_python.configuration.question.question import Question
4
+ from instant_python.configuration.question.questionary import Questionary
5
+
6
+
7
+ class FreeTextQuestion(Question[str]):
8
+ def __init__(self, key: str, message: str, questionary: Questionary, default: Optional[str] = None) -> None:
9
+ super().__init__(key, message, questionary)
10
+ self._default = default if default else ""
11
+
12
+ def ask(self) -> dict[str, str]:
13
+ answer = self._questionary.free_text_question(self._message, default=self._default)
14
+ return {self._key: answer}
@@ -0,0 +1,12 @@
1
+ from instant_python.configuration.question.question import Question
2
+ from instant_python.configuration.question.questionary import Questionary
3
+
4
+
5
+ class MultipleChoiceQuestion(Question[list[str]]):
6
+ def __init__(self, key: str, message: str, options: list[str], questionary: Questionary) -> None:
7
+ super().__init__(key, message, questionary)
8
+ self._options = options
9
+
10
+ def ask(self) -> dict[str, list[str]]:
11
+ answer = self._questionary.multiselect_question(self._message, options=self._options)
12
+ return {self._key: answer}
@@ -2,17 +2,21 @@ from abc import ABC, abstractmethod
2
2
 
3
3
  from typing import TypeVar, Generic
4
4
 
5
+ from instant_python.configuration.question.questionary import Questionary
6
+
5
7
  T = TypeVar("T")
6
8
 
9
+
7
10
  class Question(Generic[T], ABC):
8
- def __init__(self, key: str, message: str) -> None:
11
+ def __init__(self, key: str, message: str, questionary: Questionary) -> None:
9
12
  self._key = key
10
13
  self._message = message
14
+ self._questionary = questionary
11
15
 
12
16
  @abstractmethod
13
17
  def ask(self) -> dict[str, T]:
14
18
  raise NotImplementedError
15
-
19
+
16
20
  @property
17
21
  def key(self) -> str:
18
22
  return self._key