interlaced 2.8.3__tar.gz → 2.8.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 (209) hide show
  1. {interlaced-2.8.3/src/interlaced.egg-info → interlaced-2.8.4}/PKG-INFO +1 -1
  2. {interlaced-2.8.3 → interlaced-2.8.4}/pyproject.toml +3 -2
  3. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/checks/builtin.py +19 -11
  4. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/cli/operate.py +7 -0
  5. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/dsl/decorators.py +28 -3
  6. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/graph/project.py +2 -2
  7. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/scheduler/daemon.py +35 -3
  8. interlaced-2.8.4/src/interlace/scheduler/engine.py +254 -0
  9. interlaced-2.8.4/src/interlace/scheduler/triggers.py +237 -0
  10. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/app.py +5 -1
  11. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/lifespan.py +5 -0
  12. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/routes/catalog.py +10 -3
  13. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/types.py +1 -1
  14. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/views/system.js +1 -1
  15. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/interval.py +21 -4
  16. {interlaced-2.8.3 → interlaced-2.8.4/src/interlaced.egg-info}/PKG-INFO +1 -1
  17. interlaced-2.8.3/src/interlace/scheduler/engine.py +0 -126
  18. interlaced-2.8.3/src/interlace/scheduler/triggers.py +0 -127
  19. {interlaced-2.8.3 → interlaced-2.8.4}/LICENSE +0 -0
  20. {interlaced-2.8.3 → interlaced-2.8.4}/MANIFEST.in +0 -0
  21. {interlaced-2.8.3 → interlaced-2.8.4}/README.md +0 -0
  22. {interlaced-2.8.3 → interlaced-2.8.4}/setup.cfg +0 -0
  23. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/__init__.py +0 -0
  24. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/cdc/__init__.py +0 -0
  25. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/cdc/decode.py +0 -0
  26. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/cdc/publish.py +0 -0
  27. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/cdc/slot.py +0 -0
  28. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/checks/__init__.py +0 -0
  29. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/checks/runner.py +0 -0
  30. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/checks/spec.py +0 -0
  31. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/cli/__init__.py +0 -0
  32. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/cli/build.py +0 -0
  33. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/cli/inspect.py +0 -0
  34. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/cli/main.py +0 -0
  35. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/cli/support.py +0 -0
  36. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/config/__init__.py +0 -0
  37. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/config/config.py +0 -0
  38. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/connections.py +0 -0
  39. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/contracts.py +0 -0
  40. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/dsl/__init__.py +0 -0
  41. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/dsl/discovery.py +0 -0
  42. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/dsl/dynamic.py +0 -0
  43. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/dsl/sql_config.py +0 -0
  44. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/engines/__init__.py +0 -0
  45. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/engines/adbc.py +0 -0
  46. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/engines/base.py +0 -0
  47. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/engines/bigquery.py +0 -0
  48. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/engines/duckdb.py +0 -0
  49. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/engines/postgres.py +0 -0
  50. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/engines/quack.py +0 -0
  51. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/engines/redshift.py +0 -0
  52. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/engines/registry.py +0 -0
  53. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/engines/snowflake.py +0 -0
  54. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/engines/spark.py +0 -0
  55. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/exceptions.py +0 -0
  56. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/graph/__init__.py +0 -0
  57. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/graph/column_lineage.py +0 -0
  58. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/graph/dag.py +0 -0
  59. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/graph/selectors.py +0 -0
  60. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/inputs.py +0 -0
  61. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/inspect.py +0 -0
  62. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/ir/__init__.py +0 -0
  63. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/ir/canonicalize.py +0 -0
  64. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/ir/dialect.py +0 -0
  65. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/ir/fingerprint.py +0 -0
  66. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/ir/layout.py +0 -0
  67. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/ir/macros.py +0 -0
  68. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/ir/relation.py +0 -0
  69. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/ir/vars.py +0 -0
  70. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/mcp_server.py +0 -0
  71. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/physical/__init__.py +0 -0
  72. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/physical/changes.py +0 -0
  73. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/physical/ddl.py +0 -0
  74. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/physical/drift.py +0 -0
  75. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/physical/reconcile.py +0 -0
  76. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/physical/spec.py +0 -0
  77. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/__init__.py +0 -0
  78. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/annotate.py +0 -0
  79. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/apply.py +0 -0
  80. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/backfill.py +0 -0
  81. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/comment.py +0 -0
  82. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/delivery.py +0 -0
  83. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/differ.py +0 -0
  84. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/files.py +0 -0
  85. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/fit.py +0 -0
  86. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/orchestrate.py +0 -0
  87. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/payload.py +0 -0
  88. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/plan.py +0 -0
  89. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/resolve.py +0 -0
  90. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/result.py +0 -0
  91. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/run.py +0 -0
  92. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/schedule.py +0 -0
  93. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/table_diff.py +0 -0
  94. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/plan/transfer.py +0 -0
  95. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/project.py +0 -0
  96. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/py.typed +0 -0
  97. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/query.py +0 -0
  98. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/runtime/__init__.py +0 -0
  99. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/runtime/handles.py +0 -0
  100. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/runtime/python_model.py +0 -0
  101. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/scaffold.py +0 -0
  102. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/scheduler/__init__.py +0 -0
  103. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/scheduler/worker.py +0 -0
  104. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/__init__.py +0 -0
  105. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/auth.py +0 -0
  106. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/present.py +0 -0
  107. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/routes/__init__.py +0 -0
  108. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/routes/admin.py +0 -0
  109. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/routes/environments.py +0 -0
  110. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/routes/models.py +0 -0
  111. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/routes/plan.py +0 -0
  112. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/routes/runs.py +0 -0
  113. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/routes/streams.py +0 -0
  114. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/app.css +0 -0
  115. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/favicon.svg +0 -0
  116. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/index.html +0 -0
  117. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/api.js +0 -0
  118. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/app.js +0 -0
  119. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/dag.js +0 -0
  120. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/timeline.js +0 -0
  121. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/ui.js +0 -0
  122. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/views/checks.js +0 -0
  123. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/views/environments.js +0 -0
  124. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/views/lineage.js +0 -0
  125. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/views/models.js +0 -0
  126. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/views/overview.js +0 -0
  127. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/views/plan.js +0 -0
  128. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/views/query.js +0 -0
  129. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/views/runs.js +0 -0
  130. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/service/ui/js/views/streams.js +0 -0
  131. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/sinks.py +0 -0
  132. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/sources/__init__.py +0 -0
  133. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/sources/auth.py +0 -0
  134. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/sources/rest.py +0 -0
  135. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/__init__.py +0 -0
  136. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/advisory.py +0 -0
  137. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/beat.py +0 -0
  138. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/cdc.py +0 -0
  139. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/checks.py +0 -0
  140. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/codec.py +0 -0
  141. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/conn.py +0 -0
  142. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/events.py +0 -0
  143. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/janitor.py +0 -0
  144. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/keys.py +0 -0
  145. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/locks.py +0 -0
  146. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/queue.py +0 -0
  147. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/schema.py +0 -0
  148. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/snapshot.py +0 -0
  149. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/snapshots.py +0 -0
  150. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/store.py +0 -0
  151. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/state/triggers.py +0 -0
  152. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/strategies/__init__.py +0 -0
  153. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/strategies/append.py +0 -0
  154. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/strategies/base.py +0 -0
  155. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/strategies/full_merge.py +0 -0
  156. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/strategies/hash_merge.py +0 -0
  157. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/strategies/incremental.py +0 -0
  158. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/strategies/merge.py +0 -0
  159. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/strategies/replace.py +0 -0
  160. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/strategies/replace_in_place.py +0 -0
  161. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/strategies/scd.py +0 -0
  162. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/strategies/view.py +0 -0
  163. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/streaming/__init__.py +0 -0
  164. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/streaming/log.py +0 -0
  165. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/streaming/materializer.py +0 -0
  166. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/streaming/schema.py +0 -0
  167. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/cdc/README.md +0 -0
  168. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/cdc/docker-compose.yml +0 -0
  169. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/cdc/init/seed.sql +0 -0
  170. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/cdc/interlace.yaml +0 -0
  171. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/cdc/models/orders.sql +0 -0
  172. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/cdc/models/orders_by_status.sql +0 -0
  173. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/cdc/models/orders_stream.py +0 -0
  174. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/cdc/push.py +0 -0
  175. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/cdc/template.yaml +0 -0
  176. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/events/README.md +0 -0
  177. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/events/generate.py +0 -0
  178. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/events/interlace.yaml +0 -0
  179. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/events/models/events.py +0 -0
  180. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/events/models/events_by_minute.sql +0 -0
  181. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/events/models/events_by_type.sql +0 -0
  182. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/events/models/top_users.sql +0 -0
  183. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/events/models/user_spend.sql +0 -0
  184. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/events/template.yaml +0 -0
  185. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/github/README.md +0 -0
  186. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/github/interlace.yaml +0 -0
  187. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/github/models/github_issues.py +0 -0
  188. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/github/models/issues_by_state.sql +0 -0
  189. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/github/template.yaml +0 -0
  190. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/postgres/README.md +0 -0
  191. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/postgres/docker-compose.yml +0 -0
  192. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/postgres/init/seed.sql +0 -0
  193. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/postgres/interlace.yaml +0 -0
  194. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/postgres/models/orders.py +0 -0
  195. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/postgres/models/orders_by_status.sql +0 -0
  196. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/postgres/template.yaml +0 -0
  197. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/quickstart/README.md +0 -0
  198. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/quickstart/interlace.yaml +0 -0
  199. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/quickstart/models/enriched_events.py +0 -0
  200. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/quickstart/models/event_summary.sql +0 -0
  201. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/quickstart/models/raw_events.sql +0 -0
  202. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/templates/quickstart/template.yaml +0 -0
  203. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/testing/__init__.py +0 -0
  204. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlace/testing/golden.py +0 -0
  205. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlaced.egg-info/SOURCES.txt +0 -0
  206. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlaced.egg-info/dependency_links.txt +0 -0
  207. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlaced.egg-info/entry_points.txt +0 -0
  208. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlaced.egg-info/requires.txt +0 -0
  209. {interlaced-2.8.3 → interlaced-2.8.4}/src/interlaced.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interlaced
