tai-api 0.2.2__tar.gz → 0.2.4__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.
Files changed (52) hide show
  1. {tai_api-0.2.2 → tai_api-0.2.4}/PKG-INFO +3 -3
  2. {tai_api-0.2.2 → tai_api-0.2.4}/pyproject.toml +3 -3
  3. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_auth/main.py +19 -11
  4. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_init/resources/__init__.py +1 -10
  5. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_init/resources/handlers.py +2 -1
  6. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/keycloak.py +1 -1
  7. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/templates/kc/dependencies.py.j2 +6 -2
  8. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/templates/kc/keycloak_router.py.j2 +14 -47
  9. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/templates/kc/utils.py.j2 +0 -6
  10. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/routers/templates/router_template.py.j2 +28 -2
  11. {tai_api-0.2.2 → tai_api-0.2.4}/LICENSE +0 -0
  12. {tai_api-0.2.2 → tai_api-0.2.4}/README.md +0 -0
  13. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/__init__.py +0 -0
  14. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/__init__.py +0 -0
  15. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_auth/__init__.py +0 -0
  16. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_auth/database.py +0 -0
  17. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_auth/keycloak.py +0 -0
  18. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_dev/__init__.py +0 -0
  19. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_dev/main.py +0 -0
  20. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_generate/__init__.py +0 -0
  21. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_generate/funcs.py +0 -0
  22. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_generate/main.py +0 -0
  23. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_init/__init__.py +0 -0
  24. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_init/main.py +0 -0
  25. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_init/model.py +0 -0
  26. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_init/resources/exceptions.py +0 -0
  27. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_init/resources/mcp.py +0 -0
  28. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_init/resources/responses.py +0 -0
  29. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_mcp/__init__.py +0 -0
  30. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/commands/cmd_mcp/main.py +0 -0
  31. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/cli/main.py +0 -0
  32. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/config.py +0 -0
  33. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/__init__.py +0 -0
  34. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/__init__.py +0 -0
  35. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/db.py +0 -0
  36. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/templates/db/__init__.py.j2 +0 -0
  37. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/templates/db/auth_router.py.j2 +0 -0
  38. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/templates/db/dependencies.py.j2 +0 -0
  39. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/templates/db/jwt.py.j2 +0 -0
  40. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/templates/kc/__init__.py.j2 +0 -0
  41. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/auth/templates/kc/login_router.py.j2 +0 -0
  42. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/main_file/__init__.py +0 -0
  43. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/main_file/main.py +0 -0
  44. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/main_file/templates/__dev__.py.j2 +0 -0
  45. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/main_file/templates/__main__.py.j2 +0 -0
  46. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/routers/__init__.py +0 -0
  47. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/routers/main.py +0 -0
  48. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/routers/templates/__init__.py.j2 +0 -0
  49. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/routers/templates/__outerinit__.py.j2 +0 -0
  50. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/routers/templates/enums_template.py.j2 +0 -0
  51. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/generators/routers/templates/macros.j2 +0 -0
  52. {tai_api-0.2.2 → tai_api-0.2.4}/tai_api/project.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tai-api
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary:
5
5
  License-File: LICENSE
6
6
  Author: MateoSaezMata
@@ -20,8 +20,8 @@ Requires-Dist: jinja2 (>=3.1.0,<4.0.0)
20
20
  Requires-Dist: pydantic (>=2.0.0,<3.0.0)
21
21
  Requires-Dist: python-jose (>=3.5.0,<4.0.0)
22
22
  Requires-Dist: tai-alphi (>=1.0.4,<2.0.0)
23
- Requires-Dist: tai-keycloak (>=0.1.13,<0.2.0)
24
- Requires-Dist: tai-sql (>=0.3.45,<0.4.0)
23
+ Requires-Dist: tai-keycloak (>=0.1.14,<0.2.0)
24
+ Requires-Dist: tai-sql (>=0.3.48,<0.4.0)
25
25
  Requires-Dist: uvicorn[standard] (>=0.30.0,<0.31.0)
26
26
  Description-Content-Type: text/markdown
27
27
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "tai-api"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = ""
5
5
  authors = ["MateoSaezMata <msaez@triplealpha.in>"]
6
6
  readme = "README.md"
@@ -8,7 +8,7 @@ readme = "README.md"
8
8
  [tool.poetry.dependencies]
9
9
  python = "^3.10"
