tai-sql 0.7.18__tar.gz → 0.7.19__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 (229) hide show
  1. {tai_sql-0.7.18 → tai_sql-0.7.19}/PKG-INFO +1 -1
  2. {tai_sql-0.7.18 → tai_sql-0.7.19}/pyproject.toml +1 -1
  3. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_generate/funcs.py +16 -1
  4. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +5 -0
  5. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/triggers/__init__.py +3 -0
  6. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/triggers/analyzer.py +82 -22
  7. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/triggers/renderer.py +67 -10
  8. tai_sql-0.7.19/tai_sql/generators/python_client/triggers/scopes.py +164 -0
  9. tai_sql-0.7.19/tai_sql/generators/python_client/triggers/transformer.py +724 -0
  10. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/trigger_api.py +4 -2
  11. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/triggers.py +32 -9
  12. tai_sql-0.7.18/tai_sql/generators/python_client/triggers/transformer.py +0 -691
  13. {tai_sql-0.7.18 → tai_sql-0.7.19}/LICENSE +0 -0
  14. {tai_sql-0.7.18 → tai_sql-0.7.19}/README.md +0 -0
  15. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/__init__.py +0 -0
  16. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/__init__.py +0 -0
  17. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
  18. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
  19. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
  20. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
  21. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
  22. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
  23. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
  24. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
  25. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
  26. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
  27. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
  28. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
  29. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
  30. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
  31. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_info/main.py +0 -0
  32. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_info/status.py +0 -0
  33. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
  34. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_init/main.py +0 -0
  35. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_init/model.py +0 -0
  36. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
  37. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_install/main.py +0 -0
  38. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
  39. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
  40. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
  41. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_pull/funcs.py +0 -0
  42. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_pull/main.py +0 -0
  43. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
  44. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
  45. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
  46. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_rules/main.py +0 -0
  47. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
  48. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
  49. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
  50. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
  51. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +0 -0
  52. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
  53. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
  54. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
  55. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/prompts.py +0 -0
  56. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/__init__.py +0 -0
  57. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/connectivity.py +0 -0
  58. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/create.py +0 -0
  59. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
  60. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/github/auth.py +0 -0
  61. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/github/client.py +0 -0
  62. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/github/models.py +0 -0
  63. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/errors.py +0 -0
  64. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/main.py +0 -0
  65. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/preamble.py +0 -0
  66. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/services/__init__.py +0 -0
  67. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/services/backup.py +0 -0
  68. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/README.md +0 -0
  69. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/__init__.py +0 -0
  70. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/provider.py +0 -0
  71. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/recipe.py +0 -0
  72. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/session.py +0 -0
  73. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/sources.py +0 -0
  74. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/crypto.py +0 -0
  75. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/__init__.py +0 -0
  76. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/base.py +0 -0
  77. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/bigquery.py +0 -0
  78. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/capabilities.py +0 -0
  79. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/mysql.py +0 -0
  80. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/postgresql.py +0 -0
  81. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/sqlserver.py +0 -0
  82. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/errors/README.md +0 -0
  83. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/errors/__init__.py +0 -0
  84. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/errors/collector.py +0 -0
  85. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/errors/render.py +0 -0
  86. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/errors/types.py +0 -0
  87. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/README.md +0 -0
  88. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/__init__.py +0 -0
  89. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/collect.py +0 -0
  90. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/executor.py +0 -0
  91. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/feeder.py +0 -0
  92. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/plan.py +0 -0
  93. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/presenter.py +0 -0
  94. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/records.py +0 -0
  95. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/transform.py +0 -0
  96. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/__init__.py +0 -0
  97. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/base.py +0 -0
  98. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/README.md +0 -0
  99. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/__init__.py +0 -0
  100. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/assets/README.md +0 -0
  101. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
  102. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/builder.py +0 -0
  103. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/generator.py +0 -0
  104. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/graphviz_render.py +0 -0
  105. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/html_render.py +0 -0
  106. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/layout.py +0 -0
  107. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/model.py +0 -0
  108. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
  109. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/themes.py +0 -0
  110. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/__init__.py +0 -0
  111. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/calculated.py +0 -0
  112. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/context.py +0 -0
  113. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/base.py.jinja2 +0 -0
  114. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
  115. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +0 -0
  116. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +0 -0
  117. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +0 -0
  118. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/facade.py.jinja2 +0 -0
  119. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
  120. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
  121. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
  122. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +0 -0
  123. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/read.jinja2 +0 -0
  124. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
  125. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
  126. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
  127. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
  128. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
  129. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
  130. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
  131. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
  132. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
  133. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
  134. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
  135. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -0
  136. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
  137. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
  138. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/triggers/steps.py +0 -0
  139. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/rules/__init__.py +0 -0
  140. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/rules/templates/schema.md.jinja2 +0 -0
  141. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/README.md +0 -0
  142. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/__init__.py +0 -0
  143. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/blueprint.py +0 -0
  144. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/build.py +0 -0
  145. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/defaults.py +0 -0
  146. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/naming.py +0 -0
  147. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/reader.py +0 -0
  148. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
  149. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/types.py +0 -0
  150. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/writer.py +0 -0
  151. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/README.md +0 -0
  152. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/__init__.py +0 -0
  153. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/README.md +0 -0
  154. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/__init__.py +0 -0
  155. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/calculated.py +0 -0
  156. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/collector.py +0 -0
  157. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/column_analyzer.py +0 -0
  158. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/constraints.py +0 -0
  159. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/hints.py +0 -0
  160. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/relation_analyzer.py +0 -0
  161. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/schema_analyzer.py +0 -0
  162. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/README.md +0 -0
  163. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/__init__.py +0 -0
  164. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/annotations.py +0 -0
  165. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/base.py +0 -0
  166. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/calculated.py +0 -0
  167. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/columns.py +0 -0
  168. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/constraints.py +0 -0
  169. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/datasource.py +0 -0
  170. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/descriptors.py +0 -0
  171. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/encoders.py +0 -0
  172. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/enumerations.py +0 -0
  173. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/relations.py +0 -0
  174. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/table.py +0 -0
  175. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/view.py +0 -0
  176. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/mapping/README.md +0 -0
  177. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/mapping/__init__.py +0 -0
  178. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/mapping/sqlalchemy_mapper.py +0 -0
  179. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/README.md +0 -0
  180. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/__init__.py +0 -0
  181. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/column.py +0 -0
  182. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/constraints.py +0 -0
  183. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/entity.py +0 -0
  184. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/enumeration.py +0 -0
  185. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/relation.py +0 -0
  186. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/schema.py +0 -0
  187. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/trigger.py +0 -0
  188. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/naming.py +0 -0
  189. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/ordering.py +0 -0
  190. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/registry.py +0 -0
  191. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/types.py +0 -0
  192. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/README.md +0 -0
  193. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/__init__.py +0 -0
  194. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/config.py +0 -0
  195. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/context.py +0 -0
  196. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/manager.py +0 -0
  197. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/paths.py +0 -0
  198. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/README.md +0 -0
  199. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/__init__.py +0 -0
  200. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/catalog.py +0 -0
  201. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/installer.py +0 -0
  202. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/meta.py +0 -0
  203. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/service.py +0 -0
  204. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/staleness.py +0 -0
  205. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/static/comandos.md +0 -0
  206. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/static/errores.md +0 -0
  207. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/static/modelo.md +0 -0
  208. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/schema.py +0 -0
  209. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/README.md +0 -0
  210. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/__init__.py +0 -0
  211. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/diff/__init__.py +0 -0
  212. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/diff/constraints.py +0 -0
  213. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/diff/indexes.py +0 -0
  214. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/diff/tables.py +0 -0
  215. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/diff/views.py +0 -0
  216. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/executor.py +0 -0
  217. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/ownership.py +0 -0
  218. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/plan/__init__.py +0 -0
  219. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/plan/planner.py +0 -0
  220. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/plan/statements.py +0 -0
  221. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/presenter.py +0 -0
  222. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/report.py +0 -0
  223. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/safety.py +0 -0
  224. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/state/__init__.py +0 -0
  225. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/state/local.py +0 -0
  226. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/state/model.py +0 -0
  227. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/state/normalize.py +0 -0
  228. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/state/remote.py +0 -0
  229. {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/synchronizer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tai-sql
3
- Version: 0.7.18
3
+ Version: 0.7.19
4
4
  Summary: SQL database management and code generation tool
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tai-sql"
3
- version = "0.7.18"
3
+ version = "0.7.19"
4
4
  description = "SQL database management and code generation tool"
5
5
  license = "LicenseRef-Proprietary"
6
6
  license-files = ["LICENSE"]
@@ -1,7 +1,7 @@
1
1
  import click
2
2
 
3
3
  from tai_sql import pm
4
- from tai_sql.errors import ErrorCode, GenerationError, render_warning
4
+ from tai_sql.errors import ErrorCode, GenerationError, TaiSqlError, render_warning
5
5
  from tai_sql.rules import warn_if_outdated
6
6
 
7
7
  from ...preamble import load_schema
@@ -49,6 +49,21 @@ def run_generate(schema_name: str = None):
49
49
  )