3
- Version: 2.8.3
3
+ Version: 2.8.4
4
4
  Summary: Python and SQL models in one DAG. Transformation, orchestration and durable streaming in one process, on DuckDB and Postgres.
5
5
  Author-email: Mark <mark@interlace.sh>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "interlaced"
7
- version = "2.8.3"
7
+ version = "2.8.4"
8
8
  description = "Python and SQL models in one DAG. Transformation, orchestration and durable streaming in one process, on DuckDB and Postgres."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -61,7 +61,8 @@ service = [
61
61
  "msgspec>=0.18,<1.0", # the wire types (litestar serialises msgspec structs natively)
62
62
  ]
63
63
  # Remote engines via Arrow-native ADBC transfer. postgres (+ redshift, same wire)
64
- # are the tested transport; snowflake/bigquery are ALPHA (wired, not run live).
64
+ # are the tested transport. snowflake and bigquery are ALPHA: snowflake has been
65
+ # exercised against one account, outside CI; bigquery has not been run live.
65
66
  adbc = [
66
67
  "adbc-driver-manager>=1.2,<2.0",
67
68
  "adbc-driver-postgresql>=1.2,<2.0",
@@ -14,7 +14,6 @@ NULLs as pattern/range failures, which conflated the two).
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import re
18
17
  from collections.abc import Callable
