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.
- instant_python/__init__.py +1 -0
- instant_python/cli/__init__.py +0 -0
- instant_python/cli/cli.py +58 -0
- instant_python/cli/instant_python_typer.py +35 -0
- instant_python/config/__init__.py +0 -0
- instant_python/config/application/__init__.py +0 -0
- instant_python/config/application/config_generator.py +23 -0
- instant_python/config/delivery/__init__.py +0 -0
- instant_python/config/delivery/cli.py +19 -0
- instant_python/config/domain/__init__.py +0 -0
- instant_python/config/domain/question_wizard.py +7 -0
- instant_python/config/infra/__init__.py +0 -0
- instant_python/config/infra/question_wizard/__init__.py +0 -0
- instant_python/config/infra/question_wizard/questionary_console_wizard.py +25 -0
- instant_python/config/infra/question_wizard/step/__init__.py +0 -0
- instant_python/config/infra/question_wizard/step/dependencies_step.py +64 -0
- instant_python/config/infra/question_wizard/step/general_step.py +81 -0
- instant_python/config/infra/question_wizard/step/git_step.py +41 -0
- instant_python/config/infra/question_wizard/step/questionary.py +17 -0
- instant_python/config/infra/question_wizard/step/steps.py +21 -0
- instant_python/config/infra/question_wizard/step/template_step.py +63 -0
- instant_python/initialize/__init__.py +0 -0
- instant_python/initialize/application/__init__.py +0 -0
- instant_python/initialize/application/project_initializer.py +47 -0
- instant_python/initialize/delivery/__init__.py +0 -0
- instant_python/initialize/delivery/cli.py +48 -0
- instant_python/initialize/domain/__init__.py +0 -0
- instant_python/initialize/domain/env_manager.py +9 -0
- instant_python/initialize/domain/node.py +73 -0
- instant_python/initialize/domain/project_formatter.py +7 -0
- instant_python/initialize/domain/project_renderer.py +10 -0
- instant_python/initialize/domain/project_structure.py +77 -0
- instant_python/initialize/domain/project_writer.py +20 -0
- instant_python/initialize/domain/version_control_configurer.py +9 -0
- instant_python/initialize/infra/__init__.py +0 -0
- instant_python/initialize/infra/env_manager/__init__.py +0 -0
- instant_python/initialize/infra/env_manager/env_manager_factory.py +27 -0
- instant_python/initialize/infra/env_manager/pdm_env_manager.py +66 -0
- instant_python/initialize/infra/env_manager/system_console.py +65 -0
- instant_python/initialize/infra/env_manager/uv_env_manager.py +74 -0
- instant_python/initialize/infra/formatter/__init__.py +0 -0
- instant_python/initialize/infra/formatter/ruff_project_formatter.py +10 -0
- instant_python/initialize/infra/renderer/__init__.py +0 -0
- instant_python/initialize/infra/renderer/jinja_environment.py +71 -0
- instant_python/initialize/infra/renderer/jinja_project_renderer.py +57 -0
- instant_python/initialize/infra/version_control/__init__.py +0 -0
- instant_python/initialize/infra/version_control/git_configurer.py +29 -0
- instant_python/initialize/infra/writer/__init__.py +0 -0
- instant_python/initialize/infra/writer/file_system_project_writer.py +23 -0
- instant_python/shared/__init__.py +0 -0
- instant_python/shared/application_error.py +8 -0
- instant_python/shared/domain/__init__.py +0 -0
- instant_python/shared/domain/config_repository.py +18 -0
- instant_python/shared/domain/config_schema.py +113 -0
- instant_python/shared/domain/dependency_config.py +41 -0
- instant_python/shared/domain/general_config.py +71 -0
- instant_python/shared/domain/git_config.py +32 -0
- instant_python/shared/domain/template_config.py +76 -0
- instant_python/shared/infra/__init__.py +0 -0
- instant_python/shared/infra/persistence/__init__.py +0 -0
- instant_python/shared/infra/persistence/yaml_config_repository.py +39 -0
- instant_python/shared/supported_built_in_features.py +20 -0
- instant_python/shared/supported_licenses.py +11 -0
- instant_python/shared/supported_managers.py +10 -0
- instant_python/shared/supported_python_versions.py +12 -0
- instant_python/shared/supported_templates.py +12 -0
- instant_python/templates/boilerplate/.gitignore +164 -0
- instant_python/templates/boilerplate/.pre-commit-config.yml +73 -0
- instant_python/templates/boilerplate/.python-version +1 -0
- instant_python/templates/boilerplate/CITATION.cff +13 -0
- instant_python/templates/boilerplate/LICENSE +896 -0
- instant_python/templates/boilerplate/README.md +8 -0
- instant_python/templates/boilerplate/SECURITY.md +43 -0
- instant_python/templates/boilerplate/event_bus/__init__.py +0 -0
- instant_python/templates/boilerplate/event_bus/domain_event.py +15 -0
- instant_python/templates/boilerplate/event_bus/domain_event_json_deserializer.py +25 -0
- instant_python/templates/boilerplate/event_bus/domain_event_json_serializer.py +16 -0
- instant_python/templates/boilerplate/event_bus/domain_event_subscriber.py +33 -0
- instant_python/templates/boilerplate/event_bus/event_aggregate.py +19 -0
- instant_python/templates/boilerplate/event_bus/event_bus.py +9 -0
- instant_python/templates/boilerplate/event_bus/exchange_type.py +14 -0
- instant_python/templates/boilerplate/event_bus/mock_event_bus.py +16 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_configurer.py +45 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_connection.py +71 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_consumer.py +56 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_event_bus.py +26 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_queue_formatter.py +21 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_settings.py +8 -0
- instant_python/templates/boilerplate/exceptions/__init__.py +0 -0
- instant_python/templates/boilerplate/exceptions/base_error.py +13 -0
- instant_python/templates/boilerplate/exceptions/domain_error.py +6 -0
- instant_python/templates/boilerplate/exceptions/domain_event_type_not_found_error.py +6 -0
- instant_python/templates/boilerplate/exceptions/rabbit_mq_connection_not_established_error.py +7 -0
- instant_python/templates/boilerplate/exceptions/required_value_error.py +6 -0
- instant_python/templates/boilerplate/fastapi/__init__.py +0 -0
- instant_python/templates/boilerplate/fastapi/application.py +74 -0
- instant_python/templates/boilerplate/fastapi/error_handlers.py +88 -0
- instant_python/templates/boilerplate/fastapi/error_response.py +31 -0
- instant_python/templates/boilerplate/fastapi/fastapi_log_middleware.py +32 -0
- instant_python/templates/boilerplate/fastapi/lifespan.py +13 -0
- instant_python/templates/boilerplate/fastapi/success_response.py +13 -0
- instant_python/templates/boilerplate/github/action.yml +35 -0
- instant_python/templates/boilerplate/github/bug_report.yml +60 -0
- instant_python/templates/boilerplate/github/ci.yml +199 -0
- instant_python/templates/boilerplate/github/feature_request.yml +21 -0
- instant_python/templates/boilerplate/github/release.yml +94 -0
- instant_python/templates/boilerplate/logger/__init__.py +0 -0
- instant_python/templates/boilerplate/logger/file_logger.py +55 -0
- instant_python/templates/boilerplate/logger/file_rotating_handler.py +36 -0
- instant_python/templates/boilerplate/logger/json_formatter.py +16 -0
- instant_python/templates/boilerplate/mypy.ini +41 -0
- instant_python/templates/boilerplate/persistence/__init__.py +0 -0
- instant_python/templates/boilerplate/persistence/alembic_migrator.py +19 -0
- instant_python/templates/boilerplate/persistence/async/README.md +1 -0
- instant_python/templates/boilerplate/persistence/async/__init__.py +0 -0
- instant_python/templates/boilerplate/persistence/async/alembic.ini +124 -0
- instant_python/templates/boilerplate/persistence/async/async_engine_fixture.py +20 -0
- instant_python/templates/boilerplate/persistence/async/async_session.py +20 -0
- instant_python/templates/boilerplate/persistence/async/env.py +94 -0
- instant_python/templates/boilerplate/persistence/async/models_metadata.py +10 -0
- instant_python/templates/boilerplate/persistence/async/postgres_settings.py +15 -0
- instant_python/templates/boilerplate/persistence/async/script.py.mako +26 -0
- instant_python/templates/boilerplate/persistence/async/sqlalchemy_repository.py +28 -0
- instant_python/templates/boilerplate/persistence/base.py +4 -0
- instant_python/templates/boilerplate/persistence/synchronous/__init__.py +0 -0
- instant_python/templates/boilerplate/persistence/synchronous/session_maker.py +21 -0
- instant_python/templates/boilerplate/persistence/synchronous/sqlalchemy_repository.py +40 -0
- instant_python/templates/boilerplate/pyproject.toml +134 -0
- instant_python/templates/boilerplate/pytest.ini +10 -0
- instant_python/templates/boilerplate/scripts/add_dependency.py +45 -0
- instant_python/templates/boilerplate/scripts/create_aggregate.py +33 -0
- instant_python/templates/boilerplate/scripts/insert_template.py +90 -0
- instant_python/templates/boilerplate/scripts/integration.sh +39 -0
- instant_python/templates/boilerplate/scripts/local_setup.py +12 -0
- instant_python/templates/boilerplate/scripts/makefile +184 -0
- instant_python/templates/boilerplate/scripts/post-merge.py +40 -0
- instant_python/templates/boilerplate/scripts/pre-commit.py +15 -0
- instant_python/templates/boilerplate/scripts/pre-push.py +6 -0
- instant_python/templates/boilerplate/scripts/remove_dependency.py +40 -0
- instant_python/templates/boilerplate/scripts/unit.sh +40 -0
- instant_python/templates/project_structure/clean_architecture/layers/application.yml +3 -0
- instant_python/templates/project_structure/clean_architecture/layers/delivery.yml +8 -0
- instant_python/templates/project_structure/clean_architecture/layers/domain.yml +10 -0
- instant_python/templates/project_structure/clean_architecture/layers/infra.yml +12 -0
- instant_python/templates/project_structure/clean_architecture/layers/test_application.yml +3 -0
- instant_python/templates/project_structure/clean_architecture/layers/test_delivery.yml +3 -0
- instant_python/templates/project_structure/clean_architecture/layers/test_domain.yml +3 -0
- instant_python/templates/project_structure/clean_architecture/layers/test_infra.yml +9 -0
- instant_python/templates/project_structure/clean_architecture/main_structure.yml +38 -0
- instant_python/templates/project_structure/clean_architecture/source.yml +9 -0
- instant_python/templates/project_structure/clean_architecture/test.yml +9 -0
- instant_python/templates/project_structure/config_files/gitignore.yml +3 -0
- instant_python/templates/project_structure/config_files/mypy.yml +4 -0
- instant_python/templates/project_structure/config_files/pyproject.yml +4 -0
- instant_python/templates/project_structure/config_files/pytest.yml +4 -0
- instant_python/templates/project_structure/config_files/python_version.yml +3 -0
- instant_python/templates/project_structure/documentation/citation.yml +4 -0
- instant_python/templates/project_structure/documentation/license.yml +3 -0
- instant_python/templates/project_structure/documentation/readme.yml +4 -0
- instant_python/templates/project_structure/documentation/security.yml +4 -0
- instant_python/templates/project_structure/domain_driven_design/layers/bounded_context.yml +17 -0
- instant_python/templates/project_structure/domain_driven_design/layers/delivery.yml +8 -0
- instant_python/templates/project_structure/domain_driven_design/layers/shared.yml +18 -0
- instant_python/templates/project_structure/domain_driven_design/layers/shared_domain.yml +10 -0
- instant_python/templates/project_structure/domain_driven_design/layers/shared_infra.yml +12 -0
- instant_python/templates/project_structure/domain_driven_design/layers/test_shared.yml +8 -0
- instant_python/templates/project_structure/domain_driven_design/layers/test_shared_delivery.yml +3 -0
- instant_python/templates/project_structure/domain_driven_design/layers/test_shared_domain.yml +3 -0
- instant_python/templates/project_structure/domain_driven_design/layers/test_shared_infra.yml +9 -0
- instant_python/templates/project_structure/domain_driven_design/main_structure.yml +38 -0
- instant_python/templates/project_structure/domain_driven_design/source.yml +10 -0
- instant_python/templates/project_structure/domain_driven_design/test.yml +9 -0
- instant_python/templates/project_structure/errors.yml +12 -0
- instant_python/templates/project_structure/events/event_bus_domain.yml +48 -0
- instant_python/templates/project_structure/events/event_bus_infra.yml +40 -0
- instant_python/templates/project_structure/events/mock_event_bus.yml +4 -0
- instant_python/templates/project_structure/fastapi/fastapi_app.yml +32 -0
- instant_python/templates/project_structure/fastapi/fastapi_domain.yml +12 -0
- instant_python/templates/project_structure/fastapi/fastapi_infra.yml +12 -0
- instant_python/templates/project_structure/github/github_action.yml +24 -0
- instant_python/templates/project_structure/github/github_issues_template.yml +14 -0
- instant_python/templates/project_structure/github/makefile.yml +3 -0
- instant_python/templates/project_structure/github/precommit_hook.yml +4 -0
- instant_python/templates/project_structure/logger.yml +16 -0
- instant_python/templates/project_structure/macros.j2 +73 -0
- instant_python/templates/project_structure/persistence/alembic_migrator.yml +6 -0
- instant_python/templates/project_structure/persistence/async_alembic.yml +27 -0
- instant_python/templates/project_structure/persistence/async_engine_conftest.yml +4 -0
- instant_python/templates/project_structure/persistence/async_sqlalchemy.yml +14 -0
- instant_python/templates/project_structure/persistence/persistence.yml +8 -0
- instant_python/templates/project_structure/persistence/synchronous_sqlalchemy.yml +20 -0
- instant_python/templates/project_structure/standard_project/layers/source_features.yml +13 -0
- instant_python/templates/project_structure/standard_project/layers/test_event_bus.yml +6 -0
- instant_python/templates/project_structure/standard_project/layers/test_features.yml +6 -0
- instant_python/templates/project_structure/standard_project/main_structure.yml +38 -0
- instant_python/templates/project_structure/standard_project/source.yml +5 -0
- instant_python/templates/project_structure/standard_project/test.yml +5 -0
- instant_python-0.20.0.dist-info/METADATA +318 -0
- instant_python-0.20.0.dist-info/RECORD +202 -0
- instant_python-0.20.0.dist-info/WHEEL +4 -0
- instant_python-0.20.0.dist-info/entry_points.txt +2 -0
- 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,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
|
|
File without changes
|
|
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)
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|