From 8502bf75faa9484f19a0bc6c1a54aac8deed2021 Mon Sep 17 00:00:00 2001 From: Ali Tavallaie Date: Sat, 25 May 2024 14:28:03 +0330 Subject: [PATCH] refactor all --- djangowiz/core/__init__.py | 9 + djangowiz/core/base_generator.py | 31 +++ djangowiz/core/io_handler.py | 30 +++ djangowiz/core/model_extractor.py | 19 ++ djangowiz/core/project_generator.py | 220 ++++++++++++++++++ djangowiz/core/project_io_handler.py | 87 +++++++ djangowiz/repo/generators/__init__.py | 0 .../repo/generators/serializer_generator.py | 10 + .../repo/generators/viewset_generator.py | 10 + djangowiz/{ => repo}/templates/Dockerfile.j2 | 0 .../templates/docker-compose.dev.yml.j2 | 0 .../templates/docker-compose.prod.yml.j2 | 0 djangowiz/{ => repo}/templates/env.dev.j2 | 0 djangowiz/{ => repo}/templates/env.prod.j2 | 0 .../templates/multi/serializers.py.j2 | 0 .../{ => repo}/templates/multi/viewsets.py.j2 | 0 djangowiz/{ => repo}/templates/routes.py.j2 | 0 .../templates/single/serializers.py.j2 | 0 .../templates/single/viewsets.py.j2 | 0 djangowiz/{ => repo}/templates/urls.py.j2 | 0 20 files changed, 416 insertions(+) create mode 100644 djangowiz/core/__init__.py create mode 100644 djangowiz/core/base_generator.py create mode 100644 djangowiz/core/io_handler.py create mode 100644 djangowiz/core/model_extractor.py create mode 100644 djangowiz/core/project_generator.py create mode 100644 djangowiz/core/project_io_handler.py create mode 100644 djangowiz/repo/generators/__init__.py create mode 100644 djangowiz/repo/generators/serializer_generator.py create mode 100644 djangowiz/repo/generators/viewset_generator.py rename djangowiz/{ => repo}/templates/Dockerfile.j2 (100%) rename djangowiz/{ => repo}/templates/docker-compose.dev.yml.j2 (100%) rename djangowiz/{ => repo}/templates/docker-compose.prod.yml.j2 (100%) rename djangowiz/{ => repo}/templates/env.dev.j2 (100%) rename djangowiz/{ => repo}/templates/env.prod.j2 (100%) rename djangowiz/{ => repo}/templates/multi/serializers.py.j2 (100%) rename djangowiz/{ => repo}/templates/multi/viewsets.py.j2 (100%) rename djangowiz/{ => repo}/templates/routes.py.j2 (100%) rename djangowiz/{ => repo}/templates/single/serializers.py.j2 (100%) rename djangowiz/{ => repo}/templates/single/viewsets.py.j2 (100%) rename djangowiz/{ => repo}/templates/urls.py.j2 (100%) diff --git a/djangowiz/core/__init__.py b/djangowiz/core/__init__.py new file mode 100644 index 0000000..16dbbbd --- /dev/null +++ b/djangowiz/core/__init__.py @@ -0,0 +1,9 @@ +from djangowiz.core.model_extractor import ModelExtractor +from djangowiz.core.base_generator import BaseGenerator +from djangowiz.core.io_handler import IOHandler +from djangowiz.core.project_generator import ProjectGenerator +from djangowiz.core.project_io_handler import ProjectIOHandler + +from .custom_generator import CustomGenerator +from .serializer_generator import SerializerGenerator +from .viewset_generator import ViewsetGenerator diff --git a/djangowiz/core/base_generator.py b/djangowiz/core/base_generator.py new file mode 100644 index 0000000..ef32579 --- /dev/null +++ b/djangowiz/core/base_generator.py @@ -0,0 +1,31 @@ +import os +from jinja2 import Environment, FileSystemLoader +from typing import List + + +class BaseGenerator: + def __init__( + self, + app_name: str, + project_name: str, + model_names: List[str], + template_dir: str, + **kwargs, + ): + self.app_name = app_name + self.project_name = project_name + self.model_names = model_names + self.env = Environment(loader=FileSystemLoader(template_dir)) + self.options = kwargs + + def generate(self, overwrite: bool = False, template: str = None, **kwargs): + raise NotImplementedError("Subclasses must implement this method") + + def write_file(self, file_path: str, content: str, overwrite: bool = False): + if not overwrite and os.path.exists(file_path): + print(f"Skipping existing file: {file_path}") + return + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, "w") as file: + file.write(content) + print(f"Generated file: {file_path}") diff --git a/djangowiz/core/io_handler.py b/djangowiz/core/io_handler.py new file mode 100644 index 0000000..c9badf7 --- /dev/null +++ b/djangowiz/core/io_handler.py @@ -0,0 +1,30 @@ +import os +import shutil +import yaml +from typing import Dict, Any + + +class IOHandler: + @staticmethod + def load_yaml(file_path: str) -> Dict[str, Any]: + with open(file_path, "r") as file: + return yaml.safe_load(file) + + @staticmethod + def save_yaml(data: Dict[str, Any], file_path: str): + with open(file_path, "w") as file: + yaml.safe_dump(data, file) + + @staticmethod + def copy_file(src: str, dst: str): + os.makedirs(os.path.dirname(dst), exist_ok=True) + shutil.copy(src, dst) + + @staticmethod + def copy_tree(src: str, dst: str, dirs_exist_ok: bool = True): + shutil.copytree(src, dst, dirs_exist_ok=dirs_exist_ok) + + @staticmethod + def remove_file(file_path: str): + if os.path.exists(file_path): + os.remove(file_path) diff --git a/djangowiz/core/model_extractor.py b/djangowiz/core/model_extractor.py new file mode 100644 index 0000000..bef5745 --- /dev/null +++ b/djangowiz/core/model_extractor.py @@ -0,0 +1,19 @@ +import ast +from typing import List + + +class ModelExtractor: + @staticmethod + def extract_model_names(file_path: str) -> List[str]: + model_names = [] + with open(file_path, "r") as file: + tree = ast.parse(file.read(), filename=file_path) + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + bases = [ + base.id if isinstance(base, ast.Name) else base.attr + for base in node.bases + ] + if "Model" in bases or any(base for base in bases): + model_names.append(node.name) + return model_names diff --git a/djangowiz/core/project_generator.py b/djangowiz/core/project_generator.py new file mode 100644 index 0000000..a2bec4a --- /dev/null +++ b/djangowiz/core/project_generator.py @@ -0,0 +1,220 @@ +import os +import importlib +from typing import List, Dict, Any +from djangowiz.core.io_handler import IOHandler +from djangowiz.core.project_io_handler import ProjectIOHandler + + +class ProjectGenerator: + def __init__( + self, + app_name: str, + project_name: str, + model_names: List[str], + template_dir: str = None, + config_file: str = None, + repo_dir: str = None, + ): + self.app_name = app_name + self.project_name = project_name + self.model_names = model_names + self.default_template_dir = os.path.join( + os.path.dirname(__file__), "..", "repo", "templates" + ) + self.template_dir = template_dir or ( + os.path.join(repo_dir, "templates") + if repo_dir + else self.default_template_dir + ) + self.generator_dir = ( + os.path.join(repo_dir, "generators") + if repo_dir + else os.path.join(os.path.dirname(__file__), "..", "repo", "generators") + ) + self.config_file = config_file or ( + os.path.join(repo_dir, "generators.yaml") + if repo_dir + else os.path.join( + os.path.dirname(__file__), "..", "repo", "generators.yaml" + ) + ) + self.default_config_file = os.path.join( + os.path.dirname(__file__), "..", "repo", "generators.yaml" + ) + self.generators: Dict[str, Dict[str, Any]] = {} + + self.io_handler = ProjectIOHandler( + self.config_file, + self.default_config_file, + self.template_dir, + self.default_template_dir, + self.generator_dir, + ) + + self.load_generators(self.config_file) + + def load_generators(self, config_file: str): + user_config = IOHandler.load_yaml(config_file) + default_config = IOHandler.load_yaml(self.default_config_file) + + for name, generator_config in default_config.get("generators", {}).items(): + self.generators[name] = generator_config + + for name, generator_config in user_config.get("generators", {}).items(): + if name in self.generators: + self.generators[name]["options"].update( + generator_config.get("options", {}) + ) + else: + self.generators[name] = generator_config + + for name, generator_config in self.generators.items(): + for option, config in generator_config.get("options", {}).items(): + self.load_generator(name, option, config) + + def load_generator(self, name: str, option: str, config: Dict[str, Any]): + class_path = config["class"] + module_path, class_name = class_path.rsplit(".", 1) + if self.generator_dir not in module_path: + module_path = os.path.join(self.generator_dir, module_path) + module = importlib.import_module(module_path) + generator_class = getattr(module, class_name) + template_path = config.get("template", "") + + if not os.path.exists(os.path.join(self.template_dir, template_path)): + template_path = os.path.join(self.default_template_dir, template_path) + + self.generators[f"{name}_{option}"] = { + "class": generator_class( + self.app_name, + self.project_name, + self.model_names, + self.template_dir, + **config, + ), + "template": template_path, + } + + def save_generators(self): + combined_config = {"generators": {}} + for name, generator in self.generators.items(): + base_name, option = name.rsplit("_", 1) + if base_name not in combined_config["generators"]: + combined_config["generators"][base_name] = {"options": {}} + combined_config["generators"][base_name]["options"][option] = { + "class": generator["class"].__class__.__module__ + + "." + + generator["class"].__class__.__name__, + "template": generator["template"], + **{ + k: v + for k, v in generator["class"].options.items() + if k not in ["class", "template"] + }, + } + + IOHandler.save_yaml(combined_config, self.config_file) + + def add_generator( + self, name: str, class_path: str, template_path: str, option: str, **kwargs + ): + generator_key = f"{name}_{option}" + if generator_key in self.generators: + print( + f"Generator '{generator_key}' already exists. Use update_generator to update it." + ) + return + + if not os.path.exists(template_path): + template_path = os.path.join(self.default_template_dir, template_path) + + module_path, class_name = class_path.rsplit(".", 1) + module = importlib.import_module(module_path) + generator_class = getattr(module, class_name) + generator_instance = generator_class( + self.app_name, + self.project_name, + self.model_names, + self.template_dir, + **kwargs, + ) + + self.generators[generator_key] = { + "class": generator_instance, + "template": template_path, + } + + self.save_generators() + IOHandler.copy_file( + module_path.replace(".", "/") + ".py", + os.path.join(self.generator_dir, module_path.replace(".", "/") + ".py"), + ) + self.load_generators(self.config_file) # Reload configuration + print(f"Generator '{generator_key}' has been added.") + + def delete_generator(self, name: str, option: str): + generator_key = f"{name}_{option}" + if generator_key in self.generators: + IOHandler.remove_file( + self.generators[generator_key]["class"].__module__.replace(".", "/") + + ".py" + ) + del self.generators[generator_key] + self.save_generators() + self.load_generators(self.config_file) # Reload configuration + print(f"Generator '{generator_key}' has been deleted.") + else: + print(f"Generator '{generator_key}' does not exist.") + + def update_generator( + self, name: str, class_path: str, template_path: str, option: str, **kwargs + ): + generator_key = f"{name}_{option}" + if not os.path.exists(template_path): + template_path = os.path.join(self.default_template_dir, template_path) + + module_path, class_name = class_path.rsplit(".", 1) + module = importlib.import_module(module_path) + generator_class = getattr(module, class_name) + generator_instance = generator_class( + self.app_name, + self.project_name, + self.model_names, + self.template_dir, + **kwargs, + ) + + self.generators[generator_key] = { + "class": generator_instance, + "template": template_path, + } + + self.save_generators() + IOHandler.copy_file( + module_path.replace(".", "/") + ".py", + os.path.join(self.generator_dir, module_path.replace(".", "/") + ".py"), + ) + self.load_generators(self.config_file) # Reload configuration + print(f"Generator '{generator_key}' has been updated.") + + def show_generators(self): + for name, generator in self.generators.items(): + print( + f"Generator: {name} ({generator['class'].__class__.__module__}.{generator['class'].__class__.__name__}), Template: {generator['template']}, Config: {generator['class'].options}" + ) + + def generate( + self, components: List[str], option: str, overwrite: bool = False, **kwargs + ): + for component in components: + generator_key = f"{component}_{option}" + if generator_key in self.generators: + generator = self.generators[generator_key]["class"] + template = self.generators[generator_key]["template"] + generator.generate(overwrite=overwrite, template=template, **kwargs) + + def export_config(self, export_path: str): + self.io_handler.export_config(export_path) + + def import_config(self, import_path: str): + self.io_handler.import_config(import_path) diff --git a/djangowiz/core/project_io_handler.py b/djangowiz/core/project_io_handler.py new file mode 100644 index 0000000..92b7223 --- /dev/null +++ b/djangowiz/core/project_io_handler.py @@ -0,0 +1,87 @@ +import os +from djangowiz.core.io_handler import IOHandler + + +class ProjectIOHandler: + def __init__( + self, + config_file: str, + default_config_file: str, + template_dir: str, + default_template_dir: str, + generator_dir: str, + ): + self.config_file = config_file + self.default_config_file = default_config_file + self.template_dir = template_dir + self.default_template_dir = default_template_dir + self.generator_dir = generator_dir + + def export_config(self, export_path: str): + """Export combined generators.yaml, templates, and generators to a specified directory.""" + os.makedirs(export_path, exist_ok=True) + + # Combine user and default configurations + user_config = IOHandler.load_yaml(self.config_file) + default_config = IOHandler.load_yaml(self.default_config_file) + combined_config = {"generators": {}} + + for name, generator_config in default_config.get("generators", {}).items(): + combined_config["generators"][name] = generator_config + + for name, generator_config in user_config.get("generators", {}).items(): + if name in combined_config["generators"]: + combined_config["generators"][name]["options"].update( + generator_config.get("options", {}) + ) + else: + combined_config["generators"][name] = generator_config + + # Export combined generators.yaml + IOHandler.save_yaml( + combined_config, os.path.join(export_path, "generators.yaml") + ) + + print(f"Combined generators.yaml exported to {export_path}.") + + # Copy user templates + IOHandler.copy_tree(self.template_dir, os.path.join(export_path, "templates")) + + # Copy default templates that are missing in user templates + for root, dirs, files in os.walk(self.default_template_dir): + for file in files: + rel_path = os.path.relpath( + os.path.join(root, file), self.default_template_dir + ) + dst_path = os.path.join(export_path, "templates", rel_path) + if not os.path.exists(dst_path): + IOHandler.copy_file(os.path.join(root, file), dst_path) + + print(f"Templates exported to {export_path}.") + + # Copy user generators + IOHandler.copy_tree(self.generator_dir, os.path.join(export_path, "generators")) + + # Copy default generators that are missing in user generators + default_generators_dir = os.path.join( + os.path.dirname(__file__), "..", "repo", "generators" + ) + for root, dirs, files in os.walk(default_generators_dir): + for file in files: + rel_path = os.path.relpath( + os.path.join(root, file), default_generators_dir + ) + dst_path = os.path.join(export_path, "generators", rel_path) + if not os.path.exists(dst_path): + IOHandler.copy_file(os.path.join(root, file), dst_path) + + print(f"Generators exported to {export_path}.") + + def import_config(self, import_path: str): + """Import generators.yaml, templates, and generators from a specified directory.""" + IOHandler.copy_file( + os.path.join(import_path, "generators.yaml"), self.config_file + ) + IOHandler.copy_tree(os.path.join(import_path, "templates"), self.template_dir) + IOHandler.copy_tree(os.path.join(import_path, "generators"), self.generator_dir) + print(f"Configuration, templates, and generators imported from {import_path}.") diff --git a/djangowiz/repo/generators/__init__.py b/djangowiz/repo/generators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djangowiz/repo/generators/serializer_generator.py b/djangowiz/repo/generators/serializer_generator.py new file mode 100644 index 0000000..39a8a65 --- /dev/null +++ b/djangowiz/repo/generators/serializer_generator.py @@ -0,0 +1,10 @@ +from djangowiz.core.base_generator import BaseGenerator + + +class SerializerGenerator(BaseGenerator): + def generate(self, overwrite: bool = False, template: str = None, **kwargs): + template = self.env.get_template(template) + for model_name in self.model_names: + content = template.render(app_name=self.app_name, model_name=model_name) + file_path = f"{self.app_name}/serializers/{model_name.lower()}.py" + self.write_file(file_path, content, overwrite) diff --git a/djangowiz/repo/generators/viewset_generator.py b/djangowiz/repo/generators/viewset_generator.py new file mode 100644 index 0000000..a416ad6 --- /dev/null +++ b/djangowiz/repo/generators/viewset_generator.py @@ -0,0 +1,10 @@ +from djangowiz.core.base_generator import BaseGenerator + + +class ViewsetGenerator(BaseGenerator): + def generate(self, overwrite: bool = False, template: str = None, **kwargs): + template = self.env.get_template(template) + for model_name in self.model_names: + content = template.render(app_name=self.app_name, model_name=model_name) + file_path = f"{self.app_name}/viewsets/{model_name.lower()}.py" + self.write_file(file_path, content, overwrite) diff --git a/djangowiz/templates/Dockerfile.j2 b/djangowiz/repo/templates/Dockerfile.j2 similarity index 100% rename from djangowiz/templates/Dockerfile.j2 rename to djangowiz/repo/templates/Dockerfile.j2 diff --git a/djangowiz/templates/docker-compose.dev.yml.j2 b/djangowiz/repo/templates/docker-compose.dev.yml.j2 similarity index 100% rename from djangowiz/templates/docker-compose.dev.yml.j2 rename to djangowiz/repo/templates/docker-compose.dev.yml.j2 diff --git a/djangowiz/templates/docker-compose.prod.yml.j2 b/djangowiz/repo/templates/docker-compose.prod.yml.j2 similarity index 100% rename from djangowiz/templates/docker-compose.prod.yml.j2 rename to djangowiz/repo/templates/docker-compose.prod.yml.j2 diff --git a/djangowiz/templates/env.dev.j2 b/djangowiz/repo/templates/env.dev.j2 similarity index 100% rename from djangowiz/templates/env.dev.j2 rename to djangowiz/repo/templates/env.dev.j2 diff --git a/djangowiz/templates/env.prod.j2 b/djangowiz/repo/templates/env.prod.j2 similarity index 100% rename from djangowiz/templates/env.prod.j2 rename to djangowiz/repo/templates/env.prod.j2 diff --git a/djangowiz/templates/multi/serializers.py.j2 b/djangowiz/repo/templates/multi/serializers.py.j2 similarity index 100% rename from djangowiz/templates/multi/serializers.py.j2 rename to djangowiz/repo/templates/multi/serializers.py.j2 diff --git a/djangowiz/templates/multi/viewsets.py.j2 b/djangowiz/repo/templates/multi/viewsets.py.j2 similarity index 100% rename from djangowiz/templates/multi/viewsets.py.j2 rename to djangowiz/repo/templates/multi/viewsets.py.j2 diff --git a/djangowiz/templates/routes.py.j2 b/djangowiz/repo/templates/routes.py.j2 similarity index 100% rename from djangowiz/templates/routes.py.j2 rename to djangowiz/repo/templates/routes.py.j2 diff --git a/djangowiz/templates/single/serializers.py.j2 b/djangowiz/repo/templates/single/serializers.py.j2 similarity index 100% rename from djangowiz/templates/single/serializers.py.j2 rename to djangowiz/repo/templates/single/serializers.py.j2 diff --git a/djangowiz/templates/single/viewsets.py.j2 b/djangowiz/repo/templates/single/viewsets.py.j2 similarity index 100% rename from djangowiz/templates/single/viewsets.py.j2 rename to djangowiz/repo/templates/single/viewsets.py.j2 diff --git a/djangowiz/templates/urls.py.j2 b/djangowiz/repo/templates/urls.py.j2 similarity index 100% rename from djangowiz/templates/urls.py.j2 rename to djangowiz/repo/templates/urls.py.j2