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 ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ from .cmd_generate import generate
2
+
3
+ __all__ = [
4
+ "generate",
5
+ ]
@@ -0,0 +1,5 @@
1
+ from .main import generate
2
+
3
+ __all__ = [
4
+ "generate",
5
+ ]
@@ -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,14 @@
1
+ import click
2
+ from .commands import (
3
+ generate,
4
+ )
5
+
6
+ @click.group()
7
+ def cli():
8
+ """CLI para tai-api: Un framework para APIs basado en FastAPI."""
9
+ pass
10
+
11
+ cli.add_command(generate)
12
+
13
+ if __name__ == '__main__':
14
+ cli()
@@ -0,0 +1,3 @@
1
+ from .endpoints import EndpointsGenerator
2
+
3
+ __all__ = ["EndpointsGenerator"]
@@ -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,7 @@
1
+ {{ imports|join('\n') }}
2
+
3
+ generated_api_router = APIRouter()
4
+
5
+ {% for router in routers.values() %}
6
+ generated_api_router.include_router({{ router }})
7
+ {% endfor %}
@@ -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 %}