50
50
  generator.generate()
51
51
 
52
+ except TaiSqlError as e:
53
+ # El generador ya sabe qué ha fallado y cómo se arregla —un trigger que no se
54
+ # puede transpilar dice cuál y por qué—: su solución vale más que la genérica.
55
+ # Se conserva el tipo y se añade el schema y el generador delante del sitio
56
+ # que traía (el trigger), en vez de pisarlo.
57
+ raise type(e)(
58
+ message=f"{generator_name} falló al generar: {e.message}",
59
+ solution=e.solution,
60
+ location=' · '.join(filter(None, (
61
+ f"Schema '{schema_name}'", generator_name, e.diagnostic.location,
62
+ ))),
63
+ code=e.diagnostic.code,
64
+ **e.diagnostic.context,
65
+ ) from e
66
+
52
67
  except Exception as e:
53
68
  raise GenerationError(
54
69
  message=f"{generator_name} falló al generar: {e}",
@@ -62,6 +62,11 @@ class ChatbotSchema(SchemaTemplate):
62
62
 
63
63
  visible: col[bool] = column(default=True, index=True, description='Si no, la conversación no se lista')
64
64
  last_message_timestamp: col[datetime | None] = column(description='Timestamp del último mensaje del chat')
65
+ # Por alias y no como FK a modelo.id, a propósito: el alias es con lo que el catálogo y
66
+ # el front piden un modelo, y uno desactivado no tiene por qué romper la conversación
67
+ # (cae al de por defecto). Opcional y sin default: un push la añade a una base de
68
+ # datos existente sin tocar sus filas
69
+ modelo: col[str | None] = column(description='Alias del modelo con el que responde esta conversación; sin él, el de por defecto')
65
70
 
66
71
  messages: onetomany[Mensaje] # Relación one-to-many con la tabla Mensaje
67
72
  token_usage: onetomany[TokenUsage] # El consumo de la conversación, tenga mensaje o no
@@ -12,6 +12,9 @@ ninguna capa de despacho — el coste es cero.
12
12
  (metadato puro) → transformer → AST reescrito
13
13
  → renderer → dict de contexto para Jinja2
14
14
 
15
+ `scopes` lo usan los dos primeros: dice qué `t` es el parámetro y cuál es otra variable
16
+ con el mismo nombre en un ámbito interno.
17
+
15
18
  Este código vivía en `orm/triggers.py` (1.300 de sus 2.092 líneas). Está aquí porque
16
19
  conoce los nombres de los artefactos generados (`{Model}SyncDAO`, `{Model}Create`…),
17
20
  que son conocimiento del generador, no de la definición del schema.
@@ -15,9 +15,10 @@ NO le corresponde: validar que el trigger sea legal (eso ocurre en tiempo de imp
15
15
  schema, en `orm.triggers`), ni conocer la sintaxis del DSL.
16
16
  """
17
17
  from __future__ import annotations
18
- from typing import List, Optional
18
+ from typing import Dict, List, Optional, Set
19
19
  import ast
20
20
 
21
+ from . import scopes
21
22
  from .steps import TriggerStep
22
23
 
23
24
 
@@ -31,14 +32,34 @@ class TriggerAnalyzer(ast.NodeVisitor):
31
32
  - Operaciones de mutación de datos
32
33
  """
33
34
 
34
- def __init__(self, source_lines: List[str]):
35
+ def __init__(self, source_lines: List[str], trigger_var: str = 't'):
35
36
  """
36
37
  Args:
37
38
  source_lines: Líneas del código fuente para obtener el texto original
39
+ trigger_var: Nombre del parámetro TriggerAPI del trigger. Con uno fijo a `'t'`,
40
+ un trigger que lo llamara de otra forma no registraba ninguna operación, y
41
+ el DAO generado no importaba los DAOs de los modelos que toca.
38
42
  """
39
43
  self.steps: List[TriggerStep] = []
40
44
  self.source_lines = source_lines
41
- self.trigger_var = 't' # Nombre del parámetro TriggerAPI (por defecto 't')
45
+ self.trigger_var = trigger_var
46
+ # Llamadas ya registradas por `visit_Assign`, para que `visit_Call` no las repita
47
+ self._registered: Set[int] = set()
48
+ # Qué `Name` son de verdad el parámetro: los calcula la primera llamada a `visit`
49
+ self._refs: Optional[Dict[ast.Name, Optional[str]]] = None
50
+
51
+ def visit(self, node: ast.AST):
52
+ """
53
+ Recorre `node`, resolviendo antes los ámbitos si es la primera vez.
54
+
55
+ `node` es la función del trigger (lo que pasa `analyze_steps`) o un bloque de su
56
+ cuerpo. De la función cuentan el cuerpo y la `lambda` de `when=`, cada uno con su
57
+ parámetro; un `t` re-ligado en un ámbito interno (`[x for t in ...]`) no es una
58
+ operación, aunque se llame `t.count(...)`.
59
+ """
60
+ if self._refs is None:
61
+ self._refs = _trigger_refs(node, self.trigger_var)
62
+ return super().visit(node)
42
63
 
43
64
  def visit_Assign(self, node: ast.Assign) -> None:
44
65
  """Visita asignaciones para detectar mutaciones de datos"""
@@ -67,10 +88,7 @@ class TriggerAnalyzer(ast.NodeVisitor):
67
88
  if self._is_crud_method(node.value.func):
68
89
  crud_operation = node.value.func.attr
69
90
 
70
- # El primer argumento debe ser el modelo
71
- model_name = None
72
- if len(node.value.args) > 0 and isinstance(node.value.args[0], ast.Name):
73
- model_name = node.value.args[0].id
91
+ model_name = _model_name(node.value)
74
92
 
75
93
  # Los demás argumentos
76
94
  args = [self._ast_to_repr(arg) for arg in node.value.args[1:]]
@@ -92,13 +110,21 @@ class TriggerAnalyzer(ast.NodeVisitor):
92
110
  ast_node=node,
93
111
  trigger_refs=trigger_refs
94
112
  ))
