streamwright 0.1.9__tar.gz → 0.1.11__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 (242) hide show
  1. {streamwright-0.1.9/src/streamwright.egg-info → streamwright-0.1.11}/PKG-INFO +1 -1
  2. {streamwright-0.1.9 → streamwright-0.1.11}/pyproject.toml +1 -1
  3. streamwright-0.1.11/src/streamwright/core/cli/commands/connectors.py +173 -0
  4. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/cli/jobs.py +18 -2
  5. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/cli/main.py +20 -2
  6. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/cli/parser.py +13 -7
  7. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/cli.py +27 -18
  8. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/engine.py +7 -2
  9. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/executor.py +7 -2
  10. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/guide.md +73 -2
  11. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/rules.py +22 -4
  12. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/refs.py +9 -1
  13. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/types.py +10 -0
  14. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/components.py +22 -0
  15. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/contract.py +12 -0
  16. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/contract_terms.py +1 -0
  17. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/logs/__init__.py +1 -1
  18. {streamwright-0.1.9 → streamwright-0.1.11/src/streamwright.egg-info}/PKG-INFO +1 -1
  19. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_cli_subcommands.py +17 -0
  20. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_connector_contract.py +9 -1
  21. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_connectors.py +11 -0
  22. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_logging.py +42 -10
  23. streamwright-0.1.9/src/streamwright/core/cli/commands/connectors.py +0 -103
  24. {streamwright-0.1.9 → streamwright-0.1.11}/LICENSE +0 -0
  25. {streamwright-0.1.9 → streamwright-0.1.11}/README.md +0 -0
  26. {streamwright-0.1.9 → streamwright-0.1.11}/setup.cfg +0 -0
  27. {streamwright-0.1.9 → streamwright-0.1.11}/setup.py +0 -0
  28. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/__init__.py +0 -0
  29. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/catalog.py +0 -0
  30. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/cli/__init__.py +0 -0
  31. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/cli/__main__.py +0 -0
  32. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/cli/commands/__init__.py +0 -0
  33. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/cli/commands/jobs.py +0 -0
  34. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/cli/commands/source.py +0 -0
  35. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/cli/log_options.py +0 -0
  36. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/config/__init__.py +0 -0
  37. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/config/inputs.py +0 -0
  38. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/config/loader.py +0 -0
  39. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/config/reader.py +0 -0
  40. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/__init__.py +0 -0
  41. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/bookmarks.py +0 -0
  42. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/builders.py +0 -0
  43. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/errors.py +0 -0
  44. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/execution.py +0 -0
  45. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/factories.py +0 -0
  46. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/partitions.py +0 -0
  47. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/planning.py +0 -0
  48. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/queries.py +0 -0
  49. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/readers.py +0 -0
  50. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/responses.py +0 -0
  51. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/rules.py +0 -0
  52. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/runner.py +0 -0
  53. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/session.py +0 -0
  54. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/sql/__init__.py +0 -0
  55. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/sql/compilation.py +0 -0
  56. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/sql/list_parameters.py +0 -0
  57. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/sql/ordering.py +0 -0
  58. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/sql/plans.py +0 -0
  59. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/sql/sandbox.py +0 -0
  60. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/sql/stream_plan.py +0 -0
  61. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/sql/tables.py +0 -0
  62. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/sql/types.py +0 -0
  63. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/values.py +0 -0
  64. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/engine/writers.py +0 -0
  65. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/graph.py +0 -0
  66. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/__init__.py +0 -0
  67. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/api_calls.py +0 -0
  68. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/capabilities.py +0 -0
  69. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/database_connections.py +0 -0
  70. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/database_rules.py +0 -0
  71. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/databases.py +0 -0
  72. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/file_formats.py +0 -0
  73. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/file_rules.py +0 -0
  74. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/files.py +0 -0
  75. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/http.py +0 -0
  76. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/local.py +0 -0
  77. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/locations.py +0 -0
  78. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/queries.py +0 -0
  79. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/sandbox.py +0 -0
  80. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/io/store_reader.py +0 -0
  81. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/__init__.py +0 -0
  82. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/calls.py +0 -0
  83. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/__init__.py +0 -0
  84. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/connections.py +0 -0
  85. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/dependencies.py +0 -0
  86. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/findings.py +0 -0
  87. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/names.py +0 -0
  88. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/outputs.py +0 -0
  89. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/refs.py +0 -0
  90. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/rules.py +0 -0
  91. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/runtime.py +0 -0
  92. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/secrets.py +0 -0
  93. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/sections.py +0 -0
  94. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/spec.py +0 -0
  95. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/steps.py +0 -0
  96. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/check/structure.py +0 -0
  97. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/__init__.py +0 -0
  98. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/adapter.py +0 -0
  99. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/definition_adapter.py +0 -0
  100. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/loading.py +0 -0
  101. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/plugin.py +0 -0
  102. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/reads.py +0 -0
  103. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/session.py +0 -0
  104. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/steps.py +0 -0
  105. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/writers/__init__.py +0 -0
  106. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/duckdb/writers/tables.py +0 -0
  107. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/engine.py +0 -0
  108. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/fields.py +0 -0
  109. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/ingest/__init__.py +0 -0
  110. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/ingest/column_rules.py +0 -0
  111. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/ingest/formats.py +0 -0
  112. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/ingest/headers.py +0 -0
  113. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/ingest/mapped.py +0 -0
  114. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/ingest/model.py +0 -0
  115. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/ingest/plain.py +0 -0
  116. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/inputs.py +0 -0
  117. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/manifest.py +0 -0
  118. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/naming.py +0 -0
  119. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/plan.py +0 -0
  120. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/policy.py +0 -0
  121. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/runner.py +0 -0
  122. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/schema.py +0 -0
  123. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/state/__init__.py +0 -0
  124. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/state/dialects.py +0 -0
  125. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/state/migrations.py +0 -0
  126. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/state/protocol.py +0 -0
  127. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/state/store.py +0 -0
  128. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/validate.py +0 -0
  129. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/jobs/write_rules.py +0 -0
  130. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/net/__init__.py +0 -0
  131. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/net/auth.py +0 -0
  132. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/net/downloads.py +0 -0
  133. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/net/encoding.py +0 -0
  134. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/net/errors.py +0 -0
  135. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/net/http.py +0 -0
  136. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/net/paginators.py +0 -0
  137. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/net/policies.py +0 -0
  138. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/outputs/__init__.py +0 -0
  139. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/outputs/base.py +0 -0
  140. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/outputs/dlt_columns.py +0 -0
  141. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/outputs/dlt_output.py +0 -0
  142. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/outputs/ducklake.py +0 -0
  143. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/outputs/file_names.py +0 -0
  144. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/outputs/files.py +0 -0
  145. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/outputs/loading.py +0 -0
  146. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/outputs/output.py +0 -0
  147. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/outputs/staging.py +0 -0
  148. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/outputs/warehouse.py +0 -0
  149. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/py.typed +0 -0
  150. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/__init__.py +0 -0
  151. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/connector_types.py +0 -0
  152. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/constants.py +0 -0
  153. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/contract_declarations.py +0 -0
  154. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/contract_guide.py +0 -0
  155. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/contract_validation.py +0 -0
  156. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/definitions.py +0 -0
  157. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/logs/formatting.py +0 -0
  158. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/logs/handlers.py +0 -0
  159. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/logs/lines.py +0 -0
  160. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/logs/metrics.py +0 -0
  161. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/logs/network.py +0 -0
  162. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/logs/record_factory.py +0 -0
  163. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/logs/redaction.py +0 -0
  164. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/logs/setup.py +0 -0
  165. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/logs/summary.py +0 -0
  166. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/plugins.py +0 -0
  167. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/redact.py +0 -0
  168. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/source_connectors.py +0 -0
  169. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/templates.py +0 -0
  170. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/runtime/testing.py +0 -0
  171. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/__init__.py +0 -0
  172. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/constants.py +0 -0
  173. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/contract.py +0 -0
  174. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/engines/__init__.py +0 -0
  175. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/engines/duckdb.py +0 -0
  176. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/engines/source.py +0 -0
  177. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/messaging.py +0 -0
  178. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/network.py +0 -0
  179. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/operations/__init__.py +0 -0
  180. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/operations/databases.py +0 -0
  181. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/operations/files.py +0 -0
  182. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/operations/http.py +0 -0
  183. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/operations/messages.py +0 -0
  184. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/operations/tables.py +0 -0
  185. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/py.typed +0 -0
  186. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/testing/__init__.py +0 -0
  187. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/testing/contract.py +0 -0
  188. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/testing/helpers.py +0 -0
  189. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sdk/testing/storage.py +0 -0
  190. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sql/__init__.py +0 -0
  191. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sql/analysis.py +0 -0
  192. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sql/conditions.py +0 -0
  193. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/sql/quote.py +0 -0
  194. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/__init__.py +0 -0
  195. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/auth.py +0 -0
  196. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/checker.py +0 -0
  197. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/engine.py +0 -0
  198. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/issues.py +0 -0
  199. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/job_manifests.py +0 -0
  200. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/json_schemas.py +0 -0
  201. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/marked_yaml.py +0 -0
  202. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/partitions.py +0 -0
  203. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/requests.py +0 -0
  204. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/schema.py +0 -0
  205. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/__init__.py +0 -0
  206. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/async_jobs.py +0 -0
  207. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/document.py +0 -0
  208. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/partitions.py +0 -0
  209. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/references.py +0 -0
  210. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/request_partitions.py +0 -0
  211. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/requests.py +0 -0
  212. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/spec.py +0 -0
  213. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/stream_context.py +0 -0
  214. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/streams.py +0 -0
  215. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/tables.py +0 -0
  216. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/sections/templates.py +0 -0
  217. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/source.py +0 -0
  218. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/streams.py +0 -0
  219. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright/core/validation/vocabulary.py +0 -0
  220. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright.egg-info/SOURCES.txt +0 -0
  221. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright.egg-info/dependency_links.txt +0 -0
  222. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright.egg-info/entry_points.txt +0 -0
  223. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright.egg-info/requires.txt +0 -0
  224. {streamwright-0.1.9 → streamwright-0.1.11}/src/streamwright.egg-info/top_level.txt +0 -0
  225. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_catalog.py +0 -0
  226. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_conditions.py +0 -0
  227. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_connector_api_family.py +0 -0
  228. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_dlt_output.py +0 -0
  229. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_engine_reach.py +0 -0
  230. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_exact_decimals.py +0 -0
  231. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_graph.py +0 -0
  232. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_http_engine.py +0 -0
  233. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_output_contract.py +0 -0
  234. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_output_formats.py +0 -0
  235. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_plugin_contract_gate.py +0 -0
  236. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_runner.py +0 -0
  237. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_sdk_plugins.py +0 -0
  238. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_sql.py +0 -0
  239. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_sql_quote.py +0 -0
  240. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_transform.py +0 -0
  241. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_validation.py +0 -0
  242. {streamwright-0.1.9 → streamwright-0.1.11}/tests/test_warehouse_output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamwright
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Declarative data connectors — pull from APIs, files and databases with YAML, shape with SQL, write anywhere.
5
5
  Author-email: Karthick Jaganathan <kjaganathan.sde@gmail.com>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "streamwright"
7
- version = "0.1.9"
7
+ version = "0.1.11"
8
8
  description = "Declarative data connectors — pull from APIs, files and databases with YAML, shape with SQL, write anywhere."
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python
2
+ # /*************************************************************************
3
+ # * Copyright 2025 Karthick Jaganathan
4
+ # *
5
+ # * Licensed under the Apache License, Version 2.0 (the "License");
6
+ # * you may not use this file except in compliance with the License.
7
+ # * You may obtain a copy of the License at
8
+ # *
9
+ # * https://www.apache.org/licenses/LICENSE-2.0
10
+ # *
11
+ # * Unless required by applicable law or agreed to in writing, software
12
+ # * distributed under the License is distributed on an "AS IS" BASIS,
13
+ # * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # * See the License for the specific language governing permissions and
15
+ # * limitations under the License.
16
+ # **************************************************************************/
17
+
18
+ """`streamwright connectors`: the installed connectors, and the catalog's (list, search, install)."""
19
+
20
+ import re
21
+ import sys
22
+ from importlib import metadata
23
+
24
+ import duckdb
25
+
26
+ from streamwright.core import catalog
27
+ from streamwright.core.runtime import components, plugins
28
+ from streamwright.core.runtime.logs.lines import NETWORK
29
+
30
+ _REQUIREMENT_NAME = re.compile(r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)")
31
+ # a connector that logs on streamwright.network calls through StreamWright's HTTP client, which is built on requests
32
+ HTTP_CLIENT_LIBRARY = "requests"
33
+
34
+
35
+ def _installed(name):
36
+ try:
37
+ return "%s==%s" % (name, metadata.version(name))
38
+ except metadata.PackageNotFoundError:
39
+ return "%s (not installed)" % name
40
+
41
+
42
+ def libraries(distribution, loggers=()):
43
+ """
44
+ The third-party libraries a connector uses, as "name==version" (or "name (not installed)"): what its distribution
45
+ requires (not its extras', not StreamWright's), and requests when it calls through StreamWright's HTTP client.
46
+ """
47
+ try:
48
+ requires = (metadata.requires(distribution) or []) if distribution else []
49
+ except metadata.PackageNotFoundError:
50
+ requires = []
51
+ names = []
52
+ for requirement in requires:
53
+ spec, _, marker = requirement.partition(";")
54
+ match = _REQUIREMENT_NAME.match(spec)
55
+ if "extra" not in marker and match and not match.group(1).lower().startswith("streamwright"):
56
+ names.append(match.group(1))
57
+ if NETWORK.name in loggers and HTTP_CLIENT_LIBRARY not in names:
58
+ names.append(HTTP_CLIENT_LIBRARY)
59
+ return [_installed(name) for name in names]
60
+
61
+
62
+ class DuckDBExtensions:
63
+ """The installed DuckDB's extensions (one in-memory query, made when a connector names one): name@version."""
64
+
65
+ def __init__(self):
66
+ self._installed = None
67
+
68
+ def _load(self):
69
+ self._installed = {}
70
+ rows = duckdb.connect().execute(
71
+ "SELECT extension_name, aliases, installed, extension_version FROM duckdb_extensions()").fetchall()
72
+ for name, aliases, installed, version in rows:
73
+ for key in (name, *(aliases or ())):
74
+ self._installed[key] = (name, version if installed else None)
75
+ self._installed[None] = ("duckdb", duckdb.__version__)
76
+
77
+ def describe(self, names):
78
+ """("duckdb==VERSION", ["EXTENSION@VERSION" or "EXTENSION (not installed)", ...]); (None, []) for no names."""
79
+ if not names:
80
+ return None, []
81
+ if self._installed is None:
82
+ self._load()
83
+ found = []
84
+ for name in names:
85
+ extension, version = self._installed.get(name, (name, None))
86
+ found.append("%s@%s" % (extension, version) if version else "%s (not installed)" % extension)
87
+ return "duckdb==%s" % self._installed[None][1], found
88
+
89
+
90
+ def _fields(name, definition, entry, extensions):
91
+ """A connector's `pip show`-style fields."""
92
+ used = libraries(entry.get("distribution"), definition.loggers)
93
+ duckdb_library, loaded = extensions.describe(definition.duckdb_extensions)
94
+ return (("Name", name), ("Title", definition.title), ("Family", definition.family),
95
+ ("Summary", definition.summary), ("Package", entry.get("distribution") or "(registered in-process)"),
96
+ ("Version", entry.get("version") or ""), ("Libraries", ", ".join(used + [duckdb_library] * bool(loaded))),
97
+ ("DuckDB-Extensions", ", ".join(loaded)), ("Loggers", ", ".join(definition.loggers)))
98
+
99
+
100
+ def _connector_lines(names=None):
101
+ """
102
+ The installed (and registered) connectors, sorted by family and name, as `pip show` shows packages (`---` between
103
+ them): name, title, family, summary, distribution and version, the third-party libraries and DuckDB extensions it
104
+ uses (installed versions) and its loggers (for --log). `names`: only these. Only specs are imported.
105
+ """
106
+ metadata_of = {entry["name"]: entry for entry in plugins.discover(plugins.CONNECTORS)}
107
+ blocks, extensions = [], DuckDBExtensions()
108
+ for name in components.available():
109
+ if names is not None and name not in names:
110
+ continue
111
+ try:
112
+ definition = components.load(name)
113
+ except components.ComponentLoadError as exc:
114
+ blocks.append(("~", name, (("Name", name), ("Error", str(exc))))) # (after the families)
115
+ continue
116
+ blocks.append((definition.family, name, _fields(name, definition, metadata_of.get(name, {}), extensions)))
117
+ lines = []
118
+ for _, _, fields in sorted(blocks, key=lambda block: block[:2]):
119
+ if lines:
120
+ lines.append("---")
121
+ lines.extend(("%s: %s" % (key, value)).rstrip() for key, value in fields)
122
+ return lines
123
+
124
+
125
+ def run_connectors_command(args):
126
+ """`streamwright connectors`: bare or installed names = installed connectors; ls/i/find use the catalog (hub)."""
127
+ if getattr(args, "flag_list", False):
128
+ action = "ls"
129
+ query = []
130
+ elif getattr(args, "flag_install", None):
131
+ action = "install"
132
+ query = args.flag_install
133
+ elif getattr(args, "flag_search", None):
134
+ action = "find"
135
+ query = args.flag_search
136
+ else:
137
+ action = getattr(args, "action", None)
138
+ query = getattr(args, "query", [])
139
+
140
+ if action is None:
141
+ lines = _connector_lines()
142
+ print("\n".join(lines) if lines else "no connectors installed (use streamwright connectors list to find one)")
143
+ return 0
144
+
145
+ norm_action = action.lower()
146
+ if norm_action in ("ls", "list", "-ls", "--list"):
147
+ lines = catalog.listing(args.hub_url)
148
+ print("\n".join(lines) if lines else "the catalog is empty")
149
+ return 0
150
+
151
+ if norm_action in ("i", "install", "-i", "--install"):
152
+ if not query:
153
+ sys.stderr.write("streamwright: 'connectors -i' needs at least one connector KEY\n")
154
+ return 2
155
+ return catalog.install_many(query, assume_yes=args.yes, hub_url=args.hub_url)
156
+
157
+ words = [action, *query]
158
+ if norm_action in ("find", "search", "s", "-s", "-f", "--find", "--search"):
159
+ words = query
160
+ elif set(words) <= set(components.available()):
161
+ # installed connector names show those connectors, as `pip show NAME` does (`sw connectors google_ads s3`)
162
+ print("\n".join(_connector_lines(set(words))))
163
+ return 0
164
+ return _search(" ".join(words), args.hub_url) # any other words search the catalog (`sw connectors ads`)
165
+
166
+
167
+ def _search(text, hub_url):
168
+ hits = catalog.search(text, hub_url)
169
+ if not hits:
170
+ print("no connectors match %r" % text)
171
+ for key, entry in sorted(hits.items()):
172
+ print(" %-16s %-10s %s" % (key, entry.get("trust", "community"), entry.get("summary", "")))
173
+ return 0
@@ -2,6 +2,7 @@
2
2
 
3
3
  import argparse
4
4
 
5
+ from streamwright.core.cli.log_options import add_log_options
5
6
  from streamwright.core.jobs.inputs import InputError
6
7
  from streamwright.core.jobs.inputs import parse_set as parse_job_set
7
8
 
@@ -19,13 +20,28 @@ def parse_set(text):
19
20
  raise argparse.ArgumentTypeError(str(exc)) from exc
20
21
 
21
22
 
23
+ JOB_OPTION = "job_option"
24
+
25
+
26
+ def resolve_job(parser, args):
27
+ """The job path of JOB or --job JOB (one of them, or both the same); a usage error otherwise."""
28
+ option = getattr(args, JOB_OPTION, None)
29
+ if args.job and option and args.job != option:
30
+ parser.error("pass the job once: JOB or --job JOB")
31
+ args.job = args.job or option
32
+ if not args.job:
33
+ parser.error("a job manifest is required: JOB or --job JOB")
34
+ return args.job
35
+
36
+
22
37
  def add_job_arguments(parser):
23
38
  """Add common job manifest and execution options to an argparse parser."""
24
- parser.add_argument("job", metavar="JOB")
39
+ parser.add_argument("job", nargs="?", metavar="JOB", help="the job manifest (a YAML file or its folder)")
40
+ parser.add_argument("--job", "-j", dest=JOB_OPTION, metavar="JOB", help="the job manifest, as an option")
25
41
  parser.add_argument("--overlay", action="append", default=[], metavar="FILE")
26
42
  parser.add_argument("--config", metavar="FILE")
27
43
  parser.add_argument("--set", action="append", default=[], type=parse_set, metavar="NAME=VALUE")
28
44
  parser.add_argument("--allow-connector", action="append", default=[], metavar="NAME")
29
- parser.add_argument("--log-level", metavar="LEVEL")
30
45
  parser.add_argument("--strict", action="store_true")
46
+ add_log_options(parser)
31
47
  return parser
@@ -25,6 +25,7 @@ import yaml
25
25
  from streamwright.core.cli.commands import jobs
26
26
  from streamwright.core.cli.commands.connectors import run_connectors_command
27
27
  from streamwright.core.cli.commands.source import run
28
+ from streamwright.core.cli.jobs import JOB_OPTION
28
29
  from streamwright.core.cli.log_options import log_options, log_setup
29
30
  from streamwright.core.cli.parser import build_parser, build_validate_parser, command_parsers
30
31
  from streamwright.core.validation import engine
@@ -43,9 +44,14 @@ def _extract_all_paths(args, command="run"):
43
44
  index = 0
44
45
  while index < len(args):
45
46
  item = args[index]
46
- option, equals, _ = item.partition("=")
47
+ option, equals, value = item.partition("=")
47
48
  action = options.get(option)
48
49
  if action is not None:
50
+ if action.dest == JOB_OPTION: # --job JOB: a manifest path too
51
+ if not equals and index + 1 < len(args):
52
+ value = args[index + 1]
53
+ if value:
54
+ paths.append(value)
49
55
  if not equals and action.nargs != 0:
50
56
  index += 1
51
57
  if action.nargs in ("+", "*"):
@@ -118,7 +124,7 @@ def _job_kind(argv):
118
124
  """The manifest kind of a job command's path (None: a source, or not a job command); raises ValueError."""
119
125
  if argv[0] not in ("run", "validate", "plan"):
120
126
  return None
121
- paths = _extract_all_paths(argv[1:], argv[0])
127
+ paths = list(dict.fromkeys(_extract_all_paths(argv[1:], argv[0]))) # JOB and --job JOB may name the same
122
128
  kinds = {detect_kind(path) for path in paths} - {None, "source"}
123
129
  if len(kinds) > 1 or (kinds and len(paths) > 1):
124
130
  raise ValueError("a job command requires one manifest kind and path")
@@ -145,6 +151,18 @@ def main(argv=None):
145
151
  return run_connectors_command(args)
146
152
  if hasattr(args, "command_handler"):
147
153
  return args.command_handler(args)
154
+ return _run_source(args)
155
+
156
+
157
+ def _run_source(args):
158
+ """`streamwright run SOURCE`: a job manifest (JOB or --job JOB) was routed to its engine before."""
159
+ if getattr(args, JOB_OPTION, None):
160
+ sys.stderr.write("streamwright: --job %s is not a job manifest (kind: DuckDBJob, SparkJob); run a source "
161
+ "as: streamwright run SOURCE\n" % getattr(args, JOB_OPTION))
162
+ return 2
163
+ if not args.source:
164
+ sys.stderr.write("streamwright: run needs a SOURCE, or --job JOB\n")
165
+ return 2
148
166
  setup = log_setup(args)
149
167
  if setup is None:
150
168
  return 2
@@ -20,7 +20,7 @@
20
20
  import argparse
21
21
 
22
22
  from streamwright.core.cli.commands import jobs
23
- from streamwright.core.cli.jobs import add_job_arguments
23
+ from streamwright.core.cli.jobs import JOB_OPTION, add_job_arguments
24
24
  from streamwright.core.cli.log_options import add_log_options, key_value
25
25
  from streamwright.core.config.inputs import SECRET_ENV_PREFIX
26
26
  from streamwright.core.runtime import plugins
@@ -43,8 +43,13 @@ def build_parser():
43
43
  run = commands.add_parser("run", help="run a source and write its records",
44
44
  description="Run a source. Records go to stdout as Singer messages unless --output "
45
45
  "names a folder for files or a warehouse; logs go to stderr.")
46
- run.add_argument("source", metavar="SOURCE",
47
- help="a source folder (source.yaml and one file per stream in streams/) or a source file")
46
+ run.add_argument("source", nargs="?", metavar="SOURCE",
47
+ help="a source folder (source.yaml and one file per stream in streams/) or a source file; or a "
48
+ "job manifest (kind: DuckDBJob, SparkJob), which runs on its engine")
49
+ run.add_argument("--job", "-j", dest=JOB_OPTION, metavar="JOB",
50
+ help="a job manifest (kind: DuckDBJob, SparkJob) to run on its engine, e.g. "
51
+ "sw run --job jobs/campaigns.yaml (the same as passing it as the path); the job engine's "
52
+ "options apply: sw duckdb run --help, sw spark run --help")
48
53
  run.add_argument("--config", metavar="FILE",
49
54
  help="YAML/JSON file with `config` (values for spec.config) and `streams` (streams/exports to "
50
55
  "run), e.g. one client's settings for the source")
@@ -85,8 +90,8 @@ def build_parser():
85
90
  add_log_options(run)
86
91
 
87
92
  conn = commands.add_parser("connectors",
88
- help="list, find or install connectors (bare: the installed connectors and their "
89
- "SDK loggers)")
93
+ help="show, list, find or install connectors (bare or NAME ...: the installed "
94
+ "connectors, their libraries, DuckDB extensions and loggers)")
90
95
  conn.add_argument("-ls", "--list", action="store_true", dest="flag_list",
91
96
  help="list catalog and installed connectors")
92
97
  conn.add_argument("-i", "--install", nargs="+", dest="flag_install", metavar="KEY",
@@ -94,9 +99,10 @@ def build_parser():
94
99
  conn.add_argument("-s", "-f", "--search", "--find", nargs="+", dest="flag_search", metavar="TEXT",
95
100
  help="find/search connectors in the catalog by keyword")
96
101
  conn.add_argument("action", nargs="?", metavar="{ls,i,find}",
97
- help="ls (catalog + installed), -i KEY (install), or find TEXT (search catalog)")
102
+ help="ls (catalog + installed), -i KEY (install), find TEXT (search catalog), or installed "
103
+ "connector names (show them, like pip show)")
98
104
  conn.add_argument("query", nargs="*", metavar="KEY|TEXT",
99
- help="connector keys (install: one or more) or search words")
105
+ help="connector keys (install, show: one or more) or search words")
100
106
  conn.add_argument("--yes", action="store_true", help="skip the confirmation prompt when installing")
101
107
  conn.add_argument("--hub-url", metavar="URL",
102
108
  help="the connector hub (default: $STREAMWRIGHT_HUB_URL, else the live StreamWright hub; when "
@@ -22,7 +22,8 @@ The DuckDBJob command line (registered for `kind: DuckDBJob` in the `streamwrigh
22
22
  plan JOB
23
23
  run JOB [--secrets FILE] [--dry-run]
24
24
  state list|retry|abandon JOB [--batch ID] [--secrets FILE]
25
- (every command: --config FILE, --set NAME=VALUE, --overlay FILE, --allow-connector NAME, --strict, --log-level)
25
+ (every command: --config FILE, --set NAME=VALUE, --overlay FILE, --allow-connector NAME, --strict, and the logging
26
+ options of `streamwright run`: --log-level, --log NAME=LEVEL, --log-format, --log-config, --log-max-chars)
26
27
 
27
28
  The job options, exit codes and logging are the shared ones of `streamwright.core.cli.jobs`: 0 success, 1 invalid
28
29
  job or failed run, 2 usage or input error. Validation and planning never need secret values.
@@ -31,14 +32,14 @@ job or failed run, 2 usage or input error. Validation and planning never need se
31
32
  import argparse
32
33
  import sys
33
34
 
34
- from streamwright.core.cli.jobs import FAILURE, INVALID_INPUT, SUCCESS, add_job_arguments
35
+ from streamwright.core.cli.jobs import FAILURE, INVALID_INPUT, SUCCESS, add_job_arguments, resolve_job
36
+ from streamwright.core.cli.log_options import log_setup
35
37
  from streamwright.core.config.inputs import InputError, read_values_file
36
38
  from streamwright.core.jobs.duckdb.engine import JobInvalidError, job_state, redactor, run_job, validate_job
37
39
  from streamwright.core.jobs.inputs import read_config_values
38
40
  from streamwright.core.jobs.manifest import JobError
39
41
  from streamwright.core.jobs.runner import owner_id
40
42
  from streamwright.core.jobs.state import StateError
41
- from streamwright.core.runtime.logs import LogSetup
42
43
 
43
44
  __all__ = ["main"]
44
45
 
@@ -100,8 +101,16 @@ def _cmd_run(args):
100
101
  for name, rows in sorted(result.outputs.items()):
101
102
  if name in result.skipped:
102
103
  sys.stdout.write(" %-28s already committed in an earlier attempt\n" % name)
103
- else:
104
- sys.stdout.write(" %-28s %s rows\n" % (name, "?" if rows is None else rows))
104
+ continue
105
+ locations = [location for location in result.locations.get(name, ()) if location]
106
+ written = "%s rows" % ("?" if rows is None else rows)
107
+ if len(locations) == 1:
108
+ written += " -> %s" % locations[0]
109
+ elif locations:
110
+ written += " -> %d files" % len(locations)
111
+ sys.stdout.write(" %-28s %s\n" % (name, written))
112
+ if len(locations) > 1:
113
+ sys.stdout.writelines(" %s\n" % location for location in locations)
105
114
  if result.exit_code:
106
115
  _err("Run failed: %s" % (result.error or "see the log"))
107
116
  return result.exit_code
@@ -146,25 +155,26 @@ def _inputs(parser, secrets=False):
146
155
 
147
156
 
148
157
  def _build_parser(prog):
149
- parser = argparse.ArgumentParser(prog=prog, description="StreamWright DuckDBJob engine.")
158
+ # no prefix matching: --log must not be taken for --log-level
159
+ parser = argparse.ArgumentParser(prog=prog, description="StreamWright DuckDBJob engine.", allow_abbrev=False)
150
160
  sub = parser.add_subparsers(dest="subcommand")
151
- validate = sub.add_parser("validate", help="check a DuckDBJob manifest (no secrets needed)")
161
+ validate = sub.add_parser("validate", allow_abbrev=False, help="check a DuckDBJob manifest (no secrets needed)")
152
162
  validate.add_argument("--graph", action="store_true", help="print the plan of a valid job")
153
163
  _inputs(validate)
154
- validate.set_defaults(func=_cmd_validate)
155
- plan = sub.add_parser("plan", help="print the execution plan (no secrets needed)")
164
+ validate.set_defaults(func=_cmd_validate, parser=validate)
165
+ plan = sub.add_parser("plan", allow_abbrev=False, help="print the execution plan (no secrets needed)")
156
166
  _inputs(plan)
157
- plan.set_defaults(func=_cmd_plan)
158
- run = sub.add_parser("run", help="run a DuckDBJob (unfinished batches resume first)")
167
+ plan.set_defaults(func=_cmd_plan, parser=plan)
168
+ run = sub.add_parser("run", allow_abbrev=False, help="run a DuckDBJob (unfinished batches resume first)")
159
169
  run.add_argument("--dry-run", action="store_true", help="check and plan, write nothing")
160
170
  _inputs(run, secrets=True)
161
- run.set_defaults(func=_cmd_run)
162
- state = sub.add_parser("state", help="list, retry or abandon a job's batches (runtime.state)")
171
+ run.set_defaults(func=_cmd_run, parser=run)
172
+ state = sub.add_parser("state", allow_abbrev=False, help="list, retry or abandon a job's batches (runtime.state)")
163
173
  state.add_argument("action", choices=["list", "retry", "abandon"])
164
174
  state.add_argument("--batch", help="the batch id (retry, abandon)")
165
175
  state.add_argument("--limit", type=int, default=20, help="batches to list")
166
176
  _inputs(state, secrets=True)
167
- state.set_defaults(func=_cmd_state)
177
+ state.set_defaults(func=_cmd_state, parser=state)
168
178
  return parser
169
179
 
170
180
 
@@ -174,10 +184,9 @@ def main(argv=None, prog="streamwright"):
174
184
  if not getattr(args, "func", None):
175
185
  parser.print_help()
176
186
  return INVALID_INPUT
177
- try:
178
- args.log_setup = LogSetup(args.log_level)
179
- except ValueError as exc:
180
- _err("Error: %s" % exc)
187
+ resolve_job(args.parser, args)
188
+ args.log_setup = log_setup(args)
189
+ if args.log_setup is None:
181
190
  return INVALID_INPUT
182
191
  try:
183
192
  return args.func(args)
@@ -42,6 +42,7 @@ from streamwright.core.jobs.refs import resolve
42
42
  from streamwright.core.jobs.runner import Runner
43
43
  from streamwright.core.jobs.schema import json_schema as generate_schema
44
44
  from streamwright.core.jobs.state import DEFAULT_SCHEMA, open_sqlite, open_store
45
+ from streamwright.core.jobs.types import written_files
45
46
  from streamwright.core.runtime.redact import Redactor
46
47
 
47
48
  __all__ = [
@@ -78,6 +79,8 @@ class RunResult:
78
79
  outputs: dict[str, int | None] = field(default_factory=dict) # output -> rows, from the receipts
79
80
  skipped: set[str] = field(default_factory=set) # outputs already committed in an earlier attempt
80
81
  error: str | None = None
82
+ # output -> where it wrote: its files (a file_name template's, split_by's), else its destination
83
+ locations: dict[str, list[str]] = field(default_factory=dict)
81
84
 
82
85
 
83
86
  def _providers(job):
@@ -236,14 +239,16 @@ def _run(raw, *, config, secret_map, allowed_connectors, dry_run, base_dir, reda
236
239
  engine_metadata={"engine": "duckdb"})
237
240
  session.start_clock()
238
241
  code = runner.run(mode="once", run_key=state.get("run_key"), max_messages=0 if resume_only else 500)
239
- outputs = {}
242
+ outputs, locations = {}, {}
240
243
  skipped = set()
241
244
  for batch_id in runner.processed:
242
245
  for name, receipt in store.get_receipts(job["name"], batch_id).items():
243
246
  outputs[name] = receipt.get("rows")
247
+ locations[name] = written_files(receipt) or [receipt.get("destination")]
244
248
  if receipt.get("already_committed"):
245
249
  skipped.add(name)
246
- return RunResult(code, job["name"], plan.describe(), list(runner.processed), outputs, skipped, runner.error)
250
+ return RunResult(code, job["name"], plan.describe(), list(runner.processed), outputs, skipped, runner.error,
251
+ locations)
247
252
  finally:
248
253
  session.close()
249
254
 
@@ -26,8 +26,9 @@ from streamwright.core.jobs.duckdb.adapter import ReadContext, WriteContext
26
26
  from streamwright.core.jobs.duckdb.steps import build_step
27
27
  from streamwright.core.jobs.policy import SKIPPED, ExecutionPolicy, classify_transient, guard
28
28
  from streamwright.core.jobs.refs import RefError, decode_value, encode_value
29
- from streamwright.core.jobs.types import ExecutionResult, WriteReceipt
29
+ from streamwright.core.jobs.types import ExecutionResult, WriteReceipt, written_files
30
30
  from streamwright.core.jobs.write_rules import OutputSpec, check_output
31
+ from streamwright.core.runtime.logs.lines import fields
31
32
  from streamwright.core.sql.quote import quote_ident
32
33
 
33
34
  __all__ = ["DuckDBExecutor", "ExecutionError"]
@@ -131,7 +132,11 @@ class DuckDBExecutor:
131
132
  if receipt.already_committed:
132
133
  LOG.info("output %s: already committed in an earlier attempt", node.name)
133
134
  else:
134
- LOG.info("output %s: %s rows to %s", node.name, receipt.rows, receipt.destination)
135
+ LOG.info("output %s: %s rows to %s", node.name, receipt.rows, receipt.destination,
136
+ extra=fields(event="output_end", export=node.name, records=receipt.rows))
137
+ for path in written_files(receipt):
138
+ LOG.info("output %s: wrote %s", node.name, path,
139
+ extra=fields(event="output_file", export=node.name, path=path))
135
140
  return ExecutionResult(schema_signatures=tuple(run.schema_signatures),
136
141
  schema_reports=tuple(run.schema_reports))
137
142
 
@@ -25,12 +25,14 @@ secret values. Exit codes: 0 success, 1 invalid job or failed run, 2 usage or in
25
25
  `row_index`, `columns`, `column_groups`, `path_pattern`, `vocabulary`, `on_unmapped_column`; databases:
26
26
  `table` or `query`). `path` may be `{{ step.column }}`. Planned reads pin their file list in the batch.
27
27
  - call: a connection and a connector call (`service`/`method`/`arguments`, or `request`), optional
28
- `partitions: [{from, field, name}]` (crossed) and `sql` over the table `records` (column `record`).
28
+ `partitions: [{from, field, name}]` (crossed) and `sql` over the table `records` (column `record`); see Call
29
+ steps.
29
30
  - `source` + `stream`: a StreamWright source folder (relative to the job's folder) and one of its streams.
30
31
  - `outputs`: `connection`, `step` or `sql`, and a required `mode`. What else is allowed depends on the writer:
31
32
  tables (`append | overwrite | merge`, `table`, `primary_key`), files (`overwrite | error`, `path`, `format`:
32
33
  `csv | tsv | json | jsonl | parquet`; `path` is the file, or with a `file_name` template the folder: `file_name`
33
- (and `layout`, its folders, e.g. `{today:%Y/%m}`) names one file, or with `split_by` one file per split; or
34
+ (and `layout`, its folders, e.g. `{today:%Y/%m}`) names one file, or with `split_by` one file per split (see File
35
+ names); or
34
36
  `partition_by` for Hive folders), Delta
35
37
  (`append | overwrite | merge` with `replace_where` or `whole_table`, `condition`, `when_*`, `target_predicate`,
36
38
  `schema_evolution`). Modes are the same words on every engine. A key the writer does not read is an error;
@@ -39,6 +41,75 @@ secret values. Exit codes: 0 success, 1 invalid job or failed run, 2 usage or in
39
41
  `read_groups`) and `state`: `{connection: NAME, schema: streamwright}` (a `duckdb` or `postgres` connection) or
40
42
  `{sqlite: PATH}`, plus `ttl_seconds` and `run_key` (a batch with that key runs once).
41
43
 
44
+ ## Call steps
45
+ A call step runs a connector call on the driver, once per partition, and loads the records into a temp table in
46
+ batches, as JSON. A `source` + `stream` step loads its records the same way.
47
+
48
+ - **The `records` table.** With `sql`, the step is that query over the table `records`, which has **one column,
49
+ `record` (JSON)**: one row per record, as the connector returned it. Without `sql`, the records' top-level keys
50
+ become the step's columns. `records` exists only inside the step's own `sql`.
51
+ - **Partitions.** `partitions: [{from, field, name}]` calls once per distinct, non-null value of `field` in the
52
+ earlier step `from` (several partitions are crossed); the call's arguments read the value as
53
+ `{{ partition.NAME }}` (`name` defaults to `field`). The values are pinned in the batch, so a replay makes the same
54
+ calls. **Partition values are not columns of `records`**: to keep one, request it in the call (e.g. select
55
+ `campaign.id` in the query) and extract it from `record`.
56
+ - **Extract scalars with `->>`.** `record->>'$.campaign.id'` returns VARCHAR; cast it for other types
57
+ (`(record->>'$.metrics.clicks')::BIGINT`). Dot notation (`record.campaign.id`) and `->` keep the value as JSON,
58
+ so a CSV or TSV output writes it as a quoted JSON string (`"""18497872503"""`), while Parquet hides the issue.
59
+
60
+ ```yaml
61
+ - name: ad_groups
62
+ connection: google_ads_api
63
+ service: GoogleAdsService
64
+ method: search_stream
65
+ partitions: [{from: campaigns, field: campaign_id}]
66
+ arguments:
67
+ customer_id: "{{ config.customer_id }}"
68
+ query: {gaql: {select: [campaign.id, ad_group.id, ad_group.name], from: ad_group,
69
+ where: [{field: campaign.id, op: "=", type: int, value: "{{ partition.campaign_id }}"}]}}
70
+ sql: |
71
+ SELECT record->>'$.campaign.id' AS campaign_id,
72
+ record->>'$.ad_group.id' AS ad_group_id,
73
+ record->>'$.ad_group.name' AS ad_group_name
74
+ FROM records
75
+ ```
76
+
77
+ ## File names
78
+ A file output's `path` is the file; with `file_name` it is the folder, and `file_name` names the file inside it
79
+ (`layout` adds subfolders). With `split_by: [col_a, col_b]` the output writes one file per distinct combination,
80
+ and both templates may use `{col_a}` and `{col_b}`; without `split_by` they name the one file.
81
+
82
+ | Placeholder | Renders |
83
+ |---|---|
84
+ | `{today}`, `{today:%Y-%m-%d}` | the batch's UTC date (default format `%Y%m%d`) |
85
+ | `{now}`, `{batch_ts}`, `{now:%Y%m%d%H%M%S}` | the batch's UTC time (default format `%Y%m%dT%H%M%SZ`) |
86
+ | `{today-1d:%Y%m%d}`, `{now+2h:%H}` | a time with offsets: `+`/`-` N then `s m h d w`, combinable (`-1d+6h`) |
87
+ | `{batch_id}` | the batch id (no offset or format) |
88
+ | `{seq}` | the split's 5-digit number, `00001`, splits ordered by their values (no offset or format) |
89
+ | `{col}`, `{col:%Y%m}` | a `split_by` column's value, reduced to `[A-Za-z0-9._-]`; with a format (or offset), a date, timestamp or ISO text column |
90
+
91
+ - Formats are strftime directives, rendered in UTC and English: `Y y m d e H M S I j u w U G V a A b B p Z z s F %`;
92
+ `%-d` drops zero padding. `%T` is not supported (colons are unsafe in names): use `%H%M%S`.
93
+ - `today`, `now` and `batch_ts` are the batch's time, pinned when the batch starts, so a replay renders the same
94
+ names and overwrites the same files.
95
+ - `file_name` may not contain `/`; `layout` may (`{today:%Y/%m}`, `client={client_id}`). Literal text may use letters,
96
+ digits and `. _ - = , @ +`; `..` is never allowed.
97
+ - `file_name` needs at least one placeholder; for a fixed name, set the file as `path` and leave `file_name` out.
98
+ With `split_by`, two splits that render the same name fail the run: use a split column or `{seq}`.
99
+
100
+ ```yaml
101
+ outputs:
102
+ - name: daily_export
103
+ step: campaign_stats
104
+ connection: exports
105
+ mode: overwrite
106
+ path: reports
107
+ format: tsv
108
+ split_by: [customer_id]
109
+ layout: "{today:%Y/%m}"
110
+ file_name: "campaigns_{customer_id}_{today-1d:%Y%m%d}.tsv"
111
+ ```
112
+
42
113
  ## Delivery
43
114
  Every run takes the job's lease (renewed while it runs; every output write is fenced by it), resumes unfinished
44
115
  batches first and skips outputs that already have a receipt. Delta writes commit an application transaction, so a
@@ -34,6 +34,12 @@ __all__ = ["KIND", "DuckDBRules"]
34
34
 
35
35
  KIND = "DuckDBJob"
36
36
  _TEXT = Field("string")
37
+ FILE_NAME_HELP = (
38
+ "file outputs: the file's name inside `path` (then a folder). Placeholders: {today} and {now}/{batch_ts}, the "
39
+ "batch's UTC date and time, with offsets and strftime formats ({today-1d:%Y%m%d}, {now+2h:%Y%m%d%H%M%S}; "
40
+ "offsets +/-N with s m h d w); {batch_id}; {seq}, the split's 5-digit number; and the split_by columns ({col}). "
41
+ "No '/', and at least one placeholder"
42
+ )
37
43
  # the execution-policy vocabulary (contract v1.2 §6a), accepted structurally on steps and outputs
38
44
  _POLICY_FIELDS = MappingProxyType({
39
45
  "retry": Field("object"), "rate_limit": Field("object"), "paginator": Field("object"),
@@ -51,7 +57,9 @@ _COLUMN_GROUP = Field("object", fields={
51
57
  "keep_nulls": Field("boolean")})
52
58
  _STEP_FIELDS = MappingProxyType({
53
59
  "name": Field("string", required=True, pattern=NAME_PATTERN), "description": _TEXT,
54
- "sql": Field("string", description="DuckDB SQL over earlier steps (in call/source steps: over `records`)"),
60
+ "sql": Field("string", description="DuckDB SQL over earlier steps; in a call or source step, over the table "
61
+ "`records`, whose one column `record` (JSON) holds each record: extract "
62
+ "scalars with ->> (record->>'$.campaign.id'), which returns VARCHAR, not JSON"),
55
63
  "materialize": Field("string", enum=("view", "table")),
56
64
  "connection": _TEXT,
57
65
  "path": Field("string", description="a file, glob or {{ step.column }} (relative to the connection's roots)"),
@@ -71,7 +79,11 @@ _STEP_FIELDS = MappingProxyType({
71
79
  "paginator": Field("object"), "retry": Field("object"), "rate_limit": Field("object"),
72
80
  "page_size": _POLICY_FIELDS["page_size"], "on_error": _TEXT, "timeout": _POLICY_FIELDS["timeout"],
73
81
  "partitions": Field("array", items=_named(**{"from": Field("string", required=True),
74
- "field": Field("string", required=True), "name": _TEXT})),
82
+ "field": Field("string", required=True), "name": _TEXT}),
83
+ description="call steps: one call per distinct value of `field` in the step `from` (crossed "
84
+ "when several), read as {{ partition.NAME }} in the call's arguments (`name` "
85
+ "defaults to `field`). The values are not columns of `records`: request the key "
86
+ "in the call and extract it from `record`"),
75
87
  "source": Field("string", description="a StreamWright source folder (relative to the job's folder)"),
76
88
  "stream": _TEXT, "config": Field("object"),
77
89
  })
@@ -84,8 +96,14 @@ _OUTPUT_FIELDS = MappingProxyType({
84
96
  "when_matched": Field("array", items=Field("object")), "when_not_matched": Field("array", items=Field("object")),
85
97
  "when_not_matched_by_source": Field("array", items=Field("object")),
86
98
  "schema_evolution": Field("string", enum=("strict", "add_columns")),
87
- "partition_by": Field("array", items=_TEXT), "split_by": Field("array", items=_TEXT),
88
- "file_name": _TEXT, "layout": _TEXT, "options": Field("object"),
99
+ "partition_by": Field("array", items=_TEXT, description="file outputs: Hive folders (col=value/) under `path`"),
100
+ "split_by": Field("array", items=_TEXT,
101
+ description="file outputs: one file per distinct combination of these columns; file_name and "
102
+ "layout may use {col} (with a format, {col:%Y%m%d}, for a date or timestamp column)"),
103
+ "file_name": Field("string", description=FILE_NAME_HELP),
104
+ "layout": Field("string", description="the subfolders of file_name's file under `path`; the placeholders of "
105
+ "file_name, and '/', e.g. {today:%Y/%m} or client={client_id}"),
106
+ "options": Field("object"),
89
107
  **_POLICY_FIELDS,
90
108
  })
91
109
  _RUNTIME = _named(