10
10
  fastapi = {extras = ["standard"], version = "^0.116.1"}
11
- tai-sql = "^0.3.45"
11
+ tai-sql = "^0.3.48"
12
12
  jinja2 = "^3.1.0"
13
13
  pydantic = "^2.0.0"
14
14
  uvicorn = {extras = ["standard"], version = "^0.30.0"}
@@ -16,7 +16,7 @@ tai-alphi = "^1.0.4"
16
16
  asyncpg = "^0.30.0"
17
17
  python-jose = "^3.5.0"
18
18
  fastapi-mcp = "^0.4.0"
19
- tai-keycloak = "^0.1.13"
19
+ tai-keycloak = "^0.1.14"
20
20
  click = "^8.3.0"
21
21
 
22
22
  [tool.poetry.scripts]
@@ -48,18 +48,18 @@ def set_auth():
48
48
  else:
49
49
  click.echo("❌ Opción no válida. Por favor selecciona 1 o 2.")
50
50
 
51
- # Verificar configuración de tai-sql
52
- sqlconfig = sqlpm.get_project_config()
53
- if not sqlconfig:
54
- click.echo("❌ No se encontró la configuración de tai-sql.", err=True)
55
- click.echo(" Asegúrate de haber inicializado el proyecto con tai-sql init.", err=True)
56
- sys.exit(1)
57
-
58
- # Establecer esquema por defecto si existe
59
- if sqlpm.config.default_schema:
60
- sqlpm.set_current_schema(sqlconfig.default_schema)
61
-
62
51
  if auth_type == "database":
52
+
53
+ # Verificar configuración de tai-sql
54
+ sqlconfig = sqlpm.get_project_config()
55
+ if not sqlconfig:
56
+ click.echo("❌ No se encontró la configuración de tai-sql.", err=True)
57
+ click.echo(" Asegúrate de haber inicializado el proyecto con tai-sql init.", err=True)
58
+ sys.exit(1)
59
+
60
+ # Establecer esquema por defecto si existe
61
+ if sqlpm.config.default_schema:
62
+ sqlpm.set_current_schema(sqlconfig.default_schema)
63
63
 
64
64
  # Verificar que existe información de la base de datos
65
65
  if not sqlpm.db or not sqlpm.db.tables:
@@ -135,6 +135,14 @@ def set_auth():
135
135
  sys.exit(1)
136
136
 
137
137
  if pm.config.has_routers:
138
+
139
+ # Verificar configuración de tai-sql
140
+ sqlconfig = sqlpm.get_project_config()
141
+ if not sqlconfig:
142
+ click.echo("❌ No se encontró la configuración de tai-sql.", err=True)
143
+ click.echo(" Asegúrate de haber inicializado el proyecto con tai-sql init.", err=True)
144
+ sys.exit(1)
145
+
138
146
  for schema_name in sqlpm.discover_schemas():
139
147
  sqlpm.set_current_schema(schema_name)
140
148
  click.echo(f"Ejecutando: {click.style(RoutersGenerator.__name__, bold=True)}")
@@ -30,11 +30,6 @@ from .responses import (
30
30
  PaginatedResponse
31
31
  )
32
32
 
33
- from .decorators import (
34
- document_api_response,
35
- document_paginated_response
36
- )
37
-
38
33
  from .handlers import setup_exception_handlers
39
34
 
40
35
  from .mcp import set_mcp
@@ -46,11 +41,7 @@ __all__ = [
46
41
  "PaginationMeta",
47
42
  "APIResponse",
48
43
  "PaginatedResponse",
49
-
50
- # Decoradores de documentación
51
- "document_api_response",
52
- "document_paginated_response",
53
-
44
+
54
45
  # Excepciones
55
46
  "APIException",
56
47
  "ErrorCode",
@@ -73,7 +73,8 @@ async def insufficient_permissions_handler(request: Request, exc: UnAuthorizedEx
73
73
 
74
74
  api_error = APIError(
75
75
  code=ErrorCode.INSUFFICIENT_PERMISSIONS,
76
- message="Permisos insuficientes para acceder al recurso"
76
+ message=exc.message,
77
+ details=exc.details
77
78
  )
78
79
 
79
80
  response = APIResponse.error(api_error)
@@ -87,8 +87,8 @@ class AuthKeycloakGenerator:
87
87
  context = {
88
88
  'project_name': pm.config.name,
89
89
  'resources_import_path': pm.config.resources_import_path,
90
- 'crud_import_path': pm.config.crud_import_path,
91
90
  }
91
+
92
92
  login_content = login_template.render(**context)
93
93
  keycloak_content = keycloak_template.render(**context)
94
94
 
@@ -74,8 +74,12 @@ def requires_permissions(action: Literal['admin', 'read', 'write', 'update', 'de
74
74
  token: Annotated[AccessToken, Depends(validate_token)]
75
75
  ) -> AccessToken:
76
76
 
77
- # Construir el permiso requerido en formato recurso:acción
78
- required_perm = f"{resource}:{action}"
77
+ # Resolver el permiso 'sudo'
78
+ if 'sudo' in token.api_roles:
79
+ return token
80
+
81
+ # Construir el permiso requerido en formato recurso-acción
82
+ required_perm = f"{resource}-{action}"
79
83
 
80
84
  # Validar que el token tenga el permiso requerido
81
85
  if required_perm not in token.api_roles:
@@ -4,14 +4,13 @@ Generado automáticamente por tai-api set-auth
4
4
 
5
5
  Este módulo contiene endpoints para la gestión de usuarios en Keycloak.
6
6
  """
7
- from tai_keycloak import User
7
+ from tai_keycloak import User, Group, RLSAttribute
8
8
  from fastapi import APIRouter, Depends
9
9
  from typing import List
10
10
  from api.resources import APIResponse, PaginatedResponse
11
- from {{ crud_import_path }} import *
12
11
 
13
12
  from .dependencies import requires_permissions
14
- from .utils import raise_if_exception, KeycloakDep, RLSAttribute
13
+ from .utils import raise_if_exception, KeycloakDep
15
14
 
16
15
  kc_router = APIRouter(tags=["Keycloak"], dependencies=[Depends(requires_permissions('admin', 'keycloak'))])
17
16
 
@@ -56,7 +55,7 @@ async def list_users(
56
55
  message=f"Usuarios obtenidos exitosamente"
57
56
  )
58
57
 
59
- @kc_router.get("/role", response_model=APIResponse[List[User]])
58
+ @kc_router.get("/role", response_model=APIResponse[List[Group]])
60
59
  async def list_roles(
61
60
  kc: KeycloakDep
62
61
  ):
@@ -83,17 +82,17 @@ async def get_user(
83
82
  message="Usuario obtenido exitosamente"
84
83
  )
85
84
 
86
- @kc_router.patch("/user", response_model=APIResponse[User])
85
+ @kc_router.patch("/user/{username}", response_model=APIResponse[User])
87
86
  async def update_user(
87
+ username: str,
88
88
  user: User,
89
89
  kc: KeycloakDep
90
90
  ):
91
91
  """
92
- Actualizar detalles del usuario
93
- NOTA: El objeto user debe incluir el username.
92
+ Actualizar atributos de un usuario en Keycloak
94
93
  """
95
94
 
96
- result = raise_if_exception(await kc.user.update(user))
95
+ result = raise_if_exception(await kc.user.update(username, user))
97
96
 
98
97
  return APIResponse.success(
99
98
  data=result.data,
@@ -153,29 +152,23 @@ async def remove_role_from_user(
153
152
  # ============ Endpoints de Atributos RLS (Row Level Security) ============
154
153
  # Métodos disponibles: add_attribute, remove_attribute
155
154
 
156
- @kc_router.post("/user/{username}/rls", response_model=APIResponse[bool])
157
- async def add_rls_condition(
155
+ @kc_router.put("/user/{username}/rls", response_model=APIResponse[bool])
156
+ async def set_rls(
158
157
  username: str,
159
- rls_attribute: RLSAttribute,
158
+ rls_attributes: List[RLSAttribute],
160
159
  kc: KeycloakDep
161
160
  ):
162
161
  """Definir el RLS para un usuario para seguridad a nivel de fila"""
163
162
 
164
- raise_if_exception(await kc.user.add_attribute(username, f'rls.{rls_attribute.table}.{rls_attribute.field}', rls_attribute.values))
163
+ raise_if_exception(await kc.user.set_attributes(username, rls_attributes))
165
164
 
166
165
  return APIResponse.success(
167
166
  data=True,
168
- message="Atributo RLS agregado exitosamente",
169
- meta={
170
- "username": username,
171
- "table": rls_attribute.table,
172
- "field": rls_attribute.field,
173
- "values": rls_attribute.values,
174
- }
167
+ message="Atributos RLS agregado exitosamente"
175
168
  )
176
169
 
177
170
  @kc_router.get("/user/{username}/rls", response_model=APIResponse[List[RLSAttribute]])
178
- async def get_current_rls_conditions(
171
+ async def get_rls(
179
172
  username: str,
180
173
  kc: KeycloakDep
181
174
  ):
@@ -186,35 +179,9 @@ async def get_current_rls_conditions(
186
179
 
187
180
  raise_if_exception(result)
188
181
 
189
- current_rls = []
190
-
191
- for rls, values in result.data.attributes.items():
192
- _, table, field = rls.split('.', 2)
193
- current_rls.append(RLSAttribute(table=table, field=field, values=values))
194
-
195
182
  return APIResponse.success(
196
- data=current_rls,
183
+ data=result.data.attributes,
197
184
  message="Atributos RLS obtenidos exitosamente",
198
185
  meta={"username": username}
199
186
  )
200
187
 
201
- @kc_router.delete("/user/{username}/rls", response_model=APIResponse[bool])
202
- async def delete_rls_condition(
203
- username: str,
204
- rls_attribute: RLSAttribute,
205
- kc: KeycloakDep
206
- ):
207
- """Eliminar valores de atributos RLS de un usuario"""
208
-
209
- raise_if_exception(await kc.user.remove_attribute(username, f'rls.{rls_attribute.table}.{rls_attribute.field}', rls_attribute.values))
210
-
211
- return APIResponse.success(
212
- data=True,
213
- message="Atributo RLS eliminado exitosamente",
214
- meta={
215
- "username": username,
216
- "table": rls_attribute.table,
217
- "field": rls_attribute.field,
218
- "values": rls_attribute.values,
219
- }
220
- )
@@ -12,12 +12,6 @@ from typing import List
12
12
  from tai_keycloak.service.dtos import OperationResult
13
13
  from api.resources import APIException, ErrorCode
14
14
 
15
- class RLSAttribute(BaseModel):
16
- table: str
17
- field: str
18
- values: str | List[str]
19
-
20
-
21
15
  class LoginRequest(BaseModel):
22
16
  """Modelo para solicitud de login"""
23
17
  username: str
@@ -151,13 +151,26 @@ async def {{ model.tablename }}_find_many(
151
151
  {% endfor %}
152
152
  {% endif %}
153
153
  """
154
+ {% if with_keycloak %}
155
+ rls = [
156
+ RLS(
157
+ target_model=sqlalchemy_table_mapper.get(table),
158
+ target_column=col_name,
159
+ values=values
160
+ ) for table, conditions in (token.rls or {}).items()
161
+ for col_name, values in conditions.items()
162
+ ]
163
+ {% endif %}
164
+
154
165
  result = await api.{{ model.tablename }}.find_many(
155
166
  limit=limit,
156
167
  offset=offset,
157
168
  order_by=order_by,
158
169
  order=order,
159
170
  {{ macros.asing_parameters(model).rstrip('\n') | indent(8) }}
160
- includes=includes
171
+ includes=includes,
172
+ {% if with_keycloak %} rls=rls or None{% endif %}
173
+
161
174
  )
162
175
 
163
176
  # Obtener el total para metadatos de paginación si es necesario
@@ -338,12 +351,25 @@ async def {{ model.tablename }}_find(
338
351
  raise ValidationException("{{ column.name }} debe ser mayor a 0", "{{ column.name }}")
339
352
  {% endif %}
340
353
  {% endfor %}
354
+
355
+ {% if with_keycloak %}
356
+ rls = [
357
+ RLS(
358
+ target_model=sqlalchemy_table_mapper.get(table),
359
+ target_column=col_name,
360
+ values=values
361
+ ) for table, conditions in (token.rls or {}).items()
362
+ for col_name, values in conditions.items()
363
+ ]
364
+ {% endif %}
341
365
 
342
366
  result = await api.{{ model.tablename }}.find(
343
367
  {% for column in model.columns if column.args.get('primary_key', False) %}
344
368
  {{ column.name }}={{ column.name }},
345
369
  {% endfor %}
346
- includes=includes
370
+ includes=includes,
371
+ {% if with_keycloak %} rls=rls or None{% endif %}
372
+
347
373
  )
348
374
 
349
375
  if result is None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes