tai-api 0.7.2__tar.gz → 0.7.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 (238) hide show
  1. {tai_api-0.7.2 → tai_api-0.7.4}/PKG-INFO +3 -3
  2. {tai_api-0.7.2 → tai_api-0.7.4}/pyproject.toml +3 -3
  3. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/main_file/templates/app/__init__.py.j2 +24 -2
  4. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/main_file/templates/app/docs.py.j2 +1 -1
  5. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/main.py +12 -0
  6. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/post/create_many.py.j2 +9 -24
  7. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/schema/_anidados.py.j2 +125 -21
  8. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/tipos.py +1 -1
  9. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/_shared/__init__.py +10 -0
  10. tai_api-0.7.4/tai_api/rbac/providers/database/auth/_shared/utils/hierarchy.py +616 -0
  11. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/role/dao.py +86 -15
  12. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/role/dtos.py +3 -0
  13. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/static/api.md +2 -1
  14. {tai_api-0.7.2 → tai_api-0.7.4}/LICENSE +0 -0
  15. {tai_api-0.7.2 → tai_api-0.7.4}/README.md +0 -0
  16. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/README.md +0 -0
  17. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/__init__.py +0 -0
  18. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/README.md +0 -0
  19. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/__init__.py +0 -0
  20. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_auth/__init__.py +0 -0
  21. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_auth/main.py +0 -0
  22. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_auth/model.py +0 -0
  23. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_dev/__init__.py +0 -0
  24. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_dev/main.py +0 -0
  25. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_generate/__init__.py +0 -0
  26. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_generate/main.py +0 -0
  27. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_generate/model.py +0 -0
  28. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/README.md +0 -0
  29. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/__init__.py +0 -0
  30. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/config.py +0 -0
  31. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/main.py +0 -0
  32. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/model.py +0 -0
  33. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/resources/__init__.py +0 -0
  34. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/resources/exceptions.py +0 -0
  35. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/resources/handlers.py +0 -0
  36. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/resources/mcp.py +0 -0
  37. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/resources/responses.py +0 -0
  38. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/templates/README.md.j2 +0 -0
  39. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/templates/dockerfile.j2 +0 -0
  40. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/templates/entrypoint.sh.j2 +0 -0
  41. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_init/templates/pyproject.toml.j2 +0 -0
  42. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_install/__init__.py +0 -0
  43. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_install/main.py +0 -0
  44. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_mcp/__init__.py +0 -0
  45. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_mcp/main.py +0 -0
  46. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_rbac/__init__.py +0 -0
  47. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_rbac/funcs.py +0 -0
  48. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_rbac/main.py +0 -0
  49. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_rules/__init__.py +0 -0
  50. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_rules/main.py +0 -0
  51. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/cmd_rules/model.py +0 -0
  52. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/commands/prompts.py +0 -0
  53. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/group.py +0 -0
  54. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/cli/main.py +0 -0
  55. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/config.py +0 -0
  56. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/contract/README.md +0 -0
  57. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/contract/__init__.py +0 -0
  58. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/contract/auth.py +0 -0
  59. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/contract/build.py +0 -0
  60. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/contract/model.py +0 -0
  61. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/errors/README.md +0 -0
  62. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/errors/__init__.py +0 -0
  63. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/errors/render.py +0 -0
  64. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/errors/types.py +0 -0
  65. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/frontend_checks.py +0 -0
  66. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generated_auth.py +0 -0
  67. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/README.md +0 -0
  68. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/__init__.py +0 -0
  69. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/README.md +0 -0
  70. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/__init__.py +0 -0
  71. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/main.py +0 -0
  72. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/rbac.py +0 -0
  73. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/db/dependencies.py.j2 +0 -0
  74. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/db/jwt.py.j2 +0 -0
  75. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/db/login_router.py.j2 +0 -0
  76. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/db/rbac_backend.py.j2 +0 -0
  77. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/kc/dependencies.py.j2 +0 -0
  78. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/kc/login_router.py.j2 +0 -0
  79. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/kc/rbac_backend.py.j2 +0 -0
  80. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/rbac/rbac_template.py.j2 +0 -0
  81. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/rbac/tables.py.j2 +0 -0
  82. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/shared/__init__.py.j2 +0 -0
  83. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/shared/rbac_models.py.j2 +0 -0
  84. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/shared/rbac_router.py.j2 +0 -0
  85. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/shared/sesion.py.j2 +0 -0
  86. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/shared/utils.py.j2 +0 -0
  87. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/auth/templates/shared/websocket.py.j2 +0 -0
  88. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/main_file/README.md +0 -0
  89. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/main_file/__init__.py +0 -0
  90. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/main_file/main.py +0 -0
  91. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/main_file/templates/__dev__.py.j2 +0 -0
  92. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/main_file/templates/__main__.py.j2 +0 -0
  93. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/main_file/templates/app/docs_assets/docs.css.j2 +0 -0
  94. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/main_file/templates/app/docs_assets/triplealpha.svg.j2 +0 -0
  95. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/main_file/templates/app/docs_assets/zoom.js.j2 +0 -0
  96. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/main_file/templates/app/health.py.j2 +0 -0
  97. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/propiedad.py +0 -0
  98. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/README.md +0 -0
  99. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/__init__.py +0 -0
  100. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/domain/__init__.py.j2 +0 -0
  101. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/delete/delete.py.j2 +0 -0
  102. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/delete/delete_many.py.j2 +0 -0
  103. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/delete/docs.py.j2 +0 -0
  104. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/get/count.py.j2 +0 -0
  105. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/get/docs.py.j2 +0 -0
  106. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/get/exists.py.j2 +0 -0
  107. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/get/find.py.j2 +0 -0
  108. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/get/find_many.py.j2 +0 -0
  109. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/get/macros.j2 +0 -0
  110. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/get/max.py.j2 +0 -0
  111. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/get/mean.py.j2 +0 -0
  112. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/get/min.py.j2 +0 -0
  113. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/get/sum.py.j2 +0 -0
  114. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/patch/docs.py.j2 +0 -0
  115. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/patch/update.py.j2 +0 -0
  116. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/patch/update_many.py.j2 +0 -0
  117. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/post/agg.py.j2 +0 -0
  118. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/post/create.py.j2 +0 -0
  119. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/post/docs.py.j2 +0 -0
  120. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/endpoints/shared_macros.j2 +0 -0
  121. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/enums/__init__.py.j2 +0 -0
  122. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/enums/docs.py.j2 +0 -0
  123. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/enums/endpoint.py.j2 +0 -0
  124. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/routers/__init__.py.j2 +0 -0
  125. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/schema/__init__.py.j2 +0 -0
  126. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/schema/__outerinit__.py.j2 +0 -0
  127. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/schema/_cabeceras.py.j2 +0 -0
  128. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/schema/_filtros.py.j2 +0 -0
  129. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/schema/_masivos.py.j2 +0 -0
  130. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/tx/__init__.py.j2 +0 -0
  131. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/tx/docs.py.j2 +0 -0
  132. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/routers/templates/tx/transaction.py.j2 +0 -0
  133. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/README.md +0 -0
  134. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/__init__.py +0 -0
  135. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/main.py +0 -0
  136. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/README.md.j2 +0 -0
  137. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/_interfaces.j2 +0 -0
  138. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/auth/models.ts.j2 +0 -0
  139. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/auth/permissions.ts.j2 +0 -0
  140. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/auth/session.ts.j2 +0 -0
  141. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/core/bytes.ts.j2 +0 -0
  142. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/core/custom.ts.j2 +0 -0
  143. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/core/dates.ts.j2 +0 -0
  144. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/core/errors.ts.j2 +0 -0
  145. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/core/http.ts.j2 +0 -0
  146. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/core/query.ts.j2 +0 -0
  147. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/core/transaction.ts.j2 +0 -0
  148. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/core/types.ts.j2 +0 -0
  149. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/index.ts.j2 +0 -0
  150. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/rbac/index.ts.j2 +0 -0
  151. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/rbac/models.ts.j2 +0 -0
  152. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/rbac/query.ts.j2 +0 -0
  153. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/rbac/resources.ts.j2 +0 -0
  154. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/react/context.ts.j2 +0 -0
  155. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/react/hooks.ts.j2 +0 -0
  156. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/react/index.ts.j2 +0 -0
  157. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/react/render.tsx.j2 +0 -0
  158. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/react/resolve.ts.j2 +0 -0
  159. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/react/routes.tsx.j2 +0 -0
  160. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/react/session.tsx.j2 +0 -0
  161. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/routes/index.ts.j2 +0 -0
  162. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/schema/index.ts.j2 +0 -0
  163. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/schema/models.ts.j2 +0 -0
  164. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/schema/query.ts.j2 +0 -0
  165. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/schema/resources.ts.j2 +0 -0
  166. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/schema/zod.ts.j2 +0 -0
  167. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/svelte/index.ts.j2 +0 -0
  168. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/svelte/routes.ts.j2 +0 -0
  169. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/svelte/session.ts.j2 +0 -0
  170. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/generators/ts_client/templates/vite.ts.j2 +0 -0
  171. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/project.py +0 -0
  172. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/prompts.py +0 -0
  173. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/README.md +0 -0
  174. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/__init__.py +0 -0
  175. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/actions.py +0 -0
  176. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/declaration.py +0 -0
  177. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/drift.py +0 -0
  178. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/funcs.py +0 -0
  179. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/manager.py +0 -0
  180. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/README.md +0 -0
  181. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/__init__.py +0 -0
  182. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/__init__.py +0 -0
  183. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/__init__.py +0 -0
  184. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/_base.py +0 -0
  185. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/_session.py +0 -0
  186. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/_shared/aggregation.py +0 -0
  187. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/_shared/context.py +0 -0
  188. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/_shared/utils/__init__.py +0 -0
  189. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/_shared/utils/funcs.py +0 -0
  190. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/_shared/utils/rls.py +0 -0
  191. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/permission/__init__.py +0 -0
  192. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/permission/dao.py +2 -2
  193. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/permission/dtos.py +0 -0
  194. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/permission/model.py +0 -0
  195. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/realm/__init__.py +0 -0
  196. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/realm/dao.py +2 -2
  197. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/realm/dtos.py +0 -0
  198. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/realm/model.py +0 -0
  199. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/role/__init__.py +0 -0
  200. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/role/model.py +0 -0
  201. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/role_permission/__init__.py +0 -0
  202. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/role_permission/dao.py +2 -2
  203. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/role_permission/dtos.py +0 -0
  204. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/role_permission/model.py +0 -0
  205. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/row_level_security/__init__.py +0 -0
  206. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/row_level_security/dao.py +2 -2
  207. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/row_level_security/dtos.py +0 -0
  208. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/row_level_security/model.py +0 -0
  209. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/user/__init__.py +0 -0
  210. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/user/dao.py +2 -2
  211. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/user/dtos.py +0 -0
  212. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/user/model.py +0 -0
  213. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/user_role/__init__.py +0 -0
  214. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/user_role/dao.py +2 -2
  215. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/user_role/dtos.py +0 -0
  216. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/database/auth/user_role/model.py +0 -0
  217. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/providers/keycloak.py +0 -0
  218. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rbac/safety.py +0 -0
  219. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/resources_revision.py +0 -0
  220. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/README.md +0 -0
  221. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/__init__.py +0 -0
  222. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/catalog.py +0 -0
  223. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/derived.py +0 -0
  224. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/service.py +0 -0
  225. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/staleness.py +0 -0
  226. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/static/autenticacion.md +0 -0
  227. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/static/cliente-ts.md +0 -0
  228. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/static/comandos.md +0 -0
  229. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/static/errores.md +0 -0
  230. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/templates/endpoints.md.j2 +0 -0
  231. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/rules/templates/proyecto.md.j2 +0 -0
  232. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/service_dependencies.py +0 -0
  233. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/workflows/README.md +0 -0
  234. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/workflows/__init__.py +0 -0
  235. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/workflows/auth.py +0 -0
  236. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/workflows/generate.py +0 -0
  237. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/workflows/init.py +0 -0
  238. {tai_api-0.7.2 → tai_api-0.7.4}/tai_api/workflows/rbac.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tai-api
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: FastAPI framework with TAI ecosystem integration
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
@@ -29,8 +29,8 @@ Requires-Dist: python-multipart (>=0.0.9,<1.0) ; extra == "run"
29
29
  Requires-Dist: sqlalchemy[asyncio] (>=2.0.0,<3.0) ; extra == "run"
30
30
  Requires-Dist: tai-alphi (>=2.0.1,<3.0)
31
31
  Requires-Dist: tai-keycloak (>=0.3.3,<0.4) ; extra == "rbac"
32
- Requires-Dist: tai-sql (>=0.8.0,<1.0)
33
- Requires-Dist: tai-sql[diagrams,hashing] (>=0.8.0,<1.0) ; extra == "database"
32
+ Requires-Dist: tai-sql (>=0.8.4,<1.0)
33
+ Requires-Dist: tai-sql[diagrams,hashing] (>=0.8.4,<1.0) ; extra == "database"
34
34
  Requires-Dist: uvicorn[standard] (>=0.30.0,<1.0) ; extra == "run"
35
35
  Project-URL: Homepage, https://www.triplealpha.in/es/
36
36
  Project-URL: Issues, https://github.com/triplealpha-innovation/tai-api/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tai-api"
3
- version = "0.7.2"
3
+ version = "0.7.4"
4
4
  description = "FastAPI framework with TAI ecosystem integration"
5
5
  license = "LicenseRef-Proprietary"
6
6
  license-files = ["LICENSE"]
@@ -27,7 +27,7 @@ classifiers = [
27
27
  # sqlalchemy llegan como dependencias obligatorias de tai-sql. tai-alphi no: llegaba de rebote por
28
28
  # tai-keycloak 0.2, y con la 0.3 el CLI dejaba de arrancar en una instalación limpia.
29
29
  dependencies = [
30
- "tai-sql>=0.8.0,<1.0",
30
+ "tai-sql>=0.8.4,<1.0",
31
31
  "tai-alphi>=2.0.1,<3.0",
32
32
  ]
33
33
 
@@ -54,7 +54,7 @@ rbac = [
54
54
  # que lleva tai-api dentro (rbac/providers/database/auth), con contraseñas hasheadas (extra
55
55
  # `hashing`). pandas no: ese cliente solo lo importa dentro de los métodos de DataFrame.
56
56
  database = [
57
- "tai-sql[diagrams,hashing]>=0.8.0,<1.0",
57
+ "tai-sql[diagrams,hashing]>=0.8.4,<1.0",
58
58
  ]
59
59
 
60
60
  [project.urls]
@@ -39,12 +39,31 @@ from . import docs, health
39
39
  TITLE = "{{ project_name }}"
40
40
 
41
41
 
42
+ class RootPath:
43
+ """
44
+ El prefijo bajo el que se sirve la API (`ROOT_PATH` de `config.py`), en cada petición.
45
+
46
+ Es lo que hacía `FastAPI(root_path=...)`, que FastAPI desaprueba desde la 0.146 para que lo ponga
47
+ el servidor (`--root-path`). Aquí no depende de cómo se arranque —`tai-api dev`, `uvicorn` en el
48
+ contenedor, `__main__.py`—, y `ROOT_PATH` sigue en un solo sitio. De él cuelgan la ruta de la
49
+ cookie de sesión (`/api/auth`) y los enlaces de la documentación.
50
+ """
51
+
52
+ def __init__(self, app, root_path: str):
53
+ self.app = app
54
+ self.root_path = root_path
55
+
56
+ async def __call__(self, scope, receive, send):
57
+ if self.root_path and scope["type"] in ("http", "websocket"):
58
+ scope["root_path"] = self.root_path
59
+ await self.app(scope, receive, send)
60
+
61
+
42
62
  def create_app() -> FastAPI:
43
63
  app = FastAPI(
44
64
  title=TITLE,
45
65
  version=VERSION,
46
66
  lifespan=lifespan,
47
- root_path=ROOT_PATH,
48
67
  # La documentación la sirve `docs.py`, con su propia página y su protección.
49
68
  docs_url=None,
50
69
  redoc_url=None,
@@ -120,6 +139,9 @@ def create_app() -> FastAPI:
120
139
 
121
140
  _openapi_with_groups(app)
122
141
  _extensions(app)
142
+ # El último, para que quede por fuera de todo, también de lo que añada `extensions`: el resto
143
+ # ya ve el `root_path` de la petición
144
+ app.add_middleware(RootPath, root_path=ROOT_PATH)
123
145
  return app
124
146
 
125
147
 
@@ -163,7 +185,7 @@ def _openapi_with_groups(app: FastAPI) -> None:
163
185
  )
164
186
  if not secured:
165
187
  docs.without_auth(schema)
166
- docs.absolute_token_urls(schema, app.root_path)
188
+ docs.absolute_token_urls(schema, ROOT_PATH)
167
189
  schema["x-tagGroups"] = docs.tag_groups(schema, docs.SCHEMAS)
168
190
  schema["tags"] = docs.tag_names(schema, docs.SCHEMAS)
169
191
  built[secured] = schema
@@ -154,7 +154,7 @@ def absolute_token_urls(openapi: dict, root_path: str) -> dict:
154
154
  relativo, y el navegador acaba resolviéndolos contra la página. Desde `/docs/swagger` el login
155
155
  iba a `/docs/auth/docslogin`, o a `/api/docs/auth/docslogin` detrás del proxy: un 404 en los
156
156
  dos casos. Se completan aquí y no en `dependencies.py` porque es aquí donde se conoce el
157
- `root_path` de la app, y `config.py` no se puede importar desde las dependencias.
157
+ `ROOT_PATH` de `config.py`, que no se puede importar desde las dependencias.
158
158
  """
159
159
  raíz = root_path.rstrip("/")
160
160
  for esquema in openapi.get("components", {}).get("securitySchemes", {}).values():
@@ -623,10 +623,22 @@ class RoutersGenerator(BaseGenerator):
623
623
  'columnas': uniones[anidada.relation],
624
624
  'inversa': inversas.get(anidada.relation),
625
625
  }
626
+ # Con qué se puede pedir un lote de filas por cada columna: una lista (`in_`), un rango
627
+ # (`min_`/`max_`) o nada. Es el `filter_fields` del DAO, sin las columnas que el DAO no
628
+ # deja filtrar (cifradas y hasheadas).
629
+ filtros = {}
630
+ for columna in model_info['columns']:
631
+ if columna.get('encrypt') or columna.get('hash'):
632
+ continue
633
+ if 'in' in columna['filter_fields']:
634
+ filtros[columna['name']] = 'in'
635
+ elif 'range' in columna['filter_fields']:
636
+ filtros[columna['name']] = 'range'
626
637
  tablas.append({
627
638
  'name': model_info['name'],
628
639
  'tablename': entity.tablename,
629
640
  'clave': list(recurso.primary_key),
641
+ 'filtros': filtros,
630
642
  'crea': sorted(a.relation for a in crea),
631
643
  'modifica': sorted(a.relation for a in modifica),
632
644
  'relaciones': [relaciones[nombre] for nombre in sorted(relaciones)],
@@ -6,14 +6,15 @@ from fastapi import Body{% if has_auth %}, Depends{% endif %}
6
6
 
7
7
  {% if has_auth %}
8
8
  {{ shared.auth_imports(has_auth, auth_import, database_import) }}
9
- from {{ auth_import }} import build_owner_rules, owner_violation
10
9
  {% if anidadas.get('create_many') %}
11
- from {{ anidados_import }} import despues_de_crear, en_create, exigir_permisos, traducir_anidada
10
+ from {{ anidados_import }} import despues_de_crear_varias, en_create, exigir_permisos, traducir_anidada
11
+ {% else %}
12
+ from {{ anidados_import }} import despues_de_crear_varias
12
13
  {% endif %}
13
14
  {% elif anidadas.get('create_many') %}
14
15
  from {{ anidados_import }} import traducir_anidada
15
16
  {% endif %}
16
- from {{ database_import }} import {{ facade }}, {{ model.name }}Read, {{ model.name }}Create{{ ', RLS' if has_auth else '' }}
17
+ from {{ database_import }} import {{ facade }}, {{ model.name }}Read, {{ model.name }}Create
17
18
  from {{ resources_import }} import APIResponse
18
19
  from .docs import CREATE_MANY_LIMIT, CREATE_MANY_RESPONSES, CREATE_MANY_SUMMARY, CREATE_MANY_DESCRIPTION
19
20
  from ... import {{ model.tablename }}_router
@@ -38,10 +39,6 @@ async def {{ model.tablename }}_create_many(
38
39
  Crea varios {{ model.name }} en una sola transacción: o todos, o ninguno.
39
40
  """
40
41
  {% if has_auth %}
41
- propias = build_owner_rules(
42
- token, "{{ schema_name }}", get_model_by_name, RLS,
43
- model=get_model_by_name("{{ schema_name }}.{{ model.tablename }}"),
44
- )
45
42
  {% if anidadas.get('create_many') %}
46
43
  # Lo que cada registro crea en otras tablas pide el permiso de cada una, antes de escribir
47
44
  # nada: `field` lleva la posición en la lista (`3.etiquetas.0`).
@@ -67,23 +64,11 @@ async def {{ model.tablename }}_create_many(
67
64
  session=session,
68
65
  )
69
66
  {% endif %}
70
- # Como en `create`: cada fila tiene que ser del usuario, y si una no lo es, la
71
- # excepción deshace también las que ya se habían insertado. Va en la misma
72
- # transacción que la escritura, con la sesión que abrió el endpoint.
73
- if propias:
74
- for creado in creados:
75
- if await {{ dao }}.find(
76
- {% for column in model.columns if column.constraints.get('primary_key', False) %}
77
- {{ column.name }}=creado.{{ column.name }},
78
- {% endfor %}
79
- rls=propias,
80
- session=session,
81
- ) is None:
82
- raise owner_violation()
83
- {% if anidadas.get('create_many') %}
84
- for posicion, (registro, creado) in enumerate(zip(registros, creados)):
85
- await despues_de_crear(token, "{{ model.tablename }}", creado, registro, session, f"{posicion}.")
86
- {% endif %}
67
+ # Como en `create`: cada fila tiene que ser del usuario, y lo que se creó anidado con
68
+ # ella también; si una no lo es, la excepción deshace todo el lote. Va en la misma
69
+ # transacción que la escritura, y es una consulta por tabla y nivel para todo el lote,
70
+ # no una por registro (`field` lleva la posición: `3`, `3.etiquetas`).
71
+ await despues_de_crear_varias(token, "{{ model.tablename }}", creados, registros, session)
87
72
 
88
73
  return APIResponse.success(
89
74
  data=creados,
@@ -97,6 +97,15 @@ CLAVES: Dict[str, Tuple[str, ...]] = {
97
97
  {% endfor %}
98
98
  }
99
99
 
100
+ #: Cómo se pide un lote de filas por cada columna: `in` (`in_<columna>`), `range`
101
+ #: (`min_<columna>`/`max_<columna>`); la que no está no se puede pedir por lote. Es el
102
+ #: `filter_fields` del DAO.
103
+ FILTROS: Dict[str, Dict[str, str]] = {
104
+ {% for t in tablas %}
105
+ "{{ t.tablename }}": {{ t.filtros | tojson }},
106
+ {% endfor %}
107
+ }
108
+
100
109
  #: El nombre de cada tabla en los mensajes: el de su clase.
101
110
  NOMBRES: Dict[str, str] = {
102
111
  {% for t in tablas %}
@@ -364,33 +373,126 @@ async def _relacionadas(tabla: str, fila: Any, relación: str, session) -> list:
364
373
  return await DAOS[destino].find_many(**filtro, session=session)
365
374
 
366
375
 
367
- async def _comprobar_creada(token: Any, tabla: str, fila: Any, forma: Dict[str, dict], campo: str, session, cache) -> None:
368
- """Una fila creada y lo que se creó con ella: tienen que ser del usuario (403 `OWNER`)."""
376
+ async def _relacionadas_de(tabla: str, filas: List[Any], relación: str, session) -> List[list]:
377
+ """
378
+ `_relacionadas` de varias filas a la vez, en su orden: una consulta con `in_` por cada columna de
379
+ la unión, repartida después por fila. Si alguna columna no tiene `in_`, una consulta por fila.
380
+ """
381
+ destino, _, columnas, _ = RELACIONES[tabla][relación]
382
+ if len(filas) == 1 or any(FILTROS[destino].get(columna_destino) != "in" for columna_destino, _ in columnas):
383
+ return [await _relacionadas(tabla, fila, relación, session) for fila in filas]
384
+ uniones = [tuple(getattr(fila, columna_origen) for _, columna_origen in columnas) for fila in filas]
385
+ con_valor = [union for union in uniones if None not in union]
386
+ if not con_valor:
387
+ return [[] for _ in filas]
388
+ filtro = {
389
+ f"in_{columna_destino}": list({union[i] for union in con_valor})
390
+ for i, (columna_destino, _) in enumerate(columnas)
391
+ }
392
+ por_union: Dict[tuple, list] = {}
393
+ for hija in await DAOS[destino].find_many(**filtro, session=session):
394
+ por_union.setdefault(tuple(getattr(hija, columna_destino) for columna_destino, _ in columnas), []).append(hija)
395
+ return [por_union.get(union, []) for union in uniones]
396
+
397
+
398
+ def _tupla(tabla: str, fila: Any) -> tuple:
399
+ return tuple(getattr(fila, columna) for columna in CLAVES[tabla])
400
+
401
+
402
+ async def _primera_ajena(tabla: str, filas: List[Any], propias: list, session) -> Optional[int]:
403
+ """
404
+ La posición de la primera de `filas` que la regla no alcanza, o `None` si las alcanza todas.
405
+
406
+ Una consulta para todas, no una por fila: con una clave simple con `in_`, cuántas de esas claves
407
+ alcanza la regla; si no, las filas que alcanza entre las que acotan la lista o el rango de cada
408
+ columna de la clave (puede traer alguna más; lo que cuenta es que estén todas). La que no aparece
409
+ —ajena, o con un valor que vuelve de la base de datos con otra forma, como una fecha con zona— se
410
+ busca sola con la regla, que es la comprobación exacta: el lote solo ahorra las que confirma.
411
+ """
412
+ if not filas:
413
+ return None
414
+ dao, columnas = DAOS[tabla], CLAVES[tabla]
415
+ if len(columnas) == 1 and FILTROS[tabla].get(columnas[0]) == "in":
416
+ valores = list({getattr(fila, columnas[0]) for fila in filas})
417
+ if await dao.count(**{f"in_{columnas[0]}": valores}, rls=propias, session=session) == len(valores):
418
+ return None
419
+ acotado: Dict[str, Any] = {}
420
+ for columna in columnas:
421
+ valores = [getattr(fila, columna) for fila in filas]
422
+ if FILTROS[tabla].get(columna) == "in":
423
+ acotado[f"in_{columna}"] = list(set(valores))
424
+ elif FILTROS[tabla].get(columna) == "range":
425
+ try:
426
+ acotado[f"min_{columna}"], acotado[f"max_{columna}"] = min(valores), max(valores)
427
+ except TypeError: # valores que no se comparan entre sí (fechas con y sin zona)
428
+ pass
429
+ alcanzadas = set()
430
+ if acotado:
431
+ alcanzadas = {_tupla(tabla, fila) for fila in await dao.find_many(**acotado, rls=propias, session=session)}
432
+ for posición, fila in enumerate(filas):
433
+ if _tupla(tabla, fila) in alcanzadas:
434
+ continue
435
+ if await dao.find(**_clave_fila(tabla, fila), rls=propias, session=session) is None:
436
+ return posición
437
+ return None
438
+
439
+
440
+ async def _comprobar_filas(token: Any, tabla: str, filas: List[Tuple[Any, Dict[str, dict], str]], session, cache) -> None:
441
+ """
442
+ Filas de `tabla` recién creadas, cada una con lo que se creó con ella (`forma`) y su `field`: tienen
443
+ que ser del usuario (403 `OWNER`), y lo de debajo también. Una consulta por tabla y nivel para
444
+ todas, no una por fila.
445
+ """
369
446
  propias = _propias(token, tabla, cache)
370
- if propias and await DAOS[tabla].find(**_clave_fila(tabla, fila), rls=propias, session=session) is None:
371
- raise _en(owner_violation(), campo)
372
- for relación, sub in forma.items():
447
+ if propias:
448
+ ajena = await _primera_ajena(tabla, [fila for fila, _, _ in filas], propias, session)
449
+ if ajena is not None:
450
+ raise _en(owner_violation(), filas[ajena][2])
451
+ await _bajar(token, tabla, [(fila, forma, f"{campo}.") for fila, forma, campo in filas], session, cache)
452
+
453
+
454
+ async def _bajar(token: Any, tabla: str, padres: List[Tuple[Any, Dict[str, dict], str]], session, cache) -> None:
455
+ """
456
+ Lo que se creó bajo `padres` —filas de `tabla`, con su `forma` y el prefijo del `field` de lo suyo—,
457
+ relación a relación: las hijas de todos los padres en una consulta, y comprobadas todas juntas.
458
+ """
459
+ for relación in sorted({relación for _, forma, _ in padres for relación in forma}):
373
460
  destino = RELACIONES[tabla][relación][0]
374
- if not (_propias(token, destino, cache) or _alcanza(token, destino, sub, cache)):
461
+ con_ella = [(fila, forma[relación], prefijo) for fila, forma, prefijo in padres if relación in forma]
462
+ if not any(_propias(token, destino, cache) or _alcanza(token, destino, sub, cache) for _, sub, _ in con_ella):
375
463
  continue
376
- for hija in await _relacionadas(tabla, fila, relación, session):
377
- await _comprobar_creada(token, destino, hija, sub, f"{campo}.{relación}", session, cache)
464
+ hijas = await _relacionadas_de(tabla, [fila for fila, _, _ in con_ella], relación, session)
465
+ await _comprobar_filas(token, destino, [
466
+ (hija, sub, f"{prefijo}{relación}")
467
+ for (_, sub, prefijo), suyas in zip(con_ella, hijas)
468
+ for hija in suyas
469
+ ], session, cache)
378
470
 
379
471
 
380
472
  async def despues_de_crear(token: Any, tabla: str, creada: Any, cuerpo: Any, session, prefijo: str = "") -> None:
381
473
  """
382
- Después de un `create` (`POST`, `/bulk`, el `create` de `_tx`), en su transacción: lo que se creó
383
- anidado en `creada` tiene que ser del usuario. La fila de arriba la comprueba el endpoint.
474
+ Después de un `create` (`POST`, el `create` de `_tx`), en su transacción: lo que se creó anidado en
475
+ `creada` tiene que ser del usuario. La fila de arriba la comprueba el endpoint.
384
476
 
385
477
  Solo cuesta consultas si alguna tabla de lo creado tiene reglas de propietario.
386
478
  """
387
- cache: Dict[str, list] = {}
388
- for relación, sub in _forma(tabla, cuerpo).items():
389
- destino = RELACIONES[tabla][relación][0]
390
- if not (_propias(token, destino, cache) or _alcanza(token, destino, sub, cache)):
391
- continue
392
- for hija in await _relacionadas(tabla, creada, relación, session):
393
- await _comprobar_creada(token, destino, hija, sub, f"{prefijo}{relación}", session, cache)
479
+ await _bajar(token, tabla, [(creada, _forma(tabla, cuerpo), prefijo)], session, {})
480
+
481
+
482
+ async def despues_de_crear_varias(token: Any, tabla: str, creadas: List[Any], cuerpos: List[Any], session) -> None:
483
+ """
484
+ Después de `create_many` (`/bulk`), en su transacción: cada fila creada y lo que se creó anidado
485
+ con ella tiene que ser del usuario. El `field` de un 403 lleva la posición en la lista (`3`, o
486
+ `3.etiquetas` si es lo anidado).
487
+
488
+ Todo el lote a la vez: una consulta por tabla y nivel, no una por registro. Era un `find` por
489
+ fila, y con mil registros por petición eran mil viajes a la base de datos. Solo cuesta consultas
490
+ si alguna tabla de lo creado tiene reglas de propietario.
491
+ """
492
+ await _comprobar_filas(token, tabla, [
493
+ (creada, _forma(tabla, cuerpo), str(posición))
494
+ for posición, (creada, cuerpo) in enumerate(zip(creadas, cuerpos))
495
+ ], session, {})
394
496
 
395
497
 
396
498
  @dataclass
@@ -479,8 +581,10 @@ async def despues_de_escribir(token: Any, plan: Plan, session) -> None:
479
581
  fila = await DAOS[grupo.tabla].find(**ahora, session=session)
480
582
  if fila is None:
481
583
  continue
482
- for hija in await _relacionadas(grupo.tabla, fila, grupo.relacion, session):
483
- if _congelada(_clave_fila(destino, hija)) in grupo.antes:
484
- continue
485
- await _comprobar_creada(token, destino, hija, grupo.forma, grupo.campo, session, plan.cache)
584
+ nuevas = [
585
+ (hija, grupo.forma, grupo.campo)
586
+ for hija in await _relacionadas(grupo.tabla, fila, grupo.relacion, session)
587
+ if _congelada(_clave_fila(destino, hija)) not in grupo.antes
588
+ ]
589
+ await _comprobar_filas(token, destino, nuevas, session, plan.cache)
486
590
  {% endif %}
@@ -197,7 +197,7 @@ def row(info: dict, t: Traductor) -> Interfaz:
197
197
  else:
198
198
  tipo = t.tipo(c)
199
199
  campos.append(Campo(c['name'], _nulable(tipo) if c['nullable'] else tipo, doc=t.doc(c), formato=formato(c)))
200
- if info.get('has_self_reference'):
200
+ if info.get('has_hierarchy'):
201
201
  campos += [
202
202
  Campo('tree_level', 'number | null', opcional=True),
203
203
  Campo('descendants', f"{info['name']}Row[] | null", opcional=True),
@@ -43,6 +43,12 @@ from .aggregation import (
43
43
  GROUP_BY_DATETIME_TYPES,
44
44
  )
45
45
 
46
+ from .utils.hierarchy import (
47
+ HierarchyResolver,
48
+ HierarchyConfig,
49
+ TreeMode,
50
+ TreeNode,
51
+ )
46
52
 
47
53
 
48
54
  __all__ = [
@@ -79,4 +85,8 @@ __all__ = [
79
85
  'OPERATION_FUNCTIONS',
80
86
  'GROUP_BY_FORBIDDEN_TYPES',
81
87
  'GROUP_BY_DATETIME_TYPES',
88
+ 'HierarchyResolver',
89
+ 'HierarchyConfig',
90
+ 'TreeMode',
91
+ 'TreeNode',
82
92
  ]