django-req-generator 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ share/python-wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+ MANIFEST
24
+
25
+ # Virtualenv
26
+ .venv/
27
+ venv/
28
+ ENV/
29
+ env/
30
+
31
+ # Django
32
+ local_settings.py
33
+ db.sqlite3
34
+ db.sqlite3-journal
35
+ media/
36
+ staticfiles/
37
+
38
+ # IDEs
39
+ .vscode/
40
+ .idea/
41
+
42
+ # Plugin specific
43
+ temp_venv/
44
+ requirements.txt.tmp
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oscar Higuera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-req-generator
3
+ Version: 0.1.0
4
+ Summary: Generador de requirements.txt avanzado para Django con análisis estático y dinámico.
5
+ Project-URL: Homepage, https://github.com/rraczo/django-req-generator
6
+ Author-email: Oscar Higuera <higuera86@gmail.com>
7
+ License-File: LICENSE
8
+ Classifier: Framework :: Django
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.8
12
+ Requires-Dist: django>=3.2
13
+ Description-Content-Type: text/markdown
14
+
15
+ # django-req-generator 🚀
16
+
17
+ **Generador inteligente de `requirements.txt` / Smart `requirements.txt` generator**
18
+
19
+ ---
20
+
21
+ ## 🇪🇸 Español
22
+
23
+ ### Descripción
24
+ Este plugin para Django sirve para empaquetar tu proyecto e instalarlo de forma limpia en otros entornos (Docker, Servidores de Producción, CI/CD). Está específicamente diseñado para manejar proyectos Django complejos donde no basta con un simple `pip freeze`.
25
+
26
+ ### Características Principales
27
+ - 🔍 **Análisis Estático (AST)**: Detecta imports reales en todo el árbol de tu código fuente.
28
+ - 🧩 **Inspección Profunda de Django**: Analiza `INSTALLED_APPS`, `MIDDLEWARE`, `DATABASES` (detecta drivers como `oracledb`), y `CACHES` (detecta `django-redis` y `pymemcache`).
29
+ - 🧹 **Limpieza Automática**: Filtra la librería estándar de Python y tus propios módulos locales (`apps`, `models`, `serializers`, etc.).
30
+ - 🔗 **Resolución Dinámica PyPI**: No usa mapeos manuales fallidos; pregunta directamente a la API de PyPI para encontrar el paquete correcto.
31
+ - 🤖 **Auto-curación Interactiva**: Durante la validación, si detecta un módulo faltante (`ModuleNotFoundError`), te pregunta si quieres añadirlo y reintenta la validación en caliente.
32
+ - 🧪 **Validación en Venv**: Crea un entorno virtual temporal para asegurar que el archivo generado permite que el proyecto arranque.
33
+ - 🌎 **Multilingüe**: Soporta comandos y mensajes en Español e Inglés.
34
+
35
+ ### Uso
36
+ ```bash
37
+ # Generación estándar con backup automático
38
+ python manage.py generate_reqs
39
+
40
+ # Generación con validación y settings específicos
41
+ python manage.py generate_reqs --validate --settings=mi_proyecto.settings_docker
42
+
43
+ # Modo Desarrollo (instala el plugin desde código fuente en la validación)
44
+ python manage.py generate_reqs --validate -d /ruta/al/plugin
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 🇺🇸 English
50
+
51
+ ### Description
52
+ This Django plugin is designed to package your project and install it cleanly in other environments (Docker, Production Servers, CI/CD). It is specifically built for complex Django projects where a simple `pip freeze` isn't enough.
53
+
54
+ ### Key Features
55
+ - 🔍 **Static Analysis (AST)**: Deep-scans your entire source code to detect actual imports.
56
+ - 🧩 **Deep Django Inspection**: Analyzes `INSTALLED_APPS`, `MIDDLEWARE`, `DATABASES` (detects drivers like `oracledb`), and `CACHES` (detects `django-redis` and `pymemcache`).
57
+ - 🧹 **Automatic Cleanup**: Filters out Python's standard library and your own local modules (`apps`, `models`, `serializers`, etc.).
58
+ - 🔗 **Dynamic PyPI Resolution**: Replaces outdated manual mappings by querying the PyPI API directly for the correct package name.
59
+ - 🤖 **Interactive Self-Healing**: During validation, if a missing module is found (`ModuleNotFoundError`), it asks if you want to add it and retries the validation on the fly.
60
+ - 🧪 **Venv Validation**: Creates a temporary virtual environment to ensure the generated file allows the project to start.
61
+ - 🌎 **Multilingual**: Supports commands and console messages in both Spanish and English.
62
+
63
+ ### Usage
64
+ ```bash
65
+ # Standard generation with automatic backup
66
+ python manage.py generate_reqs
67
+
68
+ # Generation with validation and specific settings
69
+ python manage.py generate_reqs --validate --settings=my_project.settings_docker
70
+
71
+ # Development Mode (installs plugin from source during validation)
72
+ python manage.py generate_reqs --validate -d /path/to/plugin
73
+ ```
74
+
75
+ ---
76
+
77
+ **Made with ❤️ and AI Collaboration | Creado con ❤️ y el apoyo de IA**
@@ -0,0 +1,63 @@
1
+ # django-req-generator 🚀
2
+
3
+ **Generador inteligente de `requirements.txt` / Smart `requirements.txt` generator**
4
+
5
+ ---
6
+
7
+ ## 🇪🇸 Español
8
+
9
+ ### Descripción
10
+ Este plugin para Django sirve para empaquetar tu proyecto e instalarlo de forma limpia en otros entornos (Docker, Servidores de Producción, CI/CD). Está específicamente diseñado para manejar proyectos Django complejos donde no basta con un simple `pip freeze`.
11
+
12
+ ### Características Principales
13
+ - 🔍 **Análisis Estático (AST)**: Detecta imports reales en todo el árbol de tu código fuente.
14
+ - 🧩 **Inspección Profunda de Django**: Analiza `INSTALLED_APPS`, `MIDDLEWARE`, `DATABASES` (detecta drivers como `oracledb`), y `CACHES` (detecta `django-redis` y `pymemcache`).
15
+ - 🧹 **Limpieza Automática**: Filtra la librería estándar de Python y tus propios módulos locales (`apps`, `models`, `serializers`, etc.).
16
+ - 🔗 **Resolución Dinámica PyPI**: No usa mapeos manuales fallidos; pregunta directamente a la API de PyPI para encontrar el paquete correcto.
17
+ - 🤖 **Auto-curación Interactiva**: Durante la validación, si detecta un módulo faltante (`ModuleNotFoundError`), te pregunta si quieres añadirlo y reintenta la validación en caliente.
18
+ - 🧪 **Validación en Venv**: Crea un entorno virtual temporal para asegurar que el archivo generado permite que el proyecto arranque.
19
+ - 🌎 **Multilingüe**: Soporta comandos y mensajes en Español e Inglés.
20
+
21
+ ### Uso
22
+ ```bash
23
+ # Generación estándar con backup automático
24
+ python manage.py generate_reqs
25
+
26
+ # Generación con validación y settings específicos
27
+ python manage.py generate_reqs --validate --settings=mi_proyecto.settings_docker
28
+
29
+ # Modo Desarrollo (instala el plugin desde código fuente en la validación)
30
+ python manage.py generate_reqs --validate -d /ruta/al/plugin
31
+ ```
32
+
33
+ ---
34
+
35
+ ## 🇺🇸 English
36
+
37
+ ### Description
38
+ This Django plugin is designed to package your project and install it cleanly in other environments (Docker, Production Servers, CI/CD). It is specifically built for complex Django projects where a simple `pip freeze` isn't enough.
39
+
40
+ ### Key Features
41
+ - 🔍 **Static Analysis (AST)**: Deep-scans your entire source code to detect actual imports.
42
+ - 🧩 **Deep Django Inspection**: Analyzes `INSTALLED_APPS`, `MIDDLEWARE`, `DATABASES` (detects drivers like `oracledb`), and `CACHES` (detects `django-redis` and `pymemcache`).
43
+ - 🧹 **Automatic Cleanup**: Filters out Python's standard library and your own local modules (`apps`, `models`, `serializers`, etc.).
44
+ - 🔗 **Dynamic PyPI Resolution**: Replaces outdated manual mappings by querying the PyPI API directly for the correct package name.
45
+ - 🤖 **Interactive Self-Healing**: During validation, if a missing module is found (`ModuleNotFoundError`), it asks if you want to add it and retries the validation on the fly.
46
+ - 🧪 **Venv Validation**: Creates a temporary virtual environment to ensure the generated file allows the project to start.
47
+ - 🌎 **Multilingual**: Supports commands and console messages in both Spanish and English.
48
+
49
+ ### Usage
50
+ ```bash
51
+ # Standard generation with automatic backup
52
+ python manage.py generate_reqs
53
+
54
+ # Generation with validation and specific settings
55
+ python manage.py generate_reqs --validate --settings=my_project.settings_docker
56
+
57
+ # Development Mode (installs plugin from source during validation)
58
+ python manage.py generate_reqs --validate -d /path/to/plugin
59
+ ```
60
+
61
+ ---
62
+
63
+ **Made with ❤️ and AI Collaboration | Creado con ❤️ y el apoyo de IA**
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoReqGeneratorConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "django_req_generator"
7
+ verbose_name = "Django Requirements Generator"
@@ -0,0 +1,145 @@
1
+ from django.core.management.base import BaseCommand
2
+ import os
3
+ from django_req_generator.utils.i18n import _
4
+
5
+
6
+ class Command(BaseCommand):
7
+ help = _("command_help")
8
+
9
+ def add_arguments(self, parser):
10
+ parser.add_argument(
11
+ "-o",
12
+ "--output",
13
+ default="requirements.txt",
14
+ help=_("arg_output_help"),
15
+ )
16
+ parser.add_argument(
17
+ "--validate",
18
+ action="store_true",
19
+ help=_("arg_validate_help"),
20
+ )
21
+ parser.add_argument(
22
+ "-d",
23
+ "--develop",
24
+ help=_("arg_develop_help"),
25
+ )
26
+
27
+ def handle(self, *args, **options):
28
+ output_file = options["output"]
29
+ validate = options["validate"]
30
+
31
+ self.stdout.write(self.style.SUCCESS(_("start_gen", file=output_file)))
32
+
33
+ # 1. Estrategias de escaneo
34
+ from django_req_generator.scanner import ast_analysis, django_inspector, dynamic_tracker
35
+ from django_req_generator.utils import mapper, filter, validator
36
+
37
+ self.stdout.write(_("scan_ast"))
38
+ project_root = os.getcwd()
39
+ ast_modules = ast_analysis.scan_directory(project_root)
40
+
41
+ self.stdout.write(_("scan_django"))
42
+ django_modules = django_inspector.inspect_settings()
43
+
44
+ self.stdout.write(_("load_dynamic"))
45
+ dynamic_modules = dynamic_tracker.load_tracked_modules()
46
+
47
+ all_modules = ast_modules.union(django_modules).union(dynamic_modules)
48
+
49
+ # 2. Filtrado y Mapeo
50
+ self.stdout.write(_("filter_map"))
51
+ # Filtrar librería estándar primero
52
+ all_cleaned = filter.filter_standard_library(all_modules)
53
+ # Filtrar archivos y carpetas locales (los tuyos)
54
+ clean_modules = filter.filter_local_modules(all_cleaned, project_root)
55
+
56
+ # Log para depuración si verbosity >= 2
57
+ if options.get("verbosity", 1) >= 2:
58
+ self.stdout.write(f"DEBUG: Módulos detectados tras limpieza: {clean_modules}")
59
+
60
+ package_versions = mapper.map_modules_to_packages(clean_modules)
61
+
62
+ # 3. Filtrado de dependencias transitivas
63
+ final_packages = filter.filter_transitive_dependencies(package_versions)
64
+
65
+ # 4. Backup solo si es el nombre por defecto (requirements.txt) y ya existe
66
+ if output_file == "requirements.txt" and os.path.exists(output_file):
67
+ n = 1
68
+ while os.path.exists(f"{output_file}.backup_{n}"):
69
+ n += 1
70
+ backup_name = f"{output_file}.backup_{n}"
71
+ os.rename(output_file, backup_name)
72
+ self.stdout.write(self.style.WARNING(_("backup_created", backup=backup_name)))
73
+
74
+ # 5. Escritura del archivo
75
+ with open(output_file, "w", encoding="utf-8") as f:
76
+ f.write(_("header_autogen") + "\n")
77
+ f.write(_("header_repo") + "\n\n")
78
+
79
+ for pkg, ver in sorted(final_packages.items()):
80
+ if ver == "???":
81
+ f.write(f"{pkg}\n")
82
+ else:
83
+ f.write(f"{pkg}=={ver}\n")
84
+
85
+ self.stdout.write(self.style.SUCCESS(_("write_success", file=output_file, count=len(final_packages))))
86
+
87
+ if validate:
88
+ self.stdout.write(self.style.WARNING(_("warn_production")))
89
+ confirm = input(_("prompt_continue")).lower()
90
+
91
+ if confirm not in ["y", "s", "yes", "si"]:
92
+ self.stdout.write(self.style.NOTICE(_("validate_skipped")))
93
+ else:
94
+ self.stdout.write(_("validate_start"))
95
+ # Capturar el módulo de configuración y la ruta del plugin
96
+ settings_module = os.environ.get("DJANGO_SETTINGS_MODULE")
97
+
98
+ # Priorizar flag --develop para la ruta del plugin
99
+ plugin_root = options.get("develop")
100
+ if not plugin_root:
101
+ # Fallback si no se pasa el flag (intentar subir niveles)
102
+ plugin_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
103
+
104
+ # Gestión de entorno temporal fuera del bucle para reutilización óptima
105
+ import tempfile
106
+ with tempfile.TemporaryDirectory() as venv_dir:
107
+ # Bucle de auto-curación interactivo
108
+ while True:
109
+ report = validator.validate_requirements(
110
+ output_file,
111
+ project_root,
112
+ settings_module=settings_module,
113
+ plugin_root=plugin_root,
114
+ log_callback=self.stdout.write,
115
+ venv_dir=venv_dir
116
+ )
117
+
118
+ if report["success"]:
119
+ self.stdout.write(self.style.SUCCESS(_("validate_success")))
120
+ break
121
+
122
+ # Si falló, mirar si es por un módulo faltante (lazy import)
123
+ missing_mod = report.get("missing_module")
124
+ if missing_mod:
125
+ confirm_add = input(_("prompt_missing_module", module=missing_mod)).lower()
126
+ if confirm_add in ["y", "s", "yes", "si"]:
127
+ # Mapear y añadir a la lista actual
128
+ new_pkgs = mapper.map_modules_to_packages({missing_mod})
129
+ final_packages.update(new_pkgs)
130
+
131
+ # Sobrescribir el archivo con la nueva librería
132
+ with open(output_file, "w", encoding="utf-8") as f:
133
+ f.write(_("header_autogen") + "\n")
134
+ f.write(_("header_repo") + "\n\n")
135
+ for pkg, ver in sorted(final_packages.items()):
136
+ if ver == "???":
137
+ f.write(f"{pkg}\n")
138
+ else:
139
+ f.write(f"{pkg}=={ver}\n")
140
+
141
+ # Reintentar validación
142
+ continue
143
+
144
+ self.stdout.write(self.style.ERROR(_("validate_error", output=report['output'])))
145
+ break
@@ -0,0 +1,42 @@
1
+ from django.core.management import ManagementUtility
2
+ from django.core.management.base import BaseCommand
3
+ import sys
4
+
5
+
6
+ class Command(BaseCommand):
7
+ help = "Registra dependencias dinámicas durante la ejecución de un comando de Django (ej. runserver)."
8
+
9
+ def add_arguments(self, parser):
10
+ parser.add_argument(
11
+ "subcommand",
12
+ nargs="*",
13
+ help="El comando de Django a ejecutar (ej. runserver, test)",
14
+ )
15
+
16
+ def handle(self, *args, **options):
17
+ subcommand = options["subcommand"]
18
+ if not subcommand:
19
+ self.stdout.write(self.style.ERROR("Faltan argumentos para ejecutar el comando subyacente (ej. runserver)."))
20
+ return
21
+
22
+ from django_req_generator.scanner import dynamic_tracker
23
+
24
+ self.stdout.write(self.style.SUCCESS(f"Activando rastreador dinámico..."))
25
+ dynamic_tracker.start_tracking()
26
+
27
+ try:
28
+ # Ejecutamos el comando de Django dentro del mismo proceso
29
+ # para que el Hook de importación capture todo.
30
+ utility = ManagementUtility([sys.argv[0]] + subcommand)
31
+ utility.execute()
32
+ except KeyboardInterrupt:
33
+ self.stdout.write("\nRastreo interrumpido por el usuario.")
34
+ finally:
35
+ dynamic_tracker.stop_tracking()
36
+ used_modules = dynamic_tracker.get_tracked_modules()
37
+ dynamic_tracker.save_tracked_modules()
38
+
39
+ self.stdout.write(self.style.SUCCESS(f"\nSe detectaron {len(used_modules)} módulos raíces usados dinámicamente."))
40
+ self.stdout.write(self.style.SUCCESS(f"Los hallazgos se guardaron en '.tracked_modules.json'."))
41
+
42
+ self.stdout.write(self.style.WARNING("\nUsa 'generate_reqs' para consolidar estos hallazgos."))
@@ -0,0 +1,38 @@
1
+ import ast
2
+ import os
3
+
4
+
5
+ def scan_directory(path):
6
+ """Escanea un directorio buscando archivos .py y extrayendo sus imports."""
7
+ imports = set()
8
+ ignore_dirs = {".git", "venv", ".venv", "__pycache__", "node_modules", "dist", "build", ".pytest_cache", ".tox"}
9
+
10
+ for root, dirs, files in os.walk(path):
11
+ dirs[:] = [d for d in dirs if d not in ignore_dirs]
12
+
13
+ for file in files:
14
+ if file.endswith(".py"):
15
+ file_path = os.path.join(root, file)
16
+ # Llamada al extractor por cada archivo
17
+ imports.update(get_imports_from_file(file_path))
18
+ return imports
19
+
20
+
21
+ def get_imports_from_file(file_path):
22
+ """Extrae los nombres de los módulos importados en un archivo usando AST."""
23
+ file_imports = set()
24
+ try:
25
+ with open(file_path, "r", encoding="utf-8") as f:
26
+ tree = ast.parse(f.read())
27
+
28
+ for node in ast.walk(tree):
29
+ if isinstance(node, ast.Import):
30
+ for n in node.names:
31
+ file_imports.add(n.name.split(".")[0])
32
+ elif isinstance(node, ast.ImportFrom):
33
+ if node.module:
34
+ file_imports.add(node.module.split(".")[0])
35
+ except Exception:
36
+ # Ignorar archivos que no se pueden parsear (ej. errores de sintaxis)
37
+ pass
38
+ return file_imports
@@ -0,0 +1,58 @@
1
+ from django.conf import settings
2
+
3
+
4
+ def inspect_settings():
5
+ """Extrae nombres de módulos de las configuraciones de Django."""
6
+ found_modules = set()
7
+
8
+ # Si estamos inspeccionando esto, Django DEBE estar
9
+ found_modules.add("django")
10
+
11
+ # 1. INSTALLED_APPS
12
+ apps = getattr(settings, "INSTALLED_APPS", [])
13
+ for app in apps:
14
+ if isinstance(app, str):
15
+ app = app.strip()
16
+ if app:
17
+ found_modules.add(app.split(".")[0])
18
+
19
+ # 2. MIDDLEWARE
20
+ middlewares = getattr(settings, "MIDDLEWARE", [])
21
+ for middleware in middlewares:
22
+ found_modules.add(middleware.split(".")[0])
23
+
24
+ # 3. CACHES (Detectar django-redis, etc.)
25
+ caches = getattr(settings, "CACHES", {})
26
+ for cache_config in caches.values():
27
+ backend = cache_config.get("BACKEND", "")
28
+ if backend:
29
+ found_modules.add(backend.split(".")[0])
30
+
31
+ # 4. AUTHENTICATION_BACKENDS
32
+ auth_backends = getattr(settings, "AUTHENTICATION_BACKENDS", [])
33
+ for auth in auth_backends:
34
+ found_modules.add(auth.split(".")[0])
35
+
36
+ # 5. DATABASES (Motores internos de Django y sus drivers)
37
+ databases = getattr(settings, "DATABASES", {})
38
+ db_drivers = {
39
+ "django.db.backends.postgresql": "psycopg2", # o psycopg
40
+ "django.db.backends.mysql": "mysqlclient",
41
+ "django.db.backends.oracle": "oracledb",
42
+ "django.db.backends.sqlite3": None, # stdlib
43
+ }
44
+
45
+ for db_config in databases.values():
46
+ engine = db_config.get("ENGINE", "")
47
+ if engine:
48
+ if engine in db_drivers:
49
+ driver = db_drivers[engine]
50
+ if driver:
51
+ found_modules.add(driver)
52
+ else:
53
+ # Si es un motor de terceros
54
+ parts = engine.split(".")
55
+ if len(parts) > 1 and parts[0] != "django":
56
+ found_modules.add(parts[0])
57
+
58
+ return found_modules
@@ -0,0 +1,45 @@
1
+ import builtins
2
+
3
+
4
+ _used_modules = set()
5
+ _original_import = builtins.__import__
6
+
7
+
8
+ def tracking_import(name, *args, **kwargs):
9
+ """Hook que registra el nombre del módulo raíz importado."""
10
+ if name:
11
+ root_module = name.split(".")[0]
12
+ _used_modules.add(root_module)
13
+ return _original_import(name, *args, **kwargs)
14
+
15
+
16
+ def start_tracking():
17
+ """Activa el Hook de importación."""
18
+ builtins.__import__ = tracking_import
19
+
20
+
21
+ def stop_tracking():
22
+ """Detiene el Hook de importación y restaura el original."""
23
+ builtins.__import__ = _original_import
24
+
25
+
26
+ def get_tracked_modules():
27
+ """Devuelve la lista de módulos registrados."""
28
+ return _used_modules
29
+
30
+
31
+ def save_tracked_modules(file_path=".tracked_modules.json"):
32
+ """Guarda los módulos rastreados en un archivo JSON."""
33
+ import json
34
+ with open(file_path, "w") as f:
35
+ json.dump(list(_used_modules), f)
36
+
37
+
38
+ def load_tracked_modules(file_path=".tracked_modules.json"):
39
+ """Carga módulos rastreados desde un archivo JSON."""
40
+ import json
41
+ import os
42
+ if os.path.exists(file_path):
43
+ with open(file_path, "r") as f:
44
+ return set(json.load(f))
45
+ return set()
@@ -0,0 +1,60 @@
1
+ import sys
2
+ import os
3
+ import importlib.metadata as md
4
+
5
+ def filter_standard_library(module_names):
6
+ """Filtra módulos que pertenecen a la librería estándar de Python."""
7
+ if sys.version_info >= (3, 10):
8
+ stdlib_names = sys.stdlib_module_names
9
+ else:
10
+ from distutils.sysconfig import get_python_lib
11
+ stdlib_names = set(os.listdir(get_python_lib(standard_lib=True)))
12
+
13
+ return {name for name in module_names if name not in stdlib_names}
14
+
15
+
16
+ def filter_local_modules(module_names, project_root):
17
+ """
18
+ Filtra módulos locales pero prioriza los de Pip si hay coincidencia de nombre.
19
+ """
20
+ # 1. Identificar módulos instalados en Pip (para evitar falsos positivos locales)
21
+ pkg_dist = md.packages_distributions()
22
+
23
+ # 2. Identificar todos los nombres locales del proyecto (carpetas y archivos .py)
24
+ local_names_found = set()
25
+ root_name = os.path.basename(project_root.rstrip(os.path.sep))
26
+ local_names_found.add(root_name)
27
+
28
+ ignore_dirs = {".git", "venv", ".venv", "__pycache__", "node_modules"}
29
+ for root, dirs, files in os.walk(project_root):
30
+ dirs[:] = [d for d in dirs if d not in ignore_dirs]
31
+ for d in dirs:
32
+ local_names_found.add(d)
33
+ for f in files:
34
+ if f.endswith(".py"):
35
+ local_names_found.add(f[:-3])
36
+
37
+ non_local = set()
38
+ for name in module_names:
39
+ # WHITELIST: Apps críticas que NUNCA deben filtrarse
40
+ if name.lower() in ["django", "ckeditor", "ckeditor_uploader"]:
41
+ non_local.add(name)
42
+ continue
43
+
44
+ # Si el nombre es un paquete de pip real, se mantiene
45
+ if name in pkg_dist:
46
+ non_local.add(name)
47
+ continue
48
+
49
+ # Si NO es de pip y SÍ está en nuestra lista de carpetas/archivos locales, se filtra
50
+ if name in local_names_found:
51
+ continue
52
+
53
+ non_local.add(name)
54
+
55
+ return non_local
56
+
57
+
58
+ def filter_transitive_dependencies(package_versions):
59
+ """Inactivo para asegurar visibilidad de lxml/Django."""
60
+ return package_versions
@@ -0,0 +1,88 @@
1
+ import locale
2
+ import os
3
+
4
+ # Diccionario de traducciones
5
+ MESSAGES = {
6
+ "es": {
7
+ "command_help": "Genera un archivo requirements.txt basado en análisis estático y de configuración.",
8
+ "arg_output_help": "Nombre del archivo de salida (por defecto: requirements.txt)",
9
+ "arg_validate_help": "Valida el archivo generado en un entorno temporal.",
10
+ "arg_develop_help": "Ruta local del código del plugin para instalarlo durante la validación (modo desarrollo).",
11
+ "start_gen": "Iniciando generación de {file}...",
12
+ "scan_ast": "Escaneando código fuente (AST)...",
13
+ "scan_django": "Inspeccionando configuraciones de Django...",
14
+ "load_dynamic": "Cargando hallazgos dinámicos previos...",
15
+ "filter_map": "Filtrando librería estándar y mapeando a paquetes...",
16
+ "backup_created": "Archivo existente respaldado en: {backup}",
17
+ "write_success": "Archivo {file} generado con {count} dependencias.",
18
+ "header_autogen": "# Archivo autogenerado por django-req-generator",
19
+ "header_repo": "# Repositorio: https://github.com/oscar/django-req-generator",
20
+ "validate_start": "Validando requisitos en entorno temporal...",
21
+ "validate_success": "¡Validación exitosa! El proyecto arranca correctamente.",
22
+ "validate_error": "Error de validación:\n{output}",
23
+ "error_no_manage": "No se encontró manage.py para validar.",
24
+ "log_create_venv": "Creando entorno virtual temporal en: {path}",
25
+ "log_install_plugin": "Instalando el plugin local desde: {path}",
26
+ "log_install_reqs": "Instalando dependencias desde: {file}",
27
+ "log_running_check": "Ejecutando 'python manage.py check' con settings: {settings}",
28
+ "warn_production": "¡ADVERTENCIA!: El flag --validate crea un entorno virtual temporal y realiza instalaciones. NO se recomienda su uso en servidores de producción reales.",
29
+ "prompt_continue": "¿Desea continuar con la validación? (S/n): ",
30
+ "validate_skipped": "Validación saltada por el usuario.",
31
+ "prompt_missing_module": "Se detectó que falta el módulo '{module}'. ¿Desea agregarlo a los requisitos y reintentar? (S/n): ",
32
+ },
33
+ "en": {
34
+ "command_help": "Generates a requirements.txt file based on static and configuration analysis.",
35
+ "arg_output_help": "Output filename (default: requirements.txt)",
36
+ "arg_validate_help": "Validates the generated file in a temporary environment.",
37
+ "arg_develop_help": "Local path of the plugin source code for installation during validation (development mode).",
38
+ "start_gen": "Starting generation for {file}...",
39
+ "scan_ast": "Scanning source code (AST)...",
40
+ "scan_django": "Inspecting Django configurations...",
41
+ "load_dynamic": "Loading previous dynamic findings...",
42
+ "filter_map": "Filtering standard library and mapping to packages...",
43
+ "backup_created": "Existing file backed up to: {backup}",
44
+ "write_success": "File {file} generated with {count} dependencies.",
45
+ "header_autogen": "# File auto-generated by django-req-generator",
46
+ "header_repo": "# Repository: https://github.com/oscar/django-req-generator",
47
+ "validate_start": "Validating requirements in temporary environment...",
48
+ "validate_success": "Validation successful! The project starts correctly.",
49
+ "validate_error": "Validation error:\n{output}",
50
+ "error_no_manage": "manage.py was not found for validation.",
51
+ "log_create_venv": "Creating temporary virtual environment in: {path}",
52
+ "log_install_plugin": "Installing local plugin from: {path}",
53
+ "log_install_reqs": "Installing dependencies from: {file}",
54
+ "log_running_check": "Running 'python manage.py check' with settings: {settings}",
55
+ "warn_production": "WARNING!: The --validate flag creates a temporary virtual environment and performs installations. It is NOT recommended for use on real production servers.",
56
+ "prompt_continue": "Do you want to continue with validation? (Y/n): ",
57
+ "validate_skipped": "Validation skipped by user.",
58
+ "prompt_missing_module": "Module '{module}' was detected as missing. Would you like to add it to requirements and retry? (Y/n): ",
59
+ }
60
+ }
61
+
62
+ def get_language():
63
+ """Detecta el idioma del sistema."""
64
+ # Primero intentar con la variable de entorno
65
+ lang_env = os.environ.get("LANG", "").split(".")[0].split("_")[0].lower()
66
+ if lang_env in MESSAGES:
67
+ return lang_env
68
+
69
+ # Después intentar con locale
70
+ try:
71
+ lang_loc, _ = locale.getdefaultlocale()
72
+ if lang_loc:
73
+ lang_loc = lang_loc.split("_")[0].lower()
74
+ if lang_loc in MESSAGES:
75
+ return lang_loc
76
+ except Exception:
77
+ pass
78
+
79
+ return "es" # Por defecto español
80
+
81
+ CURRENT_LANG = get_language()
82
+
83
+ def _(key, **kwargs):
84
+ """Traduce la clave según el idioma detectado."""
85
+ text = MESSAGES.get(CURRENT_LANG, MESSAGES["es"]).get(key, key)
86
+ if kwargs:
87
+ return text.format(**kwargs)
88
+ return text
@@ -0,0 +1,67 @@
1
+ import importlib.metadata as md
2
+ import urllib.request
3
+ import json
4
+ import logging
5
+
6
+ def check_pypi_existence(package_name):
7
+ """
8
+ Verifica si un paquete existe en PyPI usando su API JSON.
9
+ Devuelve el nombre oficial si existe, None si no.
10
+ """
11
+ url = f"https://pypi.org/pypi/{package_name}/json"
12
+ try:
13
+ with urllib.request.urlopen(url, timeout=3) as response:
14
+ if response.status == 200:
15
+ data = json.loads(response.read().decode())
16
+ return data.get("info", {}).get("name", package_name)
17
+ except Exception:
18
+ pass
19
+ return None
20
+
21
+ def map_modules_to_packages(module_names):
22
+ """
23
+ Mapea módulos a paquetes de forma dinámica.
24
+ Prioriza lo instalado y usa PyPI como fallback inteligente.
25
+ """
26
+ pkg_dist = md.packages_distributions()
27
+ canonical_names = {dist.metadata.get("Name").lower(): dist.metadata.get("Name")
28
+ for dist in md.distributions() if dist.metadata.get("Name")}
29
+
30
+ mapped_packages = {}
31
+
32
+ for module in module_names:
33
+ # Django es innegociable
34
+ if module == "django":
35
+ try:
36
+ import django as dj
37
+ mapped_packages["Django"] = dj.get_version()
38
+ continue
39
+ except Exception: pass
40
+
41
+ # 1. ¿Está instalado localmente? (La vía rápida)
42
+ if module in pkg_dist:
43
+ dist_name = pkg_dist[module][0]
44
+ official_name = canonical_names.get(dist_name.lower(), dist_name)
45
+ try:
46
+ mapped_packages[official_name] = md.version(dist_name)
47
+ continue
48
+ except Exception: pass
49
+
50
+ # 2. ¿No está instalado? Vamos a preguntarle a PyPI (Detección Dinámica)
51
+ # Probamos el nombre original (con guiones si tiene guiones bajos)
52
+ search_name = module.replace("_", "-")
53
+ pypi_name = check_pypi_existence(search_name)
54
+
55
+ if pypi_name:
56
+ mapped_packages[pypi_name] = "???"
57
+ else:
58
+ # 3. No existe tal cual... ¿será una app de Django? Probemos con prefijo
59
+ django_guess = f"django-{search_name}"
60
+ pypi_name = check_pypi_existence(django_guess)
61
+ if pypi_name:
62
+ mapped_packages[pypi_name] = "???"
63
+ else:
64
+ # 4. Fallback final: lo incluimos como el módulo mismo
65
+ mapped_packages[search_name] = "???"
66
+
67
+ return mapped_packages
@@ -0,0 +1,88 @@
1
+ import subprocess
2
+ import venv
3
+ import tempfile
4
+ import os
5
+ import shutil
6
+
7
+ from django_req_generator.utils.i18n import _
8
+
9
+ def validate_requirements(requirements_file_path, project_root, settings_module=None, plugin_root=None, log_callback=None, venv_dir=None):
10
+ """
11
+ Ejecuta django check en un entorno virtual.
12
+ Si venv_dir es proporcionado, reutiliza ese entorno (mucho más rápido).
13
+ """
14
+ report = {"success": True, "output": ""}
15
+
16
+ def log(msg):
17
+ if log_callback:
18
+ log_callback(msg)
19
+
20
+ # Si no nos pasan un venv_dir, creamos uno efímero (comportamiento antiguo)
21
+ is_temporary = venv_dir is None
22
+ if is_temporary:
23
+ venv_dir = tempfile.mkdtemp()
24
+
25
+ try:
26
+ # Determinar el binario de python y pip
27
+ if os.name == "nt":
28
+ python_exe = os.path.join(venv_dir, "Scripts", "python.exe")
29
+ pip_exe = os.path.join(venv_dir, "Scripts", "pip.exe")
30
+ else:
31
+ python_exe = os.path.join(venv_dir, "bin", "python")
32
+ pip_exe = os.path.join(venv_dir, "bin", "pip")
33
+
34
+ # 1. Crear el entorno si no existe todavía
35
+ if not os.path.exists(python_exe):
36
+ log(_("log_create_venv", path=venv_dir))
37
+ venv.create(venv_dir, with_pip=True)
38
+
39
+ # 2. Instalar el propio plugin solo la primera vez
40
+ if plugin_root:
41
+ log(_("log_install_plugin", path=plugin_root))
42
+ subprocess.run(
43
+ [pip_exe, "install", plugin_root],
44
+ check=True, capture_output=True, text=True
45
+ )
46
+
47
+ # 3. Instalar los requisitos (Pip es inteligente y solo instalará lo nuevo)
48
+ log(_("log_install_reqs", file=requirements_file_path))
49
+ subprocess.run(
50
+ [pip_exe, "install", "-r", requirements_file_path],
51
+ check=True, capture_output=True, text=True
52
+ )
53
+
54
+ # 4. Ejecutar django check
55
+ manage_py = os.path.join(project_root, "manage.py")
56
+ if not os.path.exists(manage_py):
57
+ return {"success": False, "output": _("error_no_manage")}
58
+
59
+ log(_("log_running_check", settings=settings_module or "Default"))
60
+ check_cmd = [python_exe, manage_py, "check"]
61
+ if settings_module:
62
+ check_cmd.append(f"--settings={settings_module}")
63
+
64
+ result = subprocess.run(
65
+ check_cmd, check=True, capture_output=True, text=True
66
+ )
67
+ report["output"] = result.stdout
68
+
69
+ except subprocess.CalledProcessError as e:
70
+ report["success"] = False
71
+ output = (e.stdout or "") + (e.stderr or "")
72
+ report["output"] = output
73
+
74
+ # Intentar extraer el módulo faltante del traceback
75
+ import re
76
+ match = re.search(r"ModuleNotFoundError: No module named '([^']+)'", output)
77
+ if match:
78
+ report["missing_module"] = match.group(1)
79
+ except Exception as e:
80
+ report["success"] = False
81
+ report["output"] = str(e)
82
+ finally:
83
+ # Solo lo borramos si es de la sesión efímera.
84
+ # Si lo gestiona generate_reqs.py, allí se debe borrar.
85
+ if is_temporary and os.path.exists(venv_dir):
86
+ shutil.rmtree(venv_dir)
87
+
88
+ return report
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "django-req-generator"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name = "Oscar Higuera", email = "higuera86@gmail.com" },
10
+ ]
11
+ description = "Generador de requirements.txt avanzado para Django con análisis estático y dinámico."
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ classifiers = [
15
+ "Framework :: Django",
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ ]
19
+ dependencies = [
20
+ "django>=3.2",
21
+ ]
22
+
23
+ [project.urls]
24
+ "Homepage" = "https://github.com/rraczo/django-req-generator"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["django_req_generator"]