tai-api 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tai_api/__init__.py +0 -0
- tai_api/cli/commands/__init__.py +5 -0
- tai_api/cli/commands/cmd_generate/__init__.py +5 -0
- tai_api/cli/commands/cmd_generate/funcs.py +40 -0
- tai_api/cli/commands/cmd_generate/generate_endpoints.py +42 -0
- tai_api/cli/commands/cmd_generate/main.py +26 -0
- tai_api/cli/main.py +14 -0
- tai_api/generators/fastapi/__init__.py +3 -0
- tai_api/generators/fastapi/endpoints.py +161 -0
- tai_api/generators/fastapi/templates/__init__.py.j2 +7 -0
- tai_api/generators/fastapi/templates/enums_template.py.j2 +25 -0
- tai_api/generators/fastapi/templates/macros.j2 +96 -0
- tai_api/generators/fastapi/templates/router_template.py.j2 +247 -0
- tai_api/responses/__init__.py +194 -0
- tai_api/responses/exceptions.py +103 -0
- tai_api/responses/handlers.py +283 -0
- tai_api/responses/main.py +130 -0
- tai_api-0.1.1.dist-info/LICENSE +674 -0
- tai_api-0.1.1.dist-info/METADATA +22 -0
- tai_api-0.1.1.dist-info/RECORD +22 -0
- tai_api-0.1.1.dist-info/WHEEL +4 -0
- tai_api-0.1.1.dist-info/entry_points.txt +3 -0
tai_api/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import sys
|
|
3
|
+
from tai_sql.generators import BaseGenerator, ModelsGenerator, CRUDGenerator, ERDiagramGenerator
|
|
4
|
+
from tai_api.generators.fastapi import EndpointsGenerator
|
|
5
|
+
|
|
6
|
+
def run_generate():
|
|
7
|
+
"""Run the configured generators."""
|
|
8
|
+
# Ejecutar cada generador
|
|
9
|
+
click.echo("🚀 Ejecutando generadores...")
|
|
10
|
+
click.echo()
|
|
11
|
+
|
|
12
|
+
models_generator = ModelsGenerator(output_dir="api/api/database")
|
|
13
|
+
crud_generator = CRUDGenerator(output_dir="api/api/database", mode='async')
|
|
14
|
+
er_generator = ERDiagramGenerator(output_dir="api/api/diagrams")
|
|
15
|
+
endpoints_generator = EndpointsGenerator()
|
|
16
|
+
|
|
17
|
+
generators: list[BaseGenerator] = [
|
|
18
|
+
models_generator,
|
|
19
|
+
crud_generator,
|
|
20
|
+
er_generator,
|
|
21
|
+
endpoints_generator
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
for generator in generators:
|
|
25
|
+
try:
|
|
26
|
+
generator_name = generator.__class__.__name__
|
|
27
|
+
click.echo(f"Ejecutando: {click.style(generator_name, bold=True)}")
|
|
28
|
+
|
|
29
|
+
# El generador se encargará de descubrir los modelos internamente
|
|
30
|
+
result = generator.generate()
|
|
31
|
+
|
|
32
|
+
click.echo(f"✅ Generador {generator_name} completado con éxito.")
|
|
33
|
+
if result:
|
|
34
|
+
click.echo(f" Recursos en: {result}")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
click.echo(f"❌ Error al ejecutar el generador {generator_name}: {str(e)}", err=True)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
finally:
|
|
40
|
+
click.echo()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Ejemplo de uso del EndpointsGenerator de tai-api.
|
|
4
|
+
|
|
5
|
+
Este script muestra cómo usar el EndpointsGenerator para generar automáticamente
|
|
6
|
+
endpoints REST para FastAPI basados en los modelos definidos en tai_sql.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from tai_api.generators.fastapi import EndpointsGenerator
|
|
13
|
+
from tai_sql import pm
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
"""
|
|
17
|
+
Función principal que demuestra el uso del EndpointsGenerator.
|
|
18
|
+
"""
|
|
19
|
+
print("🚀 Generando endpoints de FastAPI con tai-api...")
|
|
20
|
+
config = pm.load_config()
|
|
21
|
+
|
|
22
|
+
if config:
|
|
23
|
+
pm.set_current_schema(config.default_schema)
|
|
24
|
+
|
|
25
|
+
# Crear una instancia del generador
|
|
26
|
+
generator = EndpointsGenerator()
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
# Generar los endpoints
|
|
30
|
+
generator.generate()
|
|
31
|
+
print("\n✅ ¡Endpoints generados exitosamente!")
|
|
32
|
+
|
|
33
|
+
except Exception as e:
|
|
34
|
+
import logging
|
|
35
|
+
logging.exception(e)
|
|
36
|
+
print(f"❌ Error al generar endpoints: {e}")
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
sys.exit(main())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import click
|
|
3
|
+
|
|
4
|
+
from tai_sql import pm
|
|
5
|
+
from .funcs import run_generate
|
|
6
|
+
|
|
7
|
+
@click.command()
|
|
8
|
+
@click.option('--schema', '-s', help='Nombre del esquema')
|
|
9
|
+
def generate(schema: str=None):
|
|
10
|
+
"""Genera recursos para la API."""
|
|
11
|
+
|
|
12
|
+
if schema:
|
|
13
|
+
pm.set_current_schema(schema)
|
|
14
|
+
|
|
15
|
+
else:
|
|
16
|
+
config = pm.load_config()
|
|
17
|
+
if config:
|
|
18
|
+
pm.set_current_schema(config.default_schema)
|
|
19
|
+
|
|
20
|
+
if not schema and not pm.db:
|
|
21
|
+
click.echo(f"❌ No existe ningún esquema por defecto", err=True)
|
|
22
|
+
click.echo(f" Puedes definir uno con: tai-sql set-default-schema <nombre>", err=True)
|
|
23
|
+
click.echo(f" O usar la opción: --schema <nombre_esquema>", err=True)
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
|
|
26
|
+
run_generate()
|
tai_api/cli/main.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
import jinja2
|
|
5
|
+
from tai_sql.generators import BaseGenerator
|
|
6
|
+
from tai_sql import pm
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EndpointsGenerator(BaseGenerator):
|
|
10
|
+
"""
|
|
11
|
+
Generador de endpoints para FastAPI.
|
|
12
|
+
|
|
13
|
+
Este generador crea los endpoints necesarios para interactuar con los modelos
|
|
14
|
+
definidos en tai_sql, utilizando la configuración proporcionada.
|
|
15
|
+
|
|
16
|
+
Genera automáticamente:
|
|
17
|
+
- Endpoints CRUD completos para cada tabla
|
|
18
|
+
- Archivos router_{table_name}.py en routers/generated/
|
|
19
|
+
- Importaciones automáticas de DTOs y DAOs
|
|
20
|
+
|
|
21
|
+
Atributos:
|
|
22
|
+
output_dir (str): Directorio donde se generarán los routers
|
|
23
|
+
template_dir (str): Directorio donde están los templates Jinja2
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_jinja_env: ClassVar[jinja2.Environment] = None
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
output_dir: str = "api/api/routers/generated",
|
|
31
|
+
database_resources_path: str = "api/database"):
|
|
32
|
+
"""
|
|
33
|
+
Inicializa el generador de endpoints.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
output_dir: Directorio donde se generarán los archivos router
|
|
37
|
+
template_dir: Directorio donde están los templates Jinja2
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(output_dir)
|
|
40
|
+
self.database_import_path = ".".join(Path(database_resources_path).parts + (pm.db.schema_name,))
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def jinja_env(self) -> jinja2.Environment:
|
|
44
|
+
"""Retorna el entorno Jinja2 configurado"""
|
|
45
|
+
if self._jinja_env is None:
|
|
46
|
+
templates_dir = os.path.join(os.path.dirname(__file__), 'templates')
|
|
47
|
+
self._jinja_env = jinja2.Environment(
|
|
48
|
+
loader=jinja2.FileSystemLoader(templates_dir),
|
|
49
|
+
trim_blocks=True,
|
|
50
|
+
lstrip_blocks=True
|
|
51
|
+
)
|
|
52
|
+
return self._jinja_env
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def crud_class(self) -> str:
|
|
56
|
+
"""Retorna el nombre de la clase CRUD para el esquema actual"""
|
|
57
|
+
return f"{pm.db.schema_name.title()}AsyncDBAPI"
|
|
58
|
+
|
|
59
|
+
def generate(self):
|
|
60
|
+
"""
|
|
61
|
+
Genera todos los archivos router para las tablas definidas en models.
|
|
62
|
+
"""
|
|
63
|
+
# Crear directorio de salida si no existe
|
|
64
|
+
os.makedirs(self.config.output_dir, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
# Generar routers para cada modelo
|
|
67
|
+
self._generate_routers()
|
|
68
|
+
# Generar router para enumeraciones
|
|
69
|
+
self._generate_enumerations_router()
|
|
70
|
+
# Generar archivo __init__.py para importar todos los routers
|
|
71
|
+
self._generate_init_file()
|
|
72
|
+
|
|
73
|
+
def _generate_routers(self):
|
|
74
|
+
"""
|
|
75
|
+
Genera los routers para cada modelo definido en models.
|
|
76
|
+
"""
|
|
77
|
+
# Cargar el template Jinja2
|
|
78
|
+
template = self.jinja_env.get_template("router_template.py.j2")
|
|
79
|
+
|
|
80
|
+
imports = [
|
|
81
|
+
"from fastapi import APIRouter, Depends, Query",
|
|
82
|
+
f"from {self.database_import_path} import *",
|
|
83
|
+
"from api.responses import (",
|
|
84
|
+
" APIResponse, PaginatedResponse, RecordNotFoundException,",
|
|
85
|
+
" ValidationException",
|
|
86
|
+
")",
|
|
87
|
+
"from typing import Optional, List",
|
|
88
|
+
"from tai_alphi import Alphi"
|
|
89
|
+
]
|
|
90
|
+
# Generar router para cada modelo
|
|
91
|
+
for model in self.models:
|
|
92
|
+
model_imports = imports.copy()
|
|
93
|
+
has_datetime = any(col.type == 'datetime' or col.type == 'date' or col.type == 'time' for col in model.columns.values())
|
|
94
|
+
if has_datetime:
|
|
95
|
+
model_imports.append('from datetime import datetime, date, time')
|
|
96
|
+
content = template.render(
|
|
97
|
+
imports=model_imports,
|
|
98
|
+
model=model.info(),
|
|
99
|
+
import_path=self.database_import_path,
|
|
100
|
+
crud_class=self.crud_class
|
|
101
|
+
)
|
|
102
|
+
# Escribir archivo
|
|
103
|
+
output_file = Path(self.config.output_dir) / f"router_{model.tablename}.py"
|
|
104
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
105
|
+
f.write(content)
|
|
106
|
+
|
|
107
|
+
def _generate_enumerations_router(self):
|
|
108
|
+
"""
|
|
109
|
+
Genera el router para todas las enumeraciones del sistema.
|
|
110
|
+
"""
|
|
111
|
+
# Cargar el template Jinja2 para enumeraciones
|
|
112
|
+
template = self.jinja_env.get_template("enums_template.py.j2")
|
|
113
|
+
|
|
114
|
+
# Preparar las importaciones necesarias
|
|
115
|
+
imports = [
|
|
116
|
+
"from fastapi import APIRouter, Depends",
|
|
117
|
+
"from typing import List, Dict, Optional",
|
|
118
|
+
f"from {self.database_import_path} import *",
|
|
119
|
+
"from api.responses import APIResponse"
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
# Renderizar el template
|
|
123
|
+
rendered_code = template.render(
|
|
124
|
+
imports=imports,
|
|
125
|
+
crud_class=self.crud_class,
|
|
126
|
+
enumerations=[enum.info() for enum in pm.db.enums],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Escribir el archivo
|
|
130
|
+
output_file = Path(self.config.output_dir) / "router_enums.py"
|
|
131
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
132
|
+
f.write(rendered_code)
|
|
133
|
+
|
|
134
|
+
def _generate_init_file(self):
|
|
135
|
+
"""
|
|
136
|
+
Genera el archivo __init__.py para importar todos los routers generados.
|
|
137
|
+
"""
|
|
138
|
+
# Cargar el template Jinja2 para enumeraciones
|
|
139
|
+
template = self.jinja_env.get_template("__init__.py.j2")
|
|
140
|
+
|
|
141
|
+
init_content = ["# Archivo generado automáticamente por tai-api", ""]
|
|
142
|
+
|
|
143
|
+
# Importar todos los routers
|
|
144
|
+
imports = ["from fastapi import APIRouter"]
|
|
145
|
+
routers = {}
|
|
146
|
+
for model in self.models:
|
|
147
|
+
routers[f"router_{model.tablename}"] = f"{model.tablename}_router"
|
|
148
|
+
|
|
149
|
+
routers["router_enums"] = "enumerations_router"
|
|
150
|
+
|
|
151
|
+
imports.extend([f"from .{router_file} import {router_var}" for router_file, router_var in routers.items()])
|
|
152
|
+
|
|
153
|
+
rendered_code = template.render(
|
|
154
|
+
imports=imports,
|
|
155
|
+
routers=routers,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Escribir archivo
|
|
159
|
+
init_file = os.path.join(self.config.output_dir, "__init__.py")
|
|
160
|
+
with open(init_file, 'w', encoding='utf-8') as f:
|
|
161
|
+
f.write(rendered_code)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{#- Template Jinja2 para generar router de enumeraciones -#}
|
|
2
|
+
{#- Este template genera endpoints GET para todas las enumeraciones del sistema -#}
|
|
3
|
+
{{ imports|join('\n') }}
|
|
4
|
+
|
|
5
|
+
enumerations_router = APIRouter(
|
|
6
|
+
prefix="/enums",
|
|
7
|
+
tags=["Enumeraciones"]
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
{% for enum in enumerations %}
|
|
11
|
+
@enumerations_router.get("/{{ enum.hypen_name }}", tags=["Enumeraciones"], response_model=APIResponse[List[{{ enum.type }}]])
|
|
12
|
+
async def get_{{ enum.name }}_enumeration(
|
|
13
|
+
api: {{ crud_class }} = Depends({{ crud_class }})
|
|
14
|
+
) -> APIResponse[List[{{ enum.type }}]]:
|
|
15
|
+
"""
|
|
16
|
+
Obtiene los valores de la enumeración {{ enum.name }}.
|
|
17
|
+
"""
|
|
18
|
+
values = api.{{ enum.name }}.find_many()
|
|
19
|
+
|
|
20
|
+
return APIResponse.success(
|
|
21
|
+
data=values,
|
|
22
|
+
message="Enumeración {{ enum.name }} obtenida exitosamente"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
{% endfor %}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
{% macro generate_query_parameters(model) -%}
|
|
4
|
+
{% for column in model.columns -%}
|
|
5
|
+
{% if not column.args.get('autoincrement', False) -%}
|
|
6
|
+
{% if column.args.get('primary_key', False) or column.is_foreign_key -%}
|
|
7
|
+
{{ column.name }}: Optional[{{ column.type }}] = None,
|
|
8
|
+
{% elif 'str' == column.type or 'Text' == column.type or 'bool' == column.type -%}
|
|
9
|
+
{{ column.name }}: Optional[{{ column.type }}] = None,
|
|
10
|
+
{% elif 'date' == column.type or 'int' == column.type or 'BigInteger' == column.type -%}
|
|
11
|
+
{{ column.name }}: Optional[{{ column.type }}] = None,
|
|
12
|
+
min_{{ column.name }}: Optional[{{ column.type }}] = None,
|
|
13
|
+
max_{{ column.name }}: Optional[{{ column.type }}] = None,
|
|
14
|
+
{% elif 'float' == column.type or 'Numeric' == column.type or 'datetime' == column.type or 'time' == column.type -%}
|
|
15
|
+
min_{{ column.name }}: Optional[{{ column.type }}] = None,
|
|
16
|
+
max_{{ column.name }}: Optional[{{ column.type }}] = None,
|
|
17
|
+
{% else -%}
|
|
18
|
+
{{ column.name }}: Optional[{{ column.type }}] = None,
|
|
19
|
+
{%- endif -%}
|
|
20
|
+
{%- endif -%}
|
|
21
|
+
{%- endfor -%}
|
|
22
|
+
{%- endmacro -%}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
{% macro generate_query_args(model) -%}
|
|
26
|
+
{% for column in model.columns -%}
|
|
27
|
+
{% if not column.args.get('autoincrement', False) -%}
|
|
28
|
+
{% if column.args.get('primary_key', False) or column.is_foreign_key -%}
|
|
29
|
+
{{ column.name }}: Filtrar por {{ column.name }}
|
|
30
|
+
{% elif 'str' == column.type or 'Text' == column.type or 'bool' == column.type -%}
|
|
31
|
+
{{ column.name }}: Filtrar por {{ column.name }}
|
|
32
|
+
{% elif 'date' == column.type or 'int' == column.type or 'BigInteger' == column.type -%}
|
|
33
|
+
{{ column.name }}: Filtrar por {{ column.name }}
|
|
34
|
+
min_{{ column.name }}: Filtrar por fecha mínima (incluída)
|
|
35
|
+
max_{{ column.name }}: Filtrar por fecha máxima (incluída)
|
|
36
|
+
{% elif 'float' == column.type or 'Numeric' == column.type or 'datetime' == column.type or 'time' == column.type -%}
|
|
37
|
+
min_{{ column.name }}: Filtrar por valor mínimo de {{ column.name }} (incluído)
|
|
38
|
+
max_{{ column.name }}: Filtrar por valor máximo de {{ column.name }} (incluído)
|
|
39
|
+
{% else -%}
|
|
40
|
+
{{ column.name }}: Filtrar por {{ column.name }}
|
|
41
|
+
{%- endif -%}
|
|
42
|
+
{%- endif -%}
|
|
43
|
+
{%- endfor -%}
|
|
44
|
+
{%- endmacro %}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
{% macro asing_parameters(model) -%}
|
|
48
|
+
{% for column in model.columns -%}
|
|
49
|
+
{% if not column.args.get('autoincrement', False) -%}
|
|
50
|
+
{% if column.args.get('primary_key', False) or column.is_foreign_key -%}
|
|
51
|
+
{{ column.name }}={{ column.name }},
|
|
52
|
+
{% elif 'str' == column.type or 'Text' == column.type or 'bool' == column.type -%}
|
|
53
|
+
{{ column.name }}={{ column.name }},
|
|
54
|
+
{% elif 'date' == column.type or 'int' == column.type or 'BigInteger' == column.type -%}
|
|
55
|
+
{{ column.name }}={{ column.name }},
|
|
56
|
+
min_{{ column.name }}=min_{{ column.name }},
|
|
57
|
+
max_{{ column.name }}=max_{{ column.name }},
|
|
58
|
+
{% elif 'float' == column.type or 'Numeric' == column.type or 'datetime' == column.type or 'time' == column.type -%}
|
|
59
|
+
min_{{ column.name }}=min_{{ column.name }},
|
|
60
|
+
max_{{ column.name }}=max_{{ column.name }},
|
|
61
|
+
{% else -%}
|
|
62
|
+
{{ column.name }}={{ column.name }},
|
|
63
|
+
{%- endif -%}
|
|
64
|
+
{%- endif -%}
|
|
65
|
+
{%- endfor -%}
|
|
66
|
+
{%- endmacro %}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
{% macro generate_filter_query(model) -%}
|
|
70
|
+
{% for column in model.columns -%}
|
|
71
|
+
{% if not column.args.get('autoincrement', False) -%}
|
|
72
|
+
{% if column.args.get('primary_key', False) or column.is_foreign_key -%}
|
|
73
|
+
if {{ column.name }} is not None:
|
|
74
|
+
query = query.where({{ model.name }}.{{ column.name }} == {{ column.name }})
|
|
75
|
+
{% elif 'str' == column.type or 'Text' == column.type or 'bool' == column.type -%}
|
|
76
|
+
if {{ column.name }} is not None:
|
|
77
|
+
query = query.where({{ model.name }}.{{ column.name }} == {{ column.name }})
|
|
78
|
+
{% elif 'date' == column.type or 'int' == column.type or 'BigInteger' == column.type -%}
|
|
79
|
+
if {{ column.name }} is not None:
|
|
80
|
+
query = query.where({{ model.name }}.{{ column.name }} == {{ column.name }})
|
|
81
|
+
if min_{{ column.name }} is not None:
|
|
82
|
+
query = query.where({{ model.name }}.{{ column.name }} >= min_{{ column.name }})
|
|
83
|
+
if max_{{ column.name }} is not None:
|
|
84
|
+
query = query.where({{ model.name }}.{{ column.name }} <= max_{{ column.name }})
|
|
85
|
+
{% elif 'float' == column.type or 'Numeric' == column.type or 'datetime' == column.type or 'time' == column.type -%}
|
|
86
|
+
if min_{{ column.name }} is not None:
|
|
87
|
+
query = query.where({{ model.name }}.{{ column.name }} >= min_{{ column.name }})
|
|
88
|
+
if max_{{ column.name }} is not None:
|
|
89
|
+
query = query.where({{ model.name }}.{{ column.name }} <= max_{{ column.name }})
|
|
90
|
+
{% else -%}
|
|
91
|
+
if {{ column.name }} is not None:
|
|
92
|
+
query = query.where({{ model.name }}.{{ column.name }} == {{ column.name }})
|
|
93
|
+
{%- endif -%}
|
|
94
|
+
{%- endif -%}
|
|
95
|
+
{%- endfor -%}
|
|
96
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
{#- Template Jinja2 para generar routers de FastAPI -#}
|
|
2
|
+
{#- Este template genera endpoints CRUD completos para cada tabla -#}
|
|
3
|
+
{#- Confia en los manejadores globales de excepciones para el manejo de errores -#}
|
|
4
|
+
{{ imports|join('\n') }}
|
|
5
|
+
|
|
6
|
+
{% set router_name = model.tablename + "_router" %}
|
|
7
|
+
{% set pk_path_params = "/{" + (model.columns | selectattr('args.primary_key', 'equalto', True) | map(attribute='name') | join('}/{')) + "}/" %}
|
|
8
|
+
{% import "macros.j2" as macros %}
|
|
9
|
+
|
|
10
|
+
logger = Alphi.get_logger_by_name("tai-api")
|
|
11
|
+
|
|
12
|
+
{{ router_name }} = APIRouter(
|
|
13
|
+
prefix="/{{ model.tablename | replace('_', '-') }}",
|
|
14
|
+
tags=["{{ model.name }}"]
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
@{{ router_name }}.get("/", tags=["{{ model.name }}"], response_model=APIResponse[List[{{ model.name }}Read]])
|
|
18
|
+
async def {{ model.tablename }}_find_many(
|
|
19
|
+
limit: Optional[int] = None,
|
|
20
|
+
offset: Optional[int] = None,
|
|
21
|
+
{{ macros.generate_query_parameters(model).rstrip('\n') | indent(4) }}
|
|
22
|
+
includes: List[str] = Query(None),
|
|
23
|
+
api: {{ crud_class }} = Depends({{ crud_class }})
|
|
24
|
+
) -> APIResponse[List[{{ model.name }}Read]]:
|
|
25
|
+
"""
|
|
26
|
+
Obtiene una lista de {{ model.name }}s con filtros opcionales.
|
|
27
|
+
"""
|
|
28
|
+
# Validaciones básicas de entrada
|
|
29
|
+
if limit is not None and limit < 0:
|
|
30
|
+
raise ValidationException("El límite no puede ser negativo", "limit")
|
|
31
|
+
if offset is not None and offset < 0:
|
|
32
|
+
raise ValidationException("El offset no puede ser negativo", "offset")
|
|
33
|
+
if limit is not None and limit > 1000:
|
|
34
|
+
raise ValidationException("El límite no puede ser mayor a 1000", "limit")
|
|
35
|
+
|
|
36
|
+
result = await api.{{ model.tablename }}.find_many(
|
|
37
|
+
limit=limit,
|
|
38
|
+
offset=offset,
|
|
39
|
+
{{ macros.asing_parameters(model).rstrip('\n') | indent(8) }}
|
|
40
|
+
includes=includes
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Obtener el total para metadatos de paginación si es necesario
|
|
44
|
+
total = None
|
|
45
|
+
if limit is not None or offset is not None:
|
|
46
|
+
try:
|
|
47
|
+
total = await api.{{ model.tablename }}.count(
|
|
48
|
+
{{ macros.asing_parameters(model).rstrip('\n') | indent(16) }}
|
|
49
|
+
)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.warning(f"No se pudo obtener el total de registros: {str(e)}")
|
|
52
|
+
|
|
53
|
+
return PaginatedResponse.success_paginated(
|
|
54
|
+
data=result,
|
|
55
|
+
total=total,
|
|
56
|
+
limit=limit,
|
|
57
|
+
offset=offset,
|
|
58
|
+
message=f"{{ model.name }}s obtenidos exitosamente"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
{% if not model.is_view %}
|
|
62
|
+
@{{ router_name }}.get("{{ pk_path_params }}", tags=["{{ model.name }}"], response_model=APIResponse[{{ model.name }}Read])
|
|
63
|
+
async def {{ model.tablename }}_find(
|
|
64
|
+
{% for column in model.columns if column.args.get('primary_key', False) %}
|
|
65
|
+
{{ column.name }}: {{ column.type }},
|
|
66
|
+
{% endfor %}
|
|
67
|
+
includes: List[str] = Query(None),
|
|
68
|
+
api: {{ crud_class }} = Depends({{ crud_class }})
|
|
69
|
+
) -> APIResponse[{{ model.name }}Read]:
|
|
70
|
+
"""
|
|
71
|
+
Obtiene un {{ model.name }} por su primary key.
|
|
72
|
+
"""
|
|
73
|
+
# Validaciones básicas de entrada
|
|
74
|
+
{% for column in model.columns if column.args.get('primary_key', False) and column.args.get('autoincrement', False) %}
|
|
75
|
+
{% if column.type in ['int', 'BigInteger'] %}
|
|
76
|
+
if {{ column.name }} <= 0:
|
|
77
|
+
raise ValidationException("{{ column.name }} debe ser mayor a 0", "{{ column.name }}")
|
|
78
|
+
{% endif %}
|
|
79
|
+
{% endfor %}
|
|
80
|
+
|
|
81
|
+
result = await api.{{ model.tablename }}.find(
|
|
82
|
+
{% for column in model.columns if column.args.get('primary_key', False) %}
|
|
83
|
+
{{ column.name }}={{ column.name }},
|
|
84
|
+
{% endfor %}
|
|
85
|
+
includes=includes
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if result is None:
|
|
89
|
+
raise RecordNotFoundException("{{ model.name }}")
|
|
90
|
+
|
|
91
|
+
return APIResponse.success(
|
|
92
|
+
data=result,
|
|
93
|
+
message="{{ model.name }} obtenido exitosamente"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@{{ router_name }}.get("/count", tags=["{{ model.name }}"], response_model=APIResponse[int])
|
|
97
|
+
async def {{ model.tablename }}_count(
|
|
98
|
+
{{ macros.generate_query_parameters(model).rstrip('\n') | indent(4) }}
|
|
99
|
+
api: {{ crud_class }} = Depends({{ crud_class }})
|
|
100
|
+
) -> APIResponse[int]:
|
|
101
|
+
"""
|
|
102
|
+
Cuenta el número de {{ model.name }}s que coinciden con los filtros.
|
|
103
|
+
"""
|
|
104
|
+
result = await api.{{ model.tablename }}.count(
|
|
105
|
+
{{ macros.asing_parameters(model).rstrip('\n') | indent(8) }}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return APIResponse.success(
|
|
109
|
+
data=result,
|
|
110
|
+
message="Conteo realizado exitosamente"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@{{ router_name }}.get("/exists", tags=["{{ model.name }}"], response_model=APIResponse[bool])
|
|
114
|
+
async def {{ model.tablename }}_exists(
|
|
115
|
+
{{ macros.generate_query_parameters(model).rstrip('\n') | indent(4) }}
|
|
116
|
+
api: {{ crud_class }} = Depends({{ crud_class }})
|
|
117
|
+
) -> APIResponse[bool]:
|
|
118
|
+
"""
|
|
119
|
+
Verifica si existe al menos un {{ model.name }} que coincida con los filtros.
|
|
120
|
+
"""
|
|
121
|
+
result = await api.{{ model.tablename }}.exists(
|
|
122
|
+
{{ macros.asing_parameters(model).rstrip('\n') | indent(8) }}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return APIResponse.success(
|
|
126
|
+
data=result,
|
|
127
|
+
message="Verificación realizada exitosamente"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@{{ router_name }}.post("/", tags=["{{ model.name }}"], response_model=APIResponse[{{ model.name }}Read])
|
|
131
|
+
async def {{ model.tablename }}_create(
|
|
132
|
+
{{ model.tablename }}: {{ model.name }}Create,
|
|
133
|
+
api: {{ crud_class }} = Depends({{ crud_class }})
|
|
134
|
+
) -> APIResponse[{{ model.name }}Read]:
|
|
135
|
+
"""
|
|
136
|
+
Crea un nuevo {{ model.name }}.
|
|
137
|
+
"""
|
|
138
|
+
result = await api.{{ model.tablename }}.create({{ model.tablename }})
|
|
139
|
+
|
|
140
|
+
return APIResponse.success(
|
|
141
|
+
data=result,
|
|
142
|
+
message="{{ model.name }} creado exitosamente"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
@{{ router_name }}.patch("{{ pk_path_params }}", tags=["{{ model.name }}"], response_model=APIResponse[int])
|
|
146
|
+
async def {{ model.tablename }}_update(
|
|
147
|
+
{% for column in model.columns if column.args.get('primary_key', False) %}
|
|
148
|
+
{{ column.name }}: {{ column.type }},
|
|
149
|
+
{% endfor %}
|
|
150
|
+
values: {{ model.name }}UpdateValues,
|
|
151
|
+
api: {{ crud_class }} = Depends({{ crud_class }})
|
|
152
|
+
) -> APIResponse[int]:
|
|
153
|
+
"""
|
|
154
|
+
Actualiza un {{ model.name }} específico.
|
|
155
|
+
"""
|
|
156
|
+
# Validaciones básicas de entrada
|
|
157
|
+
{% for column in model.columns if column.args.get('primary_key', False) %}
|
|
158
|
+
{% if column.type in ['int', 'BigInteger'] %}
|
|
159
|
+
if {{ column.name }} <= 0:
|
|
160
|
+
raise ValidationException("{{ column.name }} debe ser mayor a 0", "{{ column.name }}")
|
|
161
|
+
{% endif %}
|
|
162
|
+
{% endfor %}
|
|
163
|
+
|
|
164
|
+
# Verificar que el registro existe antes de actualizar
|
|
165
|
+
existing = await api.{{ model.tablename }}.find(
|
|
166
|
+
{% for column in model.columns if column.args.get('primary_key', False) %}
|
|
167
|
+
{{ column.name }}={{ column.name }},
|
|
168
|
+
{% endfor %}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if existing is None:
|
|
172
|
+
raise RecordNotFoundException("{{ model.name }}")
|
|
173
|
+
|
|
174
|
+
result = await api.{{ model.tablename }}.update(
|
|
175
|
+
{% for column in model.columns if column.args.get('primary_key', False) %}
|
|
176
|
+
{{ column.name }}={{ column.name }},
|
|
177
|
+
{% endfor %}
|
|
178
|
+
updated_values=values
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if result == 0:
|
|
182
|
+
raise RecordNotFoundException("{{ model.name }}")
|
|
183
|
+
|
|
184
|
+
return APIResponse.success(
|
|
185
|
+
data=result,
|
|
186
|
+
message="{{ model.name }} actualizado exitosamente"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
@{{ router_name }}.patch("/", tags=["{{ model.name }}"], response_model=APIResponse[int])
|
|
190
|
+
async def {{ model.tablename }}_update_many(
|
|
191
|
+
payload: {{ model.name }}Update,
|
|
192
|
+
api: {{ crud_class }} = Depends({{ crud_class }})
|
|
193
|
+
) -> APIResponse[int]:
|
|
194
|
+
"""
|
|
195
|
+
Actualiza múltiples {{ model.name }}s.
|
|
196
|
+
"""
|
|
197
|
+
result = await api.{{ model.tablename }}.update_many(payload)
|
|
198
|
+
|
|
199
|
+
message = f"{result} {{ model.name }}s actualizados exitosamente" if result > 0 else "No se encontraron registros que coincidan con los criterios"
|
|
200
|
+
|
|
201
|
+
return APIResponse.success(
|
|
202
|
+
data=result,
|
|
203
|
+
message=message
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
@{{ router_name }}.delete("{{ pk_path_params }}", tags=["{{ model.name }}"], response_model=APIResponse[int])
|
|
207
|
+
async def {{ model.tablename }}_delete(
|
|
208
|
+
{% for column in model.columns if column.args.get('primary_key', False) %}
|
|
209
|
+
{{ column.name }}: {{ column.type }},
|
|
210
|
+
{% endfor %}
|
|
211
|
+
api: {{ crud_class }} = Depends({{ crud_class }})
|
|
212
|
+
) -> APIResponse[int]:
|
|
213
|
+
"""
|
|
214
|
+
Elimina un {{ model.name }} por su primary key.
|
|
215
|
+
"""
|
|
216
|
+
# Validaciones básicas de entrada
|
|
217
|
+
{% for column in model.columns if column.args.get('primary_key', False) %}
|
|
218
|
+
{% if column.type in ['int', 'BigInteger'] %}
|
|
219
|
+
if {{ column.name }} <= 0:
|
|
220
|
+
raise ValidationException("{{ column.name }} debe ser mayor a 0", "{{ column.name }}")
|
|
221
|
+
{% endif %}
|
|
222
|
+
{% endfor %}
|
|
223
|
+
|
|
224
|
+
# Verificar que el registro existe antes de eliminar
|
|
225
|
+
existing = await api.{{ model.tablename }}.find(
|
|
226
|
+
{% for column in model.columns if column.args.get('primary_key', False) %}
|
|
227
|
+
{{ column.name }}={{ column.name }},
|
|
228
|
+
{% endfor %}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if existing is None:
|
|
232
|
+
raise RecordNotFoundException("{{ model.name }}")
|
|
233
|
+
|
|
234
|
+
result = await api.{{ model.tablename }}.delete(
|
|
235
|
+
{% for column in model.columns if column.args.get('primary_key', False) %}
|
|
236
|
+
{{ column.name }}={{ column.name }},
|
|
237
|
+
{% endfor %}
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if result == 0:
|
|
241
|
+
raise RecordNotFoundException("{{ model.name }}")
|
|
242
|
+
|
|
243
|
+
return APIResponse.success(
|
|
244
|
+
data=result,
|
|
245
|
+
message="{{ model.name }} eliminado exitosamente"
|
|
246
|
+
)
|
|
247
|
+
{% endif %}
|