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.
- django_req_generator-0.1.0/.gitignore +44 -0
- django_req_generator-0.1.0/LICENSE +21 -0
- django_req_generator-0.1.0/PKG-INFO +77 -0
- django_req_generator-0.1.0/README.md +63 -0
- django_req_generator-0.1.0/django_req_generator/__init__.py +1 -0
- django_req_generator-0.1.0/django_req_generator/apps.py +7 -0
- django_req_generator-0.1.0/django_req_generator/management/__init__.py +0 -0
- django_req_generator-0.1.0/django_req_generator/management/commands/__init__.py +0 -0
- django_req_generator-0.1.0/django_req_generator/management/commands/generate_reqs.py +145 -0
- django_req_generator-0.1.0/django_req_generator/management/commands/track_reqs.py +42 -0
- django_req_generator-0.1.0/django_req_generator/scanner/__init__.py +0 -0
- django_req_generator-0.1.0/django_req_generator/scanner/ast_analysis.py +38 -0
- django_req_generator-0.1.0/django_req_generator/scanner/django_inspector.py +58 -0
- django_req_generator-0.1.0/django_req_generator/scanner/dynamic_tracker.py +45 -0
- django_req_generator-0.1.0/django_req_generator/utils/__init__.py +0 -0
- django_req_generator-0.1.0/django_req_generator/utils/filter.py +60 -0
- django_req_generator-0.1.0/django_req_generator/utils/i18n.py +88 -0
- django_req_generator-0.1.0/django_req_generator/utils/mapper.py +67 -0
- django_req_generator-0.1.0/django_req_generator/utils/validator.py +88 -0
- django_req_generator-0.1.0/pyproject.toml +27 -0
|
@@ -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"
|
|
File without changes
|
|
File without changes
|
|
@@ -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."))
|
|
File without changes
|
|
@@ -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()
|
|
File without changes
|
|
@@ -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"]
|