113
+ self._registered.add(id(node.value))
95
114
 
96
115
  self.generic_visit(node)
97
116
 
98
- def visit_Expr(self, node: ast.Expr) -> None:
99
- """Visita expresiones para detectar llamadas a métodos del TriggerAPI"""
100
- if isinstance(node.value, ast.Call):
101
- self._visit_call(node.value)
117
+ def visit_Call(self, node: ast.Call) -> None:
118
+ """
119
+ Visita cualquier llamada a un método del TriggerAPI, esté donde esté.
120
+
121
+ Antes solo se miraban las sentencias sueltas y las asignaciones, así que una
122
+ operación en el iterable de un `for`, en un argumento o en una condición no se
123
+ registraba, y el DAO generado no importaba el DAO del modelo que tocaba. Las de
124
+ una asignación las registra `visit_Assign`, con la variable de destino.
125
+ """
126
+ if id(node) not in self._registered:
127
+ self._visit_call(node)
102
128
  self.generic_visit(node)
103
129
 
104
130
  def _visit_call(self, node: ast.Call) -> None:
@@ -137,10 +163,7 @@ class TriggerAnalyzer(ast.NodeVisitor):
137
163
  elif self._is_crud_method(node.func):
138
164
  crud_operation = node.func.attr # find, create, count, etc.
139
165
 
140
- # El primer argumento debe ser el modelo
141
- model_name = None
142
- if len(node.args) > 0 and isinstance(node.args[0], ast.Name):
143
- model_name = node.args[0].id
166
+ model_name = _model_name(node)
144
167
 
145
168
  # Los demás argumentos son parámetros de la operación
146
169
  args = [self._ast_to_repr(arg) for arg in node.args[1:]] # Saltar el modelo
@@ -158,18 +181,24 @@ class TriggerAnalyzer(ast.NodeVisitor):
158
181
  trigger_refs=trigger_refs
159
182
  ))
160
183
 
184
+ def _is_trigger_ref(self, node: ast.AST) -> bool:
185
+ """Si `node` es el parámetro del trigger, y no otra variable con su nombre."""
186
+ if not isinstance(node, ast.Name):
187
+ return False
188
+ if self._refs is None:
189
+ return node.id == self.trigger_var
190
+ return node in self._refs
191
+
161
192
  def _is_trigger_attr(self, node: ast.AST, attr_name: str) -> bool:
162
193
  """Verifica si un nodo es un atributo del trigger (ej: t.data, t.old)"""
163
194
  return (isinstance(node, ast.Attribute) and
164
- isinstance(node.value, ast.Name) and
165
- node.value.id == self.trigger_var and
195
+ self._is_trigger_ref(node.value) and
166
196
  node.attr == attr_name)
167
197
 
168
198
  def _is_trigger_method(self, node: ast.AST, method_name: str) -> bool:
169
199
  """Verifica si un nodo es una llamada a método del trigger (ej: t.log, t.abort)"""
170
200
  return (isinstance(node, ast.Attribute) and
171
- isinstance(node.value, ast.Name) and
172
- node.value.id == self.trigger_var and
201
+ self._is_trigger_ref(node.value) and
173
202
  node.attr == method_name)
174
203
 
175
204
  def _is_crud_method(self, node: ast.AST) -> bool:
@@ -184,8 +213,7 @@ class TriggerAnalyzer(ast.NodeVisitor):
184
213
  }
185
214
 
186
215
  return (isinstance(node, ast.Attribute) and
187
- isinstance(node.value, ast.Name) and
188
- node.value.id == self.trigger_var and
216
+ self._is_trigger_ref(node.value) and
189
217
  node.attr in crud_methods)
190
218
 
191
219
  def _extract_subscript_key(self, node: ast.Subscript) -> Optional[str]:
@@ -216,10 +244,11 @@ class TriggerAnalyzer(ast.NodeVisitor):
216
244
  Lista de referencias encontradas (ej: ['t.new', 't.old'])
217
245
  """
218
246
  references = []
247
+ is_trigger_ref = self._is_trigger_ref
219
248
 
220
249
  class ReferenceExtractor(ast.NodeVisitor):
221
250
  def visit_Attribute(self, node):
222
- if isinstance(node.value, ast.Name) and node.value.id == 't':
251
+ if is_trigger_ref(node.value):
223
252
  if node.attr in ['old', 'new']:
224
253
  references.append(f't.{node.attr}')
225
254
  self.generic_visit(node)
@@ -234,3 +263,34 @@ class TriggerAnalyzer(ast.NodeVisitor):
234
263
  # lineno es 1-indexed
235
264
  return self.source_lines[node.lineno - 1].strip()
236
265
  return None
266
+
267
+
268
+ def _model_name(call: ast.Call) -> Optional[str]:
269
+ """El modelo de `t.<op>(Modelo, ...)`: el primer argumento, o `model=` como en `TriggerAPI`."""
270
+ if call.args:
271
+ return call.args[0].id if isinstance(call.args[0], ast.Name) else None
272
+ for kw in call.keywords:
273
+ if kw.arg == 'model' and isinstance(kw.value, ast.Name):
274
+ return kw.value.id
275
+ return None
276
+
277
+
278
+ def _trigger_refs(node: ast.AST, trigger_var: str) -> Dict[ast.Name, Optional[str]]:
279
+ """
280
+ Las referencias al parámetro dentro de `node`, que es la función del trigger o un
281
+ bloque de su cuerpo.
282
+
283
+ De la función no se mira la firma —ahí `t` es el argumento, no un uso—: su cuerpo
284
+ con el parámetro del trigger, y la `lambda` de `when=` con el suyo, que puede
285
+ llamarse distinto.
286
+ """
287
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
288
+ return scopes.param_refs([node], trigger_var)
289
+ refs = scopes.param_refs(node.body, trigger_var)
290
+ for decorator in node.decorator_list:
291
+ if not isinstance(decorator, ast.Call):
292
+ continue
293
+ for kw in decorator.keywords:
294
+ if kw.arg == 'when' and isinstance(kw.value, ast.Lambda) and kw.value.args.args:
295
+ refs.update(scopes.param_refs([kw.value.body], kw.value.args.args[0].arg))
296
+ return refs
@@ -20,6 +20,9 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
20
20
  import ast
21
21
  import copy
22
22
  import re
23
+ import textwrap
24
+
25
+ from tai_sql.errors import GenerationError
23
26
 
24
27
  from .analyzer import TriggerAnalyzer
25
28
  from .transformer import TriggerCodeTransformer
@@ -57,7 +60,7 @@ def analyze_steps(trigger: Trigger) -> list:
57
60
  if not trigger.ast_tree or not trigger.source_code:
58
61
  return []
59
62
 
60
- analyzer = TriggerAnalyzer(trigger.source_code.split('\n'))
63
+ analyzer = TriggerAnalyzer(trigger.source_code.split('\n'), _trigger_param(trigger))
61
64
  analyzer.visit(trigger.ast_tree)
62
65
  return analyzer.steps
63
66
 
@@ -71,6 +74,54 @@ def _trigger_param(trigger: Trigger) -> str:
71
74
  return 't'
72
75
 
73
76
 
77
+ def _when_param(trigger: Trigger) -> str:
78
+ """
79
+ Nombre del parámetro de la `lambda` de `when=`.
80
+
81
+ No tiene por qué coincidir con el de la función: `when=lambda x: x.new.precio > 0`
82
+ sobre un `def f(self, t)` es válido. Con el de la función, el `x` se quedaba sin
83
+ transpilar y la condición fallaba al ejecutar con `NameError`.
84
+ """
85
+ if trigger.ast_tree:
86
+ for decorator in trigger.ast_tree.decorator_list:
87
+ if not isinstance(decorator, ast.Call):
88
+ continue
89
+ for kw in decorator.keywords:
90
+ if kw.arg == 'when' and isinstance(kw.value, ast.Lambda) and kw.value.args.args:
91
+ return kw.value.args.args[0].arg
92
+ return _trigger_param(trigger)
93
+
94
+
95
+ def _check_compiles(trigger: Trigger, code: str, mode: str, what: str) -> None:
96
+ """
97
+ Compila el código transpilado dentro de una función como la del hook que lo recibe.
98
+
99
+ Es la red de seguridad del transformer: lo que él no rechace y no sea Python válido
100
+ —un `await` donde no cabe, un argumento repetido— sale aquí, con el nombre del
101
+ trigger, y no como un DAO generado que no se puede importar.
102
+
103
+ Raises:
104
+ GenerationError: Si el código no compila.
105
+ """
106
+ header = 'async def _hook():' if mode == 'async' else 'def _hook():'
107
+ try:
108
+ compile(f"{header}\n{textwrap.indent(code, ' ')}\n", f'<trigger {trigger.name}>', 'exec')
109
+ except SyntaxError as e:
110
+ line = (e.text or '').strip()
111
+ raise GenerationError(
112
+ message=(
113
+ f"Trigger '{trigger.name}': su {what} transpilado para el cliente {mode} no "
114
+ f"compila: {e.msg}" + (f" (en `{line}`)" if line else "")
115
+ ),
116
+ solution=(
117
+ "Reescribe esa expresión de forma más sencilla —con una variable intermedia "
118
+ "o una comprensión de lista— y, si el trigger es Python válido, repórtalo: "
119
+ "es un fallo de la transpilación de tai-sql."
120
+ ),
121
+ location=trigger.name,
122
+ ) from e
123
+
124
+
74
125
  def render_body(
75
126
  trigger: Trigger,
76
127
  mode: str = 'sync',
@@ -115,11 +166,13 @@ def render_body(
115
166
  _trigger_param(trigger), trigger.event, mode,
116
167
  operative_str_fields if operative_str_fields is not None else [],
117
168
  tablename,
169
+ trigger_name=trigger.name,
118
170
  )
119
- transformed = transformer.visit(module)
120
- ast.fix_missing_locations(transformed)
171
+ transformed = transformer.transpile(module)
121
172
 
122
- return '\n'.join(ast.unparse(stmt) for stmt in transformed.body)
173
+ code = '\n'.join(ast.unparse(stmt) for stmt in transformed.body)
174
+ _check_compiles(trigger, code, mode, 'cuerpo')
175
+ return code
123
176
 
124
177
 
125
178
  def render_when(
@@ -136,20 +189,24 @@ def render_when(
136
189
  if not trigger.when_source:
137
190
  return None
138
191
 
139
- param = _trigger_param(trigger)
192
+ param = _when_param(trigger)
140
193
  when = trigger.when_source.strip()
141
194
 
142
195
  try:
143
196
  tree = ast.parse(when, mode='eval')
144
- transformer = TriggerCodeTransformer(param, trigger.event, mode, tablename=tablename)
145
- transformed = transformer.visit(tree)
146
- ast.fix_missing_locations(transformed)
147
- return ast.unparse(transformed.body)
148
197
  except SyntaxError:
149
198
  # Fallback: la fuente de la lambda no siempre se recupera como expresión
150
199
  # parseable (troceo de strings en el decorador). Quitar el prefijo `t.` deja
151
200
  # algo utilizable en la mayoría de casos.
152
- return re.sub(rf'\b{re.escape(param)}\.', '', when)
201
+ code = re.sub(rf'\b{re.escape(param)}\.', '', when)
202
+ else:
203
+ transformer = TriggerCodeTransformer(
204
+ param, trigger.event, mode, tablename=tablename, trigger_name=trigger.name,
205
+ )
206
+ code = ast.unparse(transformer.transpile(tree).body)
207
+
208
+ _check_compiles(trigger, f"if {code}:\n pass", mode, 'condición `when=`')
209
+ return code
153
210
 
154
211
 
155
212
  def render_trigger(
@@ -0,0 +1,164 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Ámbitos del cuerpo de un trigger: qué apariciones de un nombre son el parámetro.
4
+
5
+ CAPA: generador (`generators/python_client/triggers/`).
6
+
7
+ El cuerpo de un trigger puede reutilizar el nombre del parámetro (`t`) en un ámbito
8
+ interno —`[t.strip() for t in etiquetas]`, `lambda t: t.activo`, una función anidada
9
+ con un argumento `t`—, y ahí `t` ya no es `TriggerAPI`. Sin distinguirlo, el
10
+ transformer reescribía o rechazaba código que no le corresponde. Este módulo resuelve
11
+ el ámbito una vez y devuelve qué nodos `Name` son de verdad el parámetro.
12
+
13
+ De paso dice, de cada uno, si cae en un sitio donde el código generado no puede
14
+ esperar a una corrutina: dentro de una `lambda`, de una expresión generadora o de una
15
+ función anidada que no es `async`. En el DAO async, una operación ahí lleva un `await`
16
+ que no compila (`lambda`, `def`) o que convierte la expresión generadora en un
17
+ generador asíncrono que nadie sabe recorrer.
18
+
19
+ Las reglas son las de Python: los argumentos por defecto, las anotaciones y los
20
+ decoradores de una función o `lambda` se evalúan fuera; el iterable del **primer**
21
+ `for` de una comprensión, también. Lo demás de una comprensión, el cuerpo de una
22
+ `lambda` y el de una función anidada, dentro.
23
+
24
+ NO le corresponde: reescribir nada (eso es `transformer.py`) ni registrar operaciones
25
+ (eso es `analyzer.py`). Solo responde «¿este `t` es el parámetro?».
26
+ """
27
+ from __future__ import annotations
28
+ from typing import Dict, Iterable, Optional, Set
29
+ import ast
30
+
31
+
32
+ #: Dónde no se puede esperar a una corrutina, con cómo se nombra en los mensajes.
33
+ LAMBDA = 'una `lambda`'
34
+ GENERATOR = 'una expresión generadora'
35
+ FUNCTION = 'una función anidada que no es `async`'
36
+
37
+ _COMPREHENSIONS = (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
38
+ _FUNCTIONS = (ast.FunctionDef, ast.AsyncFunctionDef)
39
+
40
+
41
+ def param_refs(roots: Iterable[ast.AST], name: str) -> Dict[ast.Name, Optional[str]]:
42
+ """
43
+ Las apariciones de `name` que se refieren al parámetro del trigger.
44
+
45
+ Args:
46
+ roots: Nodos que están en el ámbito del propio trigger: las sentencias de su
47
+ cuerpo, un `ast.Module` con ellas o la expresión de su `when=`.
48
+ name: El nombre del parámetro (`t`, o el que use el trigger).
49
+
50
+ Returns:
51
+ Cada `ast.Name` que es el parámetro → dónde cae: `None` si se puede esperar
52
+ ahí, o `LAMBDA`, `GENERATOR` o `FUNCTION` si no. Las claves son los propios
53
+ nodos (un nodo AST se compara por identidad): quien reescribe el árbol puede
54
+ seguir preguntando por los que conserva.
55
+ """
56
+ refs: Dict[ast.Name, Optional[str]] = {}
57
+
58
+ def walk(node: ast.AST, shadowed: bool, deferred: Optional[str]) -> None:
59
+ if isinstance(node, ast.Name):
60
+ if node.id == name and not shadowed:
61
+ refs[node] = deferred
62
+ return
63
+
64
+ if isinstance(node, ast.Lambda):
65
+ _walk_all(_outer_parts(node.args), walk, shadowed, deferred)
66
+ walk(node.body, shadowed or name in _arg_names(node.args), LAMBDA)
67
+ return
68
+
69
+ if isinstance(node, _FUNCTIONS):
70
+ outer = [*node.decorator_list, *_outer_parts(node.args)]
71
+ if node.returns is not None:
72
+ outer.append(node.returns)
73
+ _walk_all(outer, walk, shadowed, deferred)
74
+ inner = shadowed or name in _arg_names(node.args) | local_names(node.body)
75
+ # Dentro de un `async def` se puede esperar, aunque esté en una lambda.
76
+ inner_deferred = None if isinstance(node, ast.AsyncFunctionDef) else FUNCTION
77
+ _walk_all(node.body, walk, inner, inner_deferred)
78
+ return
79
+
80
+ if isinstance(node, _COMPREHENSIONS):
81
+ first, *rest = node.generators
82
+ # El iterable del primer `for` se evalúa en el ámbito de fuera, y ya: en
83
+ # `[x for t in t.find_many(...)]` ese `t` sí es el parámetro.
84
+ walk(first.iter, shadowed, deferred)
85
+ bound = set()
86
+ for generator in node.generators:
87
+ bound |= _stored_names(generator.target)
88
+ inner = shadowed or name in bound
89
+ # Una comprensión de lista, conjunto o dict se evalúa en el sitio, y en un
90
+ # `async def` admite `await`; una expresión generadora, no.
91
+ inner_deferred = GENERATOR if isinstance(node, ast.GeneratorExp) else deferred
92
+ for i, generator in enumerate(node.generators):
93
+ walk(generator.target, inner, inner_deferred)
94
+ if i:
95
+ walk(generator.iter, inner, inner_deferred)
96
+ _walk_all(generator.ifs, walk, inner, inner_deferred)
97
+ parts = [node.key, node.value] if isinstance(node, ast.DictComp) else [node.elt]
98
+ _walk_all(parts, walk, inner, inner_deferred)
99
+ return
100
+
101
+ _walk_all(ast.iter_child_nodes(node), walk, shadowed, deferred)
102
+
103
+ _walk_all(roots, walk, False, None)
104
+ return refs
105
+
106
+
107
+ def local_names(body: Iterable[ast.stmt]) -> Set[str]:
108
+ """
109
+ Los nombres que liga un bloque en su propio ámbito, sin entrar en los internos.
110
+
111
+ Es lo que hace local un nombre en una función: un argumento, una asignación, el
112
+ destino de un `for` o de un `with`, un `import`, un `except ... as`, una `def`.
113
+ """
114
+ names: Set[str] = set()
115
+ declared: Set[str] = set()
116
+ pending = list(body)
117
+ while pending:
118
+ node = pending.pop()
119
+ if isinstance(node, (*_FUNCTIONS, ast.ClassDef)):
120
+ names.add(node.name)
121
+ continue
122
+ if isinstance(node, (ast.Lambda, *_COMPREHENSIONS)):
123
+ continue
124
+ if isinstance(node, (ast.Global, ast.Nonlocal)):
125
+ declared.update(node.names)
126
+ continue
127
+ if isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)):
128
+ names.add(node.id)
129
+ elif isinstance(node, (ast.Import, ast.ImportFrom)):
130
+ names.update((alias.asname or alias.name).split('.')[0] for alias in node.names)
131
+ elif isinstance(node, ast.ExceptHandler) and node.name:
132
+ names.add(node.name)
133
+ pending.extend(ast.iter_child_nodes(node))
134
+ return names - declared
135
+
136
+
137
+ def _walk_all(nodes, walk, shadowed: bool, deferred: Optional[str]) -> None:
138
+ for node in nodes:
139
+ if node is not None:
140
+ walk(node, shadowed, deferred)
141
+
142
+
143
+ def _outer_parts(args: ast.arguments) -> list:
144
+ """Lo de una firma que se evalúa fuera de la función: defaults y anotaciones."""
145
+ parts = [*args.defaults, *args.kw_defaults]
146
+ for arg in (*args.posonlyargs, *args.args, args.vararg, *args.kwonlyargs, args.kwarg):
147
+ if arg is not None and arg.annotation is not None:
148
+ parts.append(arg.annotation)
149
+ return parts
150
+
151
+
152
+ def _arg_names(args: ast.arguments) -> Set[str]:
153
+ return {
154
+ arg.arg
155
+ for arg in (*args.posonlyargs, *args.args, args.vararg, *args.kwonlyargs, args.kwarg)
156
+ if arg is not None
157
+ }
158
+
159
+
160
+ def _stored_names(target: ast.AST) -> Set[str]:
161
+ return {
162
+ node.id for node in ast.walk(target)
163
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store)
164
+ }