19
18
  from typing import Any, cast
20
19
 
@@ -24,10 +23,9 @@ from interlace.checks.spec import CheckSpec
24
23
  from interlace.exceptions import DefinitionError
25
24
  from interlace.ir.canonicalize import as_query
26
25
  from interlace.ir.relation import TableRef
26
+ from interlace.state.interval import as_grain
27
27
 
28
28
  _FAILURES = "failures"
29
- _AGE_RE = re.compile(r"^\s*(\d+)\s*([smhdw])\s*$")
30
- _AGE_UNITS = {"s": "SECOND", "m": "MINUTE", "h": "HOUR", "d": "DAY", "w": "WEEK"}
31
29
 
32
30
  ResolveTable = Callable[[str], TableRef]
33
31
  """Maps an upstream model name to its physical table (for ``relationships``)."""
@@ -59,11 +57,24 @@ def _parse_expr(sql: str, dialect: str) -> exp.Expr:
59
57
  return parse_one(sql, read=dialect)
60
58
 
61
59
 
60
+ def sql_interval(amount: int, unit: str) -> exp.Interval:
61
+ """``amount`` of a SQL unit (``HOUR``, ``DAY``, …) as an interval expression."""
62
+ return exp.Interval(this=exp.Literal.string(str(amount)), unit=exp.Var(this=unit))
63
+
64
+
65
+ def max_is_stale(column: exp.Expr, interval: exp.Expr) -> exp.Expr:
66
+ """``max(column)`` is null or older than ``interval``. An empty table counts."""
67
+ newest = exp.Max(this=column)
68
+ threshold = exp.Sub(this=exp.CurrentTimestamp(), expression=interval)
69
+ return exp.or_(exp.LT(this=newest, expression=threshold), newest.copy().is_(exp.null()))
70
+
71
+
62
72
  def _interval(max_age: str) -> exp.Interval:
63
- match = _AGE_RE.fullmatch(str(max_age))
64
- if match is None:
65
- raise DefinitionError(f"invalid max_age {max_age!r}; expected like '2h', '30m', '1d'")
66
- return exp.Interval(this=exp.Literal.string(match.group(1)), unit=exp.Var(this=_AGE_UNITS[match.group(2)]))
73
+ try:
74
+ parsed = as_grain(str(max_age))
75
+ except ValueError as exc:
76
+ raise DefinitionError(f"invalid max_age {max_age!r}; expected like '2h', '30m', '1d'") from exc
77
+ return sql_interval(parsed.amount, parsed.sql_unit)
67
78
 
68
79
 
69
80
  def _row_predicate(spec: CheckSpec, model: str, dialect: str, resolve: ResolveTable) -> exp.Expr | None:
@@ -147,10 +158,7 @@ def build_check_query(spec: CheckSpec, table: TableRef, model: str, dialect: str
147
158
 
148
159
  if spec.type == "freshness":
149
160
  _require(spec, model, columns=1, params=("max_age",))
150
- threshold = exp.Sub(this=exp.CurrentTimestamp(), expression=_interval(str(params["max_age"])))
151
- newest = exp.Max(this=cols[0])
152
- stale = exp.or_(exp.LT(this=newest, expression=threshold), newest.copy().is_(exp.null())) # no rows = stale
153
- return _flag(table, stale)
161
+ return _flag(table, max_is_stale(cols[0], _interval(str(params["max_age"]))))
154
162
 
155
163
  if spec.type == "sql":
156
164
  _require(spec, model, params=("query",))
@@ -285,6 +285,11 @@ def serve(
285
285
  True, "--scheduler/--no-scheduler", help="Run the scheduler loop in this process (combined daemon)."
286
286
  ),
287
287
  interval: float = typer.Option(60.0, "--interval", help="Seconds between scheduler ticks."),
288
+ apply_on_start: bool = typer.Option(
289
+ True,
290
+ "--apply/--no-apply",
291
+ help="Apply the project once at startup. --no-apply serves the warehouse as it is.",
292
+ ),
288
293
  allow_open: bool = typer.Option(
289
294
  False,
290
295
  "--allow-open",
@@ -294,6 +299,7 @@ def serve(
294
299
  """Run the interlace daemon: HTTP API + scheduler in one process (requires the `service` extra).
295
300
 
296
301
  Use --no-scheduler for an API-only process (run `interlace scheduler` separately).
302
+ The project is applied once before the loops start; pass --no-apply to skip that.
297
303
  A `cdc:` block in the project config is read here: each Postgres slot appends into its stream.
298
304
  """
299
305
  try:
@@ -339,6 +345,7 @@ def serve(
339
345
  quack_token=token or None,
340
346
  scheduler=scheduler,
341
347
  scheduler_interval=interval,
348
+ apply_on_start=apply_on_start,
342
349
  )
343
350
  config = uvicorn.Config(
344
351
  app,
@@ -9,7 +9,7 @@ Compilation, planning, and execution happen later over the registry. UK spelling
9
9
  from __future__ import annotations
10
10
 
11
11
  import re
12
- from collections.abc import Callable, Sequence
12
+ from collections.abc import Callable, Mapping, Sequence
13
13
  from contextvars import ContextVar
14
14
  from dataclasses import dataclass, field
15
15
  from typing import Any
@@ -30,6 +30,28 @@ from interlace.strategies import named_strategy
30
30
 
31
31
  ModelFn = Callable[..., Any]
32
32
 
33
+
34
+ def normalize_schedule(schedule: Mapping[str, Any] | None, *, model: str) -> dict[str, str] | None:
35
+ """Schedules are string values. ``fresh: {column, within}`` becomes ``"column within"``."""
36
+ if not schedule:
37
+ return None
38
+ normalised: dict[str, str] = {}
39
+ for key, value in schedule.items():
40
+ if key == "fresh" and isinstance(value, dict):
41
+ column = value.get("column")
42
+ window = value.get("within")
43
+ if not isinstance(column, str) or not column.strip():
44
+ raise DefinitionError(f"model {model!r}: fresh.column must name a timestamp column")
45
+ if not isinstance(window, str) or not window.strip():
46
+ raise DefinitionError(f"model {model!r}: fresh.within must be a window like '2h'")
47
+ normalised[key] = f"{column.strip()} {window.strip()}"
48
+ continue
49
+ if not isinstance(value, str):
50
+ raise DefinitionError(f"model {model!r}: schedule {key!r} must be a string")
51
+ normalised[key] = value
52
+ return normalised
53
+
54
+
33
55
  # Set while a Python model function is running. Registrations then replace a
34
56
  # model this run already created, and are recorded for the apply that builds them.
35
57
  dynamic_owner: ContextVar[str | None] = ContextVar("interlace_dynamic_owner", default=None)
@@ -130,7 +152,9 @@ class ModelDef:
130
152
  path: str | None = None # output path for materialise: file
131
153
  format: str | None = None # csv | parquet | json for materialise: file
132
154
  environments: tuple[str, ...] = ("prod",) # which environments actually deliver a terminal model
133
- schedule: dict[str, str] | None = None # cron, every, watch (glob), or webhook name
155
+ # cron, every, watch, on_change, fresh, or webhook. ``fresh`` may be the
156
+ # mapping ``{column, within}`` at construction; __post_init__ stores "column within".
157
+ schedule: dict[str, Any] | None = None
134
158
  checks: tuple[CheckSpec, ...] = () # data-quality checks; error severity gates promotion
135
159
  indexes: tuple[IndexSpec, ...] = () # physical indexes; not part of the data fingerprint
136
160
  constraints: tuple[ConstraintSpec, ...] = () # physical constraints; engine-enforced, not checks
@@ -151,6 +175,7 @@ class ModelDef:
151
175
  self.constraints = parse_constraints(self.constraints, self.name)
152
176
  self.schema_policy = parse_schema_policy(self.schema_policy, self.name)
153
177
  validate_physical_allowed(self.name, self.materialise, self.indexes, self.constraints, self.schema_policy)
178
+ self.schedule = normalize_schedule(self.schedule, model=self.name)
154
179
 
155
180
  @property
156
181
  def is_terminal(self) -> bool:
@@ -242,7 +267,7 @@ def model(
242
267
  owner: str | None = None,
243
268
  description: str | None = None,
244
269
  columns: dict[str, str | None] | Sequence[str] | None = None,
245
- schedule: dict[str, str] | None = None,
270
+ schedule: dict[str, Any] | None = None,
246
271
  checks: Sequence[dict[str, Any] | CheckSpec] | None = None,
247
272
  indexes: Sequence[dict[str, Any] | IndexSpec] | None = None,
248
273
  constraints: Sequence[dict[str, Any] | ConstraintSpec] | None = None,
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
  import inspect
14
14
  from collections.abc import Iterable, Mapping
15
15
  from dataclasses import dataclass, field
16
- from typing import Any
16
+ from typing import Any, cast
17
17
 
18
18
  from sqlglot import exp
19
19
 
@@ -308,7 +308,7 @@ def compile_models( # noqa: C901
308
308
  cursor=definition.cursor,
309
309
  interval=definition.interval,
310
310
  tags=definition.tags,
311
- schedule=definition.schedule,
311
+ schedule=cast(dict[str, str] | None, definition.schedule), # __post_init__ stores strings
312
312
  columns=definition.columns,
313
313
  target=definition.target,
314
314
  path=definition.path,
@@ -205,6 +205,35 @@ async def flusher_loop(state: Any, *, flush_interval: float) -> None:
205
205
  await asyncio.sleep(1.0)
206
206
 
207
207
 
208
+ async def startup_apply(state: Any) -> None:
209
+ """Apply the project once, before the scheduler starts.
210
+
211
+ A breaking or blocked plan is logged and the process still serves. Any other
212
+ failure propagates: a warehouse that cannot be opened should not pretend to
213
+ be up.
214
+ """
215
+ from interlace.exceptions import PlanError
216
+ from interlace.plan.orchestrate import plan_and_apply
217
+
218
+ try:
219
+ _plan, result = await plan_and_apply(
220
+ state.compiled,
221
+ environment=state.environment,
222
+ project=state.project,
223
+ engines=state.engines,
224
+ state=state.store,
225
+ lock_owner=state.lock_owner,
226
+ connections=state.connections,
227
+ on_compiled=lambda fresh: publish_compiled(state, fresh),
228
+ )
229
+ except PlanError as exc:
230
+ logger.error("startup apply refused: %s", exc.message)
231
+ return
232
+ if result is None:
233
+ return
234
+ logger.info("startup apply built %s", ", ".join(result.built) or "nothing")
235
+
236
+
208
237
  async def scheduler_loop(
209
238
  state: Any,
210
239
  *,
@@ -220,9 +249,12 @@ async def scheduler_loop(
220
249
  try:
221
250
  await reload_if_stale(state)
222
251
  compiled = state.compiled
223
- await TriggerEngine(build_triggers(compiled, root=state.project.root), state.store, compiled).tick(
224
- datetime.now()
225
- )
252
+ await TriggerEngine(
253
+ build_triggers(compiled, root=state.project.root, environment=state.environment),
254
+ state.store,
255
+ compiled,
256
+ engines=state.engines,
257
+ ).tick(datetime.now())
226
258
  if asyncio.get_running_loop().time() >= next_trim:
227
259
  await state.store.trim_logs()
228
260
  next_trim = asyncio.get_running_loop().time() + 6 * 3600
@@ -0,0 +1,254 @@
1
+ """The trigger engine — we own the scheduling loop.
2
+
3
+ Each tick, every trigger is asked what's due given when it last fired; due runs
4
+ are enqueued onto the durable work queue and the trigger's last-fired time is
5
+ persisted. State lives in our state DB (not an external scheduler's jobstore), so
6
+ it survives restarts and is unified with runs and snapshots.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import cast
15
+
16
+ from sqlglot import exp
17
+
18
+ from interlace.engines.base import EngineAdapter, relation_is_absent
19
+ from interlace.engines.registry import EngineRegistry
20
+ from interlace.exceptions import DefinitionError
21
+ from interlace.graph.project import CompiledModel, CompiledProject
22
+ from interlace.ir.canonicalize import table_references
23
+ from interlace.ir.layout import env_view
24
+ from interlace.ir.relation import TableRef
25
+ from interlace.scheduler.triggers import (
26
+ ABSENT,
27
+ CronTrigger,
28
+ FreshTrigger,
29
+ IntervalTrigger,
30
+ OnChangeTrigger,
31
+ Trigger,
32
+ WatchTrigger,
33
+ )
34
+ from interlace.state.interval import Grain, as_grain, parse_grain
35
+ from interlace.state.store import SqliteStateStore
36
+
37
+ _SCHEDULE_KINDS = ("cron", "every", "watch", "on_change", "fresh", "webhook")
38
+ _IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
39
+
40
+
41
+ def schedule_kind(model: str, schedule: dict[str, str]) -> tuple[str, str]:
42
+ """The one schedule key and its value. Cron, interval, file watch, table change, freshness, or webhook."""
43
+ present = [key for key in _SCHEDULE_KINDS if key in schedule]
44
+ if len(present) != 1:
45
+ raise DefinitionError(
46
+ f"model {model!r}: schedule needs exactly one of {', '.join(_SCHEDULE_KINDS)}",
47
+ details={"schedule": schedule},
48
+ )
49
+ return present[0], schedule[present[0]]
50
+
51
+
52
+ def webhook_targets(project: CompiledProject) -> dict[str, str]:
53
+ """Map a webhook name to the model ``POST /hooks/{name}`` enqueues."""
54
+ found: dict[str, str] = {}
55
+ for model in project.models.values():
56
+ if not model.schedule or "webhook" not in model.schedule:
57
+ continue
58
+ kind, name = schedule_kind(model.name, model.schedule)
59
+ if kind != "webhook":
60
+ continue
61
+ other = found.get(name)
62
+ if other is not None:
63
+ raise DefinitionError(f"webhook {name!r} is declared by both {other!r} and {model.name!r}")
64
+ found[name] = model.name
65
+ return found
66
+
67
+
68
+ def build_triggers(project: CompiledProject, *, root: Path | None = None, environment: str = "prod") -> list[Trigger]:
69
+ """Construct triggers from each model's ``schedule`` config.
70
+
71
+ ``root`` is the project directory a ``watch:`` glob is relative to.
72
+ ``environment`` names the view an ``on_change`` or ``fresh`` schedule reads when its
73
+ source is another model. Webhook schedules are validated here and enqueued
74
+ by ``POST /hooks/{name}``, so they do not become ticking triggers.
75
+ """
76
+ triggers: list[Trigger] = []
77
+ for model in project.models.values():
78
+ trigger = _trigger_for(model, project, root, environment)
79
+ if trigger is not None:
80
+ triggers.append(trigger)
81
+ webhook_targets(project) # reject two models sharing one hook name
82
+ return triggers
83
+
84
+
85
+ def _trigger_for(model: CompiledModel, project: CompiledProject, root: Path | None, environment: str) -> Trigger | None:
86
+ schedule = model.schedule
87
+ if not schedule:
88
+ return None
89
+ kind, value = schedule_kind(model.name, schedule)
90
+ if kind == "cron":
91
+ return CronTrigger(model.name, value)
92
+ if kind == "every":
93
+ return IntervalTrigger(model.name, parse_grain(value))
94
+ if kind == "watch":
95
+ if root is None:
96
+ raise DefinitionError(f"model {model.name!r}: a watch schedule needs the project root")
97
+ return WatchTrigger(model.name, value, root)
98
+ if kind == "on_change":
99
+ table, column, engine = resolve_column(model, project, environment, value)
100
+ return OnChangeTrigger(model.name, column, table, engine)
101
+ if kind == "fresh":
102
+ column_spec, window = _fresh_parts(model.name, value)
103
+ table, column, engine = resolve_column(model, project, environment, column_spec, label="fresh")
104
+ return FreshTrigger(model.name, column, table, engine, _fresh_grain(model.name, window))
105
+ return None
106
+
107
+
108
+ def _fresh_parts(model: str, value: str) -> tuple[str, str]:
109
+ column_spec, _, window = value.strip().rpartition(" ")
110
+ if not column_spec.strip() or not window.strip():
111
+ raise DefinitionError(f"model {model!r}: fresh {value!r} must look like 'updated_at 2h'")
112
+ return column_spec.strip(), window.strip()
113
+
114
+
115
+ def _fresh_grain(model: str, window: str) -> Grain:
116
+ try:
117
+ return as_grain(window)
118
+ except ValueError as exc:
119
+ raise DefinitionError(f"model {model!r}: {exc}") from exc
120
+
121
+
122
+ def resolve_column(
123
+ model: CompiledModel, project: CompiledProject, environment: str, value: str, *, label: str = "on_change"
124
+ ) -> tuple[TableRef, str, str]:
125
+ """The table, column, and engine a column schedule probes.
126
+
127
+ A bare column is read from the one table the model reads. ``table.column``
128
+ and ``schema.table.column`` name it. A name that is a model is that model's
129
+ environment view; anything else is a table on the model's own engine.
130
+ """
131
+ parts = value.split(".")
132
+ if not parts or any(not _IDENT.match(part) for part in parts):
133
+ raise DefinitionError(
134
+ f"model {model.name!r}: {label} {value!r} must be a column, table.column, or schema.table.column"
135
+ )
136
+ column = parts[-1]
137
+ if len(parts) == 1:
138
+ table, engine = _infer_change_table(model, project, environment, label=label)
139
+ return table, column, engine
140
+ if len(parts) == 2:
141
+ schema = None
142
+ relation = parts[0]
143
+ elif len(parts) == 3:
144
+ schema, relation = parts[0], parts[1]
145
+ else:
146
+ raise DefinitionError(
147
+ f"model {model.name!r}: {label} {value!r} must be a column, table.column, or schema.table.column"
148
+ )
149
+ qualified = f"{schema}.{relation}" if schema else relation
150
+ if qualified in project.models:
151
+ upstream = project.models[qualified]
152
+ return env_view(environment, qualified), column, upstream.engine
153
+ return TableRef(schema=schema or "main", name=relation), column, model.engine
154
+
155
+
156
+ def _infer_change_table(
157
+ model: CompiledModel, project: CompiledProject, environment: str, *, label: str = "on_change"
158
+ ) -> tuple[TableRef, str]:
159
+ refs = table_references(model.ast) if model.ast is not None else []
160
+ external = [ref for ref in refs if ref not in project.models]
161
+ models = [ref for ref in refs if ref in project.models]
162
+ if model.ast is None:
163
+ external = []
164
+ models = list(model.dependencies)
165
+ if len(external) == 1 and not models:
166
+ ref = external[0]
167
+ schema, _, name = ref.rpartition(".")
168
+ return TableRef(schema=schema or "main", name=name), model.engine
169
+ if not external and len(models) == 1:
170
+ upstream = project.models[models[0]]
171
+ return env_view(environment, models[0]), upstream.engine
172
+ raise DefinitionError(
173
+ f"model {model.name!r}: {label} needs one source table; write table.column or schema.table.column",
174
+ details={"reads": refs or list(model.dependencies)},
175
+ )
176
+
177
+
178
+ def scheduled_closure(project: CompiledProject, models: list[str]) -> list[str]:
179
+ """The triggered models plus every model downstream of them.
180
+
181
+ A cron, interval, file watch, table change, freshness, or webhook means that model's inputs changed.
182
+ Downstream snapshots would otherwise keep the previous build. An explicit
183
+ ``run --select`` is not expanded here: ``model``, ``model+``, and ``+model``
184
+ stay exactly what was asked for.
185
+ """
186
+ chosen: set[str] = set()
187
+ for name in models:
188
+ chosen.add(name)
189
+ if name in project.models:
190
+ chosen |= project.graph.descendants(name)
191
+ return sorted(chosen)
192
+
193
+
194
+ class TriggerEngine:
195
+ """Evaluates triggers on each tick and enqueues due runs."""
196
+
197
+ def __init__(
198
+ self,
199
+ triggers: list[Trigger],
200
+ store: SqliteStateStore,
201
+ project: CompiledProject,
202
+ *,
203
+ engines: EngineRegistry | EngineAdapter | None = None,
204
+ ) -> None:
205
+ self.triggers = triggers
206
+ self.store = store
207
+ self.project = project
208
+ self.engines = engines
209
+
210
+ async def tick(self, now: datetime) -> int:
211
+ """Enqueue all runs due at ``now``; returns how many were newly enqueued."""
212
+ enqueued = 0
213
+ for trigger in self.triggers:
214
+ last_fired = await self.store.get_trigger_last_fired(trigger.id)
215
+ requests = await trigger.due(now, last_fired, self)
216
+ for request in requests:
217
+ selector = scheduled_closure(self.project, request.flow_selector)
218
+ partition = (
219
+ (request.partition.start.isoformat(), request.partition.end.isoformat())
220
+ if request.partition is not None
221
+ else None
222
+ )
223
+ if await self.store.enqueue_run(request.idempotency_key, selector, partition, request.priority):
224
+ enqueued += 1
225
+ await self.store.append_event(
226
+ "run.enqueued", entity=request.idempotency_key, payload={"models": selector}
227
+ )
228
+ if requests:
229
+ await self.store.set_trigger_last_fired(trigger.id, now)
230
+ return enqueued
231
+
232
+ async def scalar(self, engine: str, table: TableRef, query: exp.Expression) -> object | None:
233
+ """First cell of ``query``. ``ABSENT`` when the table is not there yet; ``None`` when the cell is NULL."""
234
+ adapter = self._engine(engine)
235
+ if not await adapter.table_exists(table):
236
+ return ABSENT
237
+ try:
238
+ reader = await adapter.fetch(query)
239
+ except Exception as exc:
240
+ if relation_is_absent(exc):
241
+ return ABSENT
242
+ raise
243
+ rows = reader.read_all()
244
+ if rows.num_rows == 0:
245
+ return ABSENT
246
+ return cast(object | None, rows.column(0)[0].as_py())
247
+
248
+ def _engine(self, name: str) -> EngineAdapter:
249
+ engines = self.engines
250
+ if isinstance(engines, EngineRegistry):
251
+ return engines.get(name)
252
+ if isinstance(engines, EngineAdapter):
253
+ return engines
254
+ raise DefinitionError("a column schedule needs the project's engines")