codeupipe 0.1.0__tar.gz → 0.10.0__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 (203) hide show
  1. {codeupipe-0.1.0 → codeupipe-0.10.0}/PKG-INFO +90 -6
  2. {codeupipe-0.1.0 → codeupipe-0.10.0}/README.md +89 -5
  3. codeupipe-0.10.0/codeupipe/__init__.py +96 -0
  4. codeupipe-0.10.0/codeupipe/auth/__init__.py +45 -0
  5. codeupipe-0.10.0/codeupipe/auth/_server.py +147 -0
  6. codeupipe-0.10.0/codeupipe/auth/credential.py +210 -0
  7. codeupipe-0.10.0/codeupipe/auth/hook.py +70 -0
  8. codeupipe-0.10.0/codeupipe/auth/provider.py +260 -0
  9. codeupipe-0.10.0/codeupipe/auth/proxy_token.py +190 -0
  10. codeupipe-0.10.0/codeupipe/auth/token_ledger.py +188 -0
  11. codeupipe-0.10.0/codeupipe/auth/token_vault.py +184 -0
  12. codeupipe-0.10.0/codeupipe/auth/vault_hook.py +91 -0
  13. codeupipe-0.10.0/codeupipe/cli/__init__.py +101 -0
  14. codeupipe-0.10.0/codeupipe/cli/__main__.py +5 -0
  15. codeupipe-0.10.0/codeupipe/cli/_bundle.py +80 -0
  16. codeupipe-0.10.0/codeupipe/cli/_registry.py +35 -0
  17. codeupipe-0.10.0/codeupipe/cli/_scaffold.py +299 -0
  18. codeupipe-0.10.0/codeupipe/cli/_templates.py +604 -0
  19. codeupipe-0.10.0/codeupipe/cli/commands/__init__.py +37 -0
  20. codeupipe-0.10.0/codeupipe/cli/commands/analysis_cmds.py +357 -0
  21. codeupipe-0.10.0/codeupipe/cli/commands/auth_cmds.py +189 -0
  22. codeupipe-0.10.0/codeupipe/cli/commands/connect_cmds.py +192 -0
  23. codeupipe-0.10.0/codeupipe/cli/commands/deploy_cmds.py +246 -0
  24. codeupipe-0.10.0/codeupipe/cli/commands/distribute_cmds.py +132 -0
  25. codeupipe-0.10.0/codeupipe/cli/commands/project_cmds.py +243 -0
  26. codeupipe-0.10.0/codeupipe/cli/commands/run_cmds.py +188 -0
  27. codeupipe-0.10.0/codeupipe/cli/commands/scaffold_cmds.py +61 -0
  28. codeupipe-0.10.0/codeupipe/cli/commands/vault_cmds.py +321 -0
  29. codeupipe-0.10.0/codeupipe/connect/__init__.py +22 -0
  30. codeupipe-0.10.0/codeupipe/connect/config.py +105 -0
  31. codeupipe-0.10.0/codeupipe/connect/discovery.py +128 -0
  32. codeupipe-0.10.0/codeupipe/connect/http.py +114 -0
  33. codeupipe-0.10.0/codeupipe/core/__init__.py +31 -0
  34. codeupipe-0.10.0/codeupipe/core/event.py +63 -0
  35. codeupipe-0.10.0/codeupipe/core/govern.py +244 -0
  36. codeupipe-0.10.0/codeupipe/core/payload.py +157 -0
  37. codeupipe-0.10.0/codeupipe/core/pipeline.py +764 -0
  38. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/state.py +40 -2
  39. codeupipe-0.10.0/codeupipe/deploy/__init__.py +62 -0
  40. codeupipe-0.10.0/codeupipe/deploy/adapter.py +47 -0
  41. codeupipe-0.10.0/codeupipe/deploy/apprunner.py +238 -0
  42. codeupipe-0.10.0/codeupipe/deploy/azure_container_apps.py +237 -0
  43. codeupipe-0.10.0/codeupipe/deploy/cloudrun.py +244 -0
  44. codeupipe-0.10.0/codeupipe/deploy/discovery.py +68 -0
  45. codeupipe-0.10.0/codeupipe/deploy/docker.py +357 -0
  46. codeupipe-0.10.0/codeupipe/deploy/fly.py +234 -0
  47. codeupipe-0.10.0/codeupipe/deploy/handlers.py +130 -0
  48. codeupipe-0.10.0/codeupipe/deploy/huggingface.py +225 -0
  49. codeupipe-0.10.0/codeupipe/deploy/init.py +1285 -0
  50. codeupipe-0.10.0/codeupipe/deploy/koyeb.py +250 -0
  51. codeupipe-0.10.0/codeupipe/deploy/manifest.py +115 -0
  52. codeupipe-0.10.0/codeupipe/deploy/netlify.py +259 -0
  53. codeupipe-0.10.0/codeupipe/deploy/oracle.py +300 -0
  54. codeupipe-0.10.0/codeupipe/deploy/railway.py +218 -0
  55. codeupipe-0.10.0/codeupipe/deploy/recipe.py +143 -0
  56. codeupipe-0.10.0/codeupipe/deploy/render.py +300 -0
  57. codeupipe-0.10.0/codeupipe/deploy/vercel.py +259 -0
  58. codeupipe-0.10.0/codeupipe/distribute/__init__.py +18 -0
  59. codeupipe-0.10.0/codeupipe/distribute/checkpoint.py +97 -0
  60. codeupipe-0.10.0/codeupipe/distribute/remote.py +57 -0
  61. codeupipe-0.10.0/codeupipe/distribute/source.py +68 -0
  62. codeupipe-0.10.0/codeupipe/distribute/worker.py +49 -0
  63. codeupipe-0.10.0/codeupipe/doctor.py +185 -0
  64. codeupipe-0.10.0/codeupipe/graph.py +92 -0
  65. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/assemble_doc_report.py +5 -0
  66. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_symbols.py +3 -0
  67. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/detect_drift.py +1 -0
  68. codeupipe-0.10.0/codeupipe/linter/scan_docs.py +106 -0
  69. codeupipe-0.10.0/codeupipe/marketplace/__init__.py +18 -0
  70. codeupipe-0.10.0/codeupipe/marketplace/index.py +165 -0
  71. codeupipe-0.10.0/codeupipe/observe.py +202 -0
  72. codeupipe-0.10.0/codeupipe/registry.py +289 -0
  73. codeupipe-0.10.0/codeupipe/runtime.py +186 -0
  74. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/testing.py +158 -4
  75. codeupipe-0.10.0/codeupipe/upgrade.py +128 -0
  76. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe.egg-info/PKG-INFO +90 -6
  77. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe.egg-info/SOURCES.txt +91 -2
  78. {codeupipe-0.1.0 → codeupipe-0.10.0}/pyproject.toml +3 -1
  79. codeupipe-0.10.0/tests/test_auth.py +782 -0
  80. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_cli.py +4 -5
  81. codeupipe-0.10.0/tests/test_cli_new_commands.py +251 -0
  82. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_cli_smoke.py +1 -1
  83. codeupipe-0.10.0/tests/test_config_pipeline.py +235 -0
  84. codeupipe-0.10.0/tests/test_config_ring3.py +564 -0
  85. codeupipe-0.10.0/tests/test_connect.py +530 -0
  86. codeupipe-0.10.0/tests/test_cup_run.py +114 -0
  87. codeupipe-0.10.0/tests/test_deploy.py +2711 -0
  88. codeupipe-0.10.0/tests/test_discovery.py +196 -0
  89. codeupipe-0.10.0/tests/test_distribute.py +382 -0
  90. codeupipe-0.10.0/tests/test_distribute_cli.py +114 -0
  91. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_doc_check_cli.py +2 -1
  92. codeupipe-0.10.0/tests/test_doctor.py +139 -0
  93. codeupipe-0.10.0/tests/test_doctor_health.py +70 -0
  94. codeupipe-0.10.0/tests/test_execution_modes.py +463 -0
  95. codeupipe-0.10.0/tests/test_govern.py +549 -0
  96. codeupipe-0.10.0/tests/test_graph.py +178 -0
  97. codeupipe-0.10.0/tests/test_marketplace.py +584 -0
  98. codeupipe-0.10.0/tests/test_new_templates.py +44 -0
  99. codeupipe-0.10.0/tests/test_observe.py +573 -0
  100. codeupipe-0.10.0/tests/test_observe_taps.py +247 -0
  101. codeupipe-0.10.0/tests/test_proxy_token.py +226 -0
  102. codeupipe-0.10.0/tests/test_registry.py +232 -0
  103. codeupipe-0.10.0/tests/test_runtime.py +288 -0
  104. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_testing.py +125 -0
  105. codeupipe-0.10.0/tests/test_token_ledger.py +233 -0
  106. codeupipe-0.10.0/tests/test_token_vault.py +280 -0
  107. codeupipe-0.10.0/tests/test_upgrade.py +80 -0
  108. codeupipe-0.10.0/tests/test_vault_cli.py +500 -0
  109. codeupipe-0.10.0/tests/test_vault_hook.py +260 -0
  110. codeupipe-0.1.0/codeupipe/__init__.py +0 -39
  111. codeupipe-0.1.0/codeupipe/cli.py +0 -1502
  112. codeupipe-0.1.0/codeupipe/core/__init__.py +0 -20
  113. codeupipe-0.1.0/codeupipe/core/payload.py +0 -94
  114. codeupipe-0.1.0/codeupipe/core/pipeline.py +0 -231
  115. codeupipe-0.1.0/codeupipe/linter/scan_docs.py +0 -62
  116. {codeupipe-0.1.0 → codeupipe-0.10.0}/LICENSE +0 -0
  117. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/__init__.py +0 -0
  118. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/config.py +0 -0
  119. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/__init__.py +0 -0
  120. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/analyze.py +0 -0
  121. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/classify.py +0 -0
  122. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/classify_files.py +0 -0
  123. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/generate_export.py +0 -0
  124. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/generate_import.py +0 -0
  125. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/parse_config.py +0 -0
  126. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/scan_project.py +0 -0
  127. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/pipelines/__init__.py +0 -0
  128. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/pipelines/export_pipeline.py +0 -0
  129. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/pipelines/import_pipeline.py +0 -0
  130. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/taps/__init__.py +0 -0
  131. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/taps/conversion_log.py +0 -0
  132. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/filter.py +0 -0
  133. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/hook.py +0 -0
  134. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/stream_filter.py +0 -0
  135. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/tap.py +0 -0
  136. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/valve.py +0 -0
  137. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/__init__.py +0 -0
  138. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/assemble_report.py +0 -0
  139. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_bundle.py +0 -0
  140. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_index.py +0 -0
  141. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_naming.py +0 -0
  142. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_protocols.py +0 -0
  143. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_structure.py +0 -0
  144. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_tests.py +0 -0
  145. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/coverage_pipeline.py +0 -0
  146. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/detect_orphans.py +0 -0
  147. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/doc_check_pipeline.py +0 -0
  148. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/git_history.py +0 -0
  149. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/lint_pipeline.py +0 -0
  150. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/map_coverage.py +0 -0
  151. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/report_gaps.py +0 -0
  152. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/report_pipeline.py +0 -0
  153. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/resolve_refs.py +0 -0
  154. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/scan_components.py +0 -0
  155. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/scan_directory.py +0 -0
  156. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/scan_tests.py +0 -0
  157. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/py.typed +0 -0
  158. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/utils/__init__.py +0 -0
  159. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/utils/error_handling.py +0 -0
  160. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe.egg-info/dependency_links.txt +0 -0
  161. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe.egg-info/entry_points.txt +0 -0
  162. {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe.egg-info/top_level.txt +0 -0
  163. {codeupipe-0.1.0 → codeupipe-0.10.0}/setup.cfg +0 -0
  164. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_assemble_doc_report.py +0 -0
  165. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_assemble_report.py +0 -0
  166. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_bundle.py +0 -0
  167. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_index.py +0 -0
  168. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_naming.py +0 -0
  169. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_protocols.py +0 -0
  170. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_structure.py +0 -0
  171. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_symbols.py +0 -0
  172. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_tests.py +0 -0
  173. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_core_edge_cases.py +0 -0
  174. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_coverage_pipeline.py +0 -0
  175. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_detect_drift.py +0 -0
  176. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_detect_orphans.py +0 -0
  177. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_doc_check_pipeline.py +0 -0
  178. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_docs_examples.py +0 -0
  179. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_error_handling.py +0 -0
  180. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_filter.py +0 -0
  181. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_git_history.py +0 -0
  182. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_hook.py +0 -0
  183. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_lint_pipeline.py +0 -0
  184. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_map_coverage.py +0 -0
  185. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_mixed_stream_pipeline.py +0 -0
  186. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_payload.py +0 -0
  187. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_pipeline.py +0 -0
  188. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_real_world_pipelines.py +0 -0
  189. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_report_gaps.py +0 -0
  190. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_report_pipeline.py +0 -0
  191. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_resolve_refs.py +0 -0
  192. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_scan_components.py +0 -0
  193. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_scan_directory.py +0 -0
  194. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_scan_docs.py +0 -0
  195. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_scan_tests.py +0 -0
  196. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_state.py +0 -0
  197. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_stream_filter_run_protection.py +0 -0
  198. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_streaming.py +0 -0
  199. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_sync_support.py +0 -0
  200. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_tap.py +0 -0
  201. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_typed.py +0 -0
  202. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_unintended_usage.py +0 -0
  203. {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_valve.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeupipe
3
- Version: 0.1.0
3
+ Version: 0.10.0
4
4
  Summary: Python pipeline framework — composable Payload-Filter-Pipeline pattern
5
5
  Author: Joshua Wink
6
6
  License: Apache-2.0
@@ -26,7 +26,7 @@ Dynamic: license-file
26
26
 
27
27
  # codeupipe
28
28
 
29
- <!-- cup:ref file=codeupipe/__init__.py hash=afd4ab5 -->
29
+ <!-- cup:ref file=codeupipe/__init__.py hash=3053319 -->
30
30
 
31
31
  Python pipeline framework — composable **Payload → Filter → Pipeline** pattern with streaming support. Zero external dependencies.
32
32
 
@@ -36,7 +36,7 @@ Experimental successor to [codeuchain](https://github.com/codeuchain/codeuchain)
36
36
 
37
37
  ## Core Concepts
38
38
 
39
- <!-- cup:ref file=codeupipe/core/__init__.py hash=e3e2418 -->
39
+ <!-- cup:ref file=codeupipe/core/__init__.py hash=6ed16dd -->
40
40
  | Concept | Role |
41
41
  |---|---|
42
42
  | **Payload** | Immutable data container flowing through the pipeline |
@@ -49,6 +49,7 @@ Experimental successor to [codeuchain](https://github.com/codeuchain/codeuchain)
49
49
  | **State** | Pipeline execution metadata — tracks what ran, what was skipped, errors, chunk counts |
50
50
  | **Hook** | Lifecycle hooks — before/after/on_error for pipeline execution (sync or async) |
51
51
  | **RetryFilter** | Resilience wrapper — retries a Filter up to N times before giving up |
52
+ | **CircuitOpenError** | Raised when a pipeline circuit breaker is open and rejecting calls |
52
53
  <!-- /cup:ref -->
53
54
 
54
55
  ## Install
@@ -151,6 +152,69 @@ class SplitWords:
151
152
  yield Payload({"word": word})
152
153
  ```
153
154
 
155
+ ## Synchronous Execution
156
+
157
+ No manual `asyncio.run()` needed — `run_sync()` handles it:
158
+
159
+ ```python
160
+ pipeline = Pipeline()
161
+ pipeline.add_filter(CleanInput(), name="clean")
162
+ pipeline.add_filter(Validate(), name="validate")
163
+
164
+ result = pipeline.run_sync(Payload({"text": " hello "}))
165
+ print(result.get("text")) # "hello"
166
+ ```
167
+
168
+ ## Parallel Execution (Fan-out / Fan-in)
169
+
170
+ Run independent filters concurrently; results merge back into the payload:
171
+
172
+ ```python
173
+ pipeline = Pipeline()
174
+ pipeline.add_parallel([
175
+ FetchUserFilter(),
176
+ FetchOrdersFilter(),
177
+ FetchRecommendationsFilter(),
178
+ ], name="fan-out")
179
+
180
+ result = pipeline.run_sync(Payload({"user_id": 42}))
181
+ ```
182
+
183
+ ## Pipeline Nesting
184
+
185
+ Compose pipelines from smaller pipelines:
186
+
187
+ ```python
188
+ validation = Pipeline()
189
+ validation.add_filter(CleanInput(), name="clean")
190
+ validation.add_filter(Validate(), name="validate")
191
+
192
+ processing = Pipeline()
193
+ processing.add_pipeline(validation, name="validation-sub")
194
+ processing.add_filter(TransformFilter(), name="transform")
195
+
196
+ result = processing.run_sync(Payload({"text": " hello "}))
197
+ ```
198
+
199
+ ## Retry & Circuit Breaker
200
+
201
+ Pipeline-level resilience wrappers:
202
+
203
+ ```python
204
+ # Retry the entire pipeline up to 3 times on failure
205
+ retrying = pipeline.with_retry(max_retries=3)
206
+ result = retrying.run_sync(Payload({"input": "data"}))
207
+
208
+ # Open the circuit after 5 consecutive failures
209
+ from codeupipe import CircuitOpenError
210
+
211
+ breaker = pipeline.with_circuit_breaker(failure_threshold=5)
212
+ try:
213
+ result = breaker.run_sync(Payload({"input": "data"}))
214
+ except CircuitOpenError:
215
+ print("Service unavailable — circuit is open")
216
+ ```
217
+
154
218
  ## Execution State
155
219
 
156
220
  ```python
@@ -168,10 +232,15 @@ print(pipeline.state.chunks_processed) # {'upper': 3} (streaming mode)
168
232
  | [CONCEPTS.md](CONCEPTS.md) | Full API reference with runnable examples |
169
233
  | [BEST_PRACTICES.md](BEST_PRACTICES.md) | Project structure, naming, testing strategy |
170
234
  | [SKILL.md](SKILL.md) | Agent skill reference — types, patterns, conversion |
235
+ | [ROADMAP.md](ROADMAP.md) | Expansion rings — from framework to platform |
171
236
 
172
237
  ## CLI (`cup`)
173
238
 
174
- <!-- cup:ref file=codeupipe/cli.py symbols=main,scaffold,bundle,lint,coverage,report,doc_check hash=1e63d0e -->
239
+ <!-- cup:ref file=codeupipe/cli/__init__.py symbols=main hash=4691a2f -->
240
+ <!-- cup:ref file=codeupipe/cli/_registry.py symbols=CommandRegistry hash=8e82ece -->
241
+ <!-- cup:ref file=codeupipe/cli/_scaffold.py symbols=scaffold,COMPONENT_TYPES hash=1f62e60 -->
242
+ <!-- cup:ref file=codeupipe/cli/_bundle.py symbols=bundle hash=9a1b776 -->
243
+ <!-- cup:ref file=codeupipe/cli/commands/analysis_cmds.py symbols=lint,coverage,report,doc_check hash=9d54f93 -->
175
244
  The `cup` command-line tool scaffolds, lints, and analyzes CUP projects:
176
245
 
177
246
  ```bash
@@ -183,12 +252,27 @@ cup lint src/signup # Check CUP conventions (CUP000–CUP
183
252
  cup coverage src/signup # Map component↔test coverage gaps
184
253
  cup report src/signup # Health report with scores, orphans, staleness
185
254
  cup doc-check . # Verify doc freshness (cup:ref markers)
255
+ cup run pipeline.json --discover ./filters # Execute a pipeline from config
256
+ cup connect --list # Show configured connectors
257
+ cup connect --health # Run connector health checks
258
+ cup describe pipeline.json # Inspect pipeline inputs, outputs, steps
259
+ cup describe pipeline.json --json # Machine-readable output (--json works globally)
260
+ cup distribute checkpoint cp.json --status # Manage payload checkpoints
261
+ cup distribute remote https://api.example # Test a remote filter endpoint
262
+ cup test # Smart test runner with markers
263
+ cup doctor # Project health diagnostics
264
+ cup graph pipeline.json # Mermaid pipeline visualization
265
+ cup version --bump patch # Show/bump semver
186
266
  ```
187
267
  <!-- /cup:ref -->
268
+ <!-- /cup:ref -->
269
+ <!-- /cup:ref -->
270
+ <!-- /cup:ref -->
271
+ <!-- /cup:ref -->
188
272
 
189
273
  ## Testing Utilities
190
274
 
191
- <!-- cup:ref file=codeupipe/testing.py symbols=run_filter,assert_payload,mock_filter hash=c119f9c -->
275
+ <!-- cup:ref file=codeupipe/testing.py symbols=run_filter,assert_payload,mock_filter hash=65f0296 -->
192
276
  `codeupipe.testing` provides zero-boilerplate test helpers:
193
277
 
194
278
  ```python
@@ -208,7 +292,7 @@ def test_with_mock():
208
292
  ## Test
209
293
 
210
294
  ```bash
211
- pytest # 909 tests
295
+ pytest # 1676 tests
212
296
  ```
213
297
 
214
298
  ## License
@@ -1,6 +1,6 @@
1
1
  # codeupipe
2
2
 
3
- <!-- cup:ref file=codeupipe/__init__.py hash=afd4ab5 -->
3
+ <!-- cup:ref file=codeupipe/__init__.py hash=3053319 -->
4
4
 
5
5
  Python pipeline framework — composable **Payload → Filter → Pipeline** pattern with streaming support. Zero external dependencies.
6
6
 
@@ -10,7 +10,7 @@ Experimental successor to [codeuchain](https://github.com/codeuchain/codeuchain)
10
10
 
11
11
  ## Core Concepts
12
12
 
13
- <!-- cup:ref file=codeupipe/core/__init__.py hash=e3e2418 -->
13
+ <!-- cup:ref file=codeupipe/core/__init__.py hash=6ed16dd -->
14
14
  | Concept | Role |
15
15
  |---|---|
16
16
  | **Payload** | Immutable data container flowing through the pipeline |
@@ -23,6 +23,7 @@ Experimental successor to [codeuchain](https://github.com/codeuchain/codeuchain)
23
23
  | **State** | Pipeline execution metadata — tracks what ran, what was skipped, errors, chunk counts |
24
24
  | **Hook** | Lifecycle hooks — before/after/on_error for pipeline execution (sync or async) |
25
25
  | **RetryFilter** | Resilience wrapper — retries a Filter up to N times before giving up |
26
+ | **CircuitOpenError** | Raised when a pipeline circuit breaker is open and rejecting calls |
26
27
  <!-- /cup:ref -->
27
28
 
28
29
  ## Install
@@ -125,6 +126,69 @@ class SplitWords:
125
126
  yield Payload({"word": word})
126
127
  ```
127
128
 
129
+ ## Synchronous Execution
130
+
131
+ No manual `asyncio.run()` needed — `run_sync()` handles it:
132
+
133
+ ```python
134
+ pipeline = Pipeline()
135
+ pipeline.add_filter(CleanInput(), name="clean")
136
+ pipeline.add_filter(Validate(), name="validate")
137
+
138
+ result = pipeline.run_sync(Payload({"text": " hello "}))
139
+ print(result.get("text")) # "hello"
140
+ ```
141
+
142
+ ## Parallel Execution (Fan-out / Fan-in)
143
+
144
+ Run independent filters concurrently; results merge back into the payload:
145
+
146
+ ```python
147
+ pipeline = Pipeline()
148
+ pipeline.add_parallel([
149
+ FetchUserFilter(),
150
+ FetchOrdersFilter(),
151
+ FetchRecommendationsFilter(),
152
+ ], name="fan-out")
153
+
154
+ result = pipeline.run_sync(Payload({"user_id": 42}))
155
+ ```
156
+
157
+ ## Pipeline Nesting
158
+
159
+ Compose pipelines from smaller pipelines:
160
+
161
+ ```python
162
+ validation = Pipeline()
163
+ validation.add_filter(CleanInput(), name="clean")
164
+ validation.add_filter(Validate(), name="validate")
165
+
166
+ processing = Pipeline()
167
+ processing.add_pipeline(validation, name="validation-sub")
168
+ processing.add_filter(TransformFilter(), name="transform")
169
+
170
+ result = processing.run_sync(Payload({"text": " hello "}))
171
+ ```
172
+
173
+ ## Retry & Circuit Breaker
174
+
175
+ Pipeline-level resilience wrappers:
176
+
177
+ ```python
178
+ # Retry the entire pipeline up to 3 times on failure
179
+ retrying = pipeline.with_retry(max_retries=3)
180
+ result = retrying.run_sync(Payload({"input": "data"}))
181
+
182
+ # Open the circuit after 5 consecutive failures
183
+ from codeupipe import CircuitOpenError
184
+
185
+ breaker = pipeline.with_circuit_breaker(failure_threshold=5)
186
+ try:
187
+ result = breaker.run_sync(Payload({"input": "data"}))
188
+ except CircuitOpenError:
189
+ print("Service unavailable — circuit is open")
190
+ ```
191
+
128
192
  ## Execution State
129
193
 
130
194
  ```python
@@ -142,10 +206,15 @@ print(pipeline.state.chunks_processed) # {'upper': 3} (streaming mode)
142
206
  | [CONCEPTS.md](CONCEPTS.md) | Full API reference with runnable examples |
143
207
  | [BEST_PRACTICES.md](BEST_PRACTICES.md) | Project structure, naming, testing strategy |
144
208
  | [SKILL.md](SKILL.md) | Agent skill reference — types, patterns, conversion |
209
+ | [ROADMAP.md](ROADMAP.md) | Expansion rings — from framework to platform |
145
210
 
146
211
  ## CLI (`cup`)
147
212
 
148
- <!-- cup:ref file=codeupipe/cli.py symbols=main,scaffold,bundle,lint,coverage,report,doc_check hash=1e63d0e -->
213
+ <!-- cup:ref file=codeupipe/cli/__init__.py symbols=main hash=4691a2f -->
214
+ <!-- cup:ref file=codeupipe/cli/_registry.py symbols=CommandRegistry hash=8e82ece -->
215
+ <!-- cup:ref file=codeupipe/cli/_scaffold.py symbols=scaffold,COMPONENT_TYPES hash=1f62e60 -->
216
+ <!-- cup:ref file=codeupipe/cli/_bundle.py symbols=bundle hash=9a1b776 -->
217
+ <!-- cup:ref file=codeupipe/cli/commands/analysis_cmds.py symbols=lint,coverage,report,doc_check hash=9d54f93 -->
149
218
  The `cup` command-line tool scaffolds, lints, and analyzes CUP projects:
150
219
 
151
220
  ```bash
@@ -157,12 +226,27 @@ cup lint src/signup # Check CUP conventions (CUP000–CUP
157
226
  cup coverage src/signup # Map component↔test coverage gaps
158
227
  cup report src/signup # Health report with scores, orphans, staleness
159
228
  cup doc-check . # Verify doc freshness (cup:ref markers)
229
+ cup run pipeline.json --discover ./filters # Execute a pipeline from config
230
+ cup connect --list # Show configured connectors
231
+ cup connect --health # Run connector health checks
232
+ cup describe pipeline.json # Inspect pipeline inputs, outputs, steps
233
+ cup describe pipeline.json --json # Machine-readable output (--json works globally)
234
+ cup distribute checkpoint cp.json --status # Manage payload checkpoints
235
+ cup distribute remote https://api.example # Test a remote filter endpoint
236
+ cup test # Smart test runner with markers
237
+ cup doctor # Project health diagnostics
238
+ cup graph pipeline.json # Mermaid pipeline visualization
239
+ cup version --bump patch # Show/bump semver
160
240
  ```
161
241
  <!-- /cup:ref -->
242
+ <!-- /cup:ref -->
243
+ <!-- /cup:ref -->
244
+ <!-- /cup:ref -->
245
+ <!-- /cup:ref -->
162
246
 
163
247
  ## Testing Utilities
164
248
 
165
- <!-- cup:ref file=codeupipe/testing.py symbols=run_filter,assert_payload,mock_filter hash=c119f9c -->
249
+ <!-- cup:ref file=codeupipe/testing.py symbols=run_filter,assert_payload,mock_filter hash=65f0296 -->
166
250
  `codeupipe.testing` provides zero-boilerplate test helpers:
167
251
 
168
252
  ```python
@@ -182,7 +266,7 @@ def test_with_mock():
182
266
  ## Test
183
267
 
184
268
  ```bash
185
- pytest # 909 tests
269
+ pytest # 1676 tests
186
270
  ```
187
271
 
188
272
  ## License
@@ -0,0 +1,96 @@
1
+ """
2
+ codeupipe: Pipeline framework for Python
3
+
4
+ Composable Payload-Filter-Pipeline pattern with Valves, Taps, Hooks, and Streaming.
5
+ Filters and Taps can be sync or async. Zero external dependencies.
6
+
7
+ Core concepts:
8
+ - Payload: Immutable data container flowing through the pipeline
9
+ - MutablePayload: Mutable sibling for performance-critical bulk edits
10
+ - Filter: Processing unit that transforms payloads (sync or async)
11
+ - StreamFilter: Streaming unit — yields 0, 1, or N output chunks per input
12
+ - Pipeline: Orchestrator — .run() for batch, .stream() for streaming
13
+ - Valve: Conditional flow control — gates a filter with a predicate
14
+ - Tap: Non-modifying observation point (sync or async)
15
+ - State: Pipeline execution metadata — tracks executed, skipped, errors, chunk counts
16
+ - Hook: Lifecycle hooks — before / after / on_error (sync or async)
17
+ """
18
+
19
+ from .core import (
20
+ Payload, MutablePayload, Filter, StreamFilter, Pipeline, CircuitOpenError, Valve, Tap,
21
+ State, Hook, PipelineEvent, EventEmitter,
22
+ PayloadSchema, SchemaViolation, ContractViolation, PipelineTimeoutError,
23
+ AuditEntry, AuditTrail, AuditHook,
24
+ DeadLetterHandler, LogDeadLetterHandler,
25
+ )
26
+ from .utils import ErrorHandlingMixin, RetryFilter
27
+ from .converter import load_config, DEFAULT_CONFIG, PATTERN_DEFAULTS
28
+ from .converter.pipelines import build_export_pipeline, build_import_pipeline
29
+ from .registry import Registry, cup_component, default_registry
30
+ from .distribute import RemoteFilter, Checkpoint, CheckpointHook, IterableSource, FileSource, WorkerPool
31
+ from .deploy import (
32
+ DeployTarget, DeployAdapter, DockerAdapter, VercelAdapter, NetlifyAdapter, RenderAdapter,
33
+ FlyAdapter, RailwayAdapter, CloudRunAdapter, KoyebAdapter,
34
+ AppRunnerAdapter, OracleAdapter, AzureContainerAppsAdapter, HuggingFaceAdapter,
35
+ find_adapters, load_manifest, ManifestError,
36
+ resolve_recipe, list_recipes, RecipeError,
37
+ init_project, list_templates, InitError,
38
+ render_vercel_handler, render_netlify_handler, render_lambda_handler,
39
+ )
40
+ from .connect import (
41
+ ConnectorConfig, load_connector_configs, ConfigError,
42
+ discover_connectors, check_health, HttpConnector,
43
+ )
44
+ from .observe import CaptureTap, MetricsTap, RunRecord
45
+ from .graph import pipeline_to_mermaid
46
+ from .runtime import TapSwitch, HotSwap
47
+ from .auth import Credential, CredentialStore, AuthProvider, GoogleOAuth, GitHubOAuth, AuthHook
48
+
49
+ from importlib.metadata import version as _pkg_version, PackageNotFoundError
50
+ try:
51
+ __version__ = _pkg_version("codeupipe")
52
+ except PackageNotFoundError:
53
+ __version__ = "0.10.0" # fallback for editable / non-installed usage
54
+ __all__ = [
55
+ # Core
56
+ "Payload", "MutablePayload",
57
+ "Filter", "StreamFilter", "Pipeline", "CircuitOpenError", "Valve", "Tap",
58
+ "State", "Hook",
59
+ # Observe
60
+ "PipelineEvent", "EventEmitter",
61
+ # Govern
62
+ "PayloadSchema", "SchemaViolation", "ContractViolation", "PipelineTimeoutError",
63
+ "AuditEntry", "AuditTrail", "AuditHook",
64
+ "DeadLetterHandler", "LogDeadLetterHandler",
65
+ # Utils
66
+ "ErrorHandlingMixin", "RetryFilter",
67
+ # Converter
68
+ "load_config", "DEFAULT_CONFIG", "PATTERN_DEFAULTS",
69
+ "build_export_pipeline", "build_import_pipeline",
70
+ # Registry
71
+ "Registry", "cup_component", "default_registry",
72
+ # Distribute
73
+ "RemoteFilter", "Checkpoint", "CheckpointHook",
74
+ "IterableSource", "FileSource", "WorkerPool",
75
+ # Deploy
76
+ "DeployTarget", "DeployAdapter", "DockerAdapter",
77
+ "VercelAdapter", "NetlifyAdapter", "RenderAdapter",
78
+ "FlyAdapter", "RailwayAdapter", "CloudRunAdapter", "KoyebAdapter",
79
+ "AppRunnerAdapter", "OracleAdapter", "AzureContainerAppsAdapter", "HuggingFaceAdapter",
80
+ "find_adapters", "load_manifest", "ManifestError",
81
+ "resolve_recipe", "list_recipes", "RecipeError",
82
+ "init_project", "list_templates", "InitError",
83
+ "render_vercel_handler", "render_netlify_handler", "render_lambda_handler",
84
+ # Connect
85
+ "ConnectorConfig", "load_connector_configs", "ConfigError",
86
+ "discover_connectors", "check_health", "HttpConnector",
87
+ # Observe
88
+ "CaptureTap", "MetricsTap", "RunRecord",
89
+ # Graph
90
+ "pipeline_to_mermaid",
91
+ # Runtime
92
+ "TapSwitch", "HotSwap",
93
+ # Auth
94
+ "Credential", "CredentialStore", "AuthProvider",
95
+ "GoogleOAuth", "GitHubOAuth", "AuthHook",
96
+ ]
@@ -0,0 +1,45 @@
1
+ """
2
+ codeupipe.auth: OAuth2 credential management for pipelines.
3
+
4
+ Provides browser-based OAuth2 flows, persistent token storage,
5
+ automatic credential injection, and proxy token vaulting.
6
+ Zero external dependencies — stdlib only.
7
+
8
+ Core types:
9
+ - Credential: Token container (access_token, refresh_token, expiry, scopes)
10
+ - CredentialStore: Persist + refresh credentials — file-backed JSON
11
+ - AuthProvider: Protocol for OAuth2 flows (authorize_url, exchange_code, refresh)
12
+ - AuthHook: Pipeline Hook — injects fresh tokens into Payload before each run
13
+
14
+ Vault types (proxy token indirection):
15
+ - ProxyToken: Opaque reference token (cup_tok_*) — never exposes real credentials
16
+ - TokenLedger: Audit trail — issued, resolved, revoked events
17
+ - TokenVault: Central authority — issues, resolves, revokes proxy tokens
18
+ - VaultHook: Pipeline Hook — injects proxy tokens instead of real credentials
19
+
20
+ Built-in providers:
21
+ - GoogleOAuth: Google OAuth2 (Calendar, Drive, Gmail, etc.)
22
+ - GitHubOAuth: GitHub OAuth2 (repos, issues, actions, etc.)
23
+ """
24
+
25
+ from .credential import Credential, CredentialStore
26
+ from .hook import AuthHook
27
+ from .provider import AuthProvider, GitHubOAuth, GoogleOAuth
28
+ from .proxy_token import ProxyToken
29
+ from .token_ledger import LedgerEvent, TokenLedger
30
+ from .token_vault import TokenVault
31
+ from .vault_hook import VaultHook
32
+
33
+ __all__ = [
34
+ "AuthHook",
35
+ "AuthProvider",
36
+ "Credential",
37
+ "CredentialStore",
38
+ "GitHubOAuth",
39
+ "GoogleOAuth",
40
+ "LedgerEvent",
41
+ "ProxyToken",
42
+ "TokenLedger",
43
+ "TokenVault",
44
+ "VaultHook",
45
+ ]
@@ -0,0 +1,147 @@
1
+ """
2
+ Local OAuth2 callback server.
3
+
4
+ Starts a temporary HTTP server on a free port, opens the browser
5
+ to the authorization URL, waits for the callback with the auth code,
6
+ then shuts down. Pure stdlib — http.server + webbrowser.
7
+ """
8
+
9
+ import html
10
+ import secrets
11
+ import threading
12
+ import urllib.parse
13
+ import webbrowser
14
+ from http.server import HTTPServer, BaseHTTPRequestHandler
15
+ from typing import Optional, Tuple
16
+
17
+ __all__ = ["run_oauth_flow"]
18
+
19
+ _SUCCESS_PAGE = """\
20
+ <!DOCTYPE html>
21
+ <html>
22
+ <head><title>codeupipe — Authenticated</title></head>
23
+ <body style="font-family:system-ui;text-align:center;padding:60px;background:#1a1a2e;color:#e0e0e0">
24
+ <h1 style="color:#4ecdc4">&#x2713; Authenticated</h1>
25
+ <p>You can close this tab and return to the terminal.</p>
26
+ </body>
27
+ </html>
28
+ """
29
+
30
+ _ERROR_PAGE = """\
31
+ <!DOCTYPE html>
32
+ <html>
33
+ <head><title>codeupipe — Auth Error</title></head>
34
+ <body style="font-family:system-ui;text-align:center;padding:60px;background:#1a1a2e;color:#e0e0e0">
35
+ <h1 style="color:#e74c3c">&#x2717; Authentication Failed</h1>
36
+ <p>{error}</p>
37
+ </body>
38
+ </html>
39
+ """
40
+
41
+
42
+ class _CallbackHandler(BaseHTTPRequestHandler):
43
+ """Handles the OAuth2 callback redirect."""
44
+
45
+ def do_GET(self):
46
+ parsed = urllib.parse.urlparse(self.path)
47
+ params = urllib.parse.parse_qs(parsed.query)
48
+
49
+ # Extract code and state
50
+ code = params.get("code", [None])[0]
51
+ state = params.get("state", [None])[0]
52
+ error = params.get("error", [None])[0]
53
+ error_desc = params.get("error_description", ["Unknown error"])[0]
54
+
55
+ if error:
56
+ self.server._auth_result = ("error", error_desc) # type: ignore[attr-defined]
57
+ self._send_html(400, _ERROR_PAGE.format(error=html.escape(error_desc)))
58
+ elif code and state == self.server._expected_state: # type: ignore[attr-defined]
59
+ self.server._auth_result = ("ok", code) # type: ignore[attr-defined]
60
+ self._send_html(200, _SUCCESS_PAGE)
61
+ else:
62
+ msg = "State mismatch — possible CSRF. Please try again."
63
+ self.server._auth_result = ("error", msg) # type: ignore[attr-defined]
64
+ self._send_html(400, _ERROR_PAGE.format(error=html.escape(msg)))
65
+
66
+ # Signal the main thread to shut down
67
+ threading.Thread(target=self.server.shutdown, daemon=True).start()
68
+
69
+ def _send_html(self, code: int, body: str):
70
+ self.send_response(code)
71
+ self.send_header("Content-Type", "text/html; charset=utf-8")
72
+ self.end_headers()
73
+ self.wfile.write(body.encode("utf-8"))
74
+
75
+ def log_message(self, format, *args):
76
+ # Suppress noisy request logs
77
+ pass
78
+
79
+
80
+ def run_oauth_flow(
81
+ provider,
82
+ port: int = 0,
83
+ timeout: int = 120,
84
+ open_browser: bool = True,
85
+ ) -> Tuple[str, str]:
86
+ """Run a complete browser-based OAuth2 flow.
87
+
88
+ 1. Starts a local HTTP server on *port* (0 = auto-pick free port).
89
+ 2. Generates a random state parameter for CSRF protection.
90
+ 3. Opens the browser to the provider's authorization URL.
91
+ 4. Waits for the callback with the auth code.
92
+ 5. Returns (code, redirect_uri) for token exchange.
93
+
94
+ Args:
95
+ provider: An AuthProvider instance.
96
+ port: Local port for callback server (0 = auto).
97
+ timeout: Max seconds to wait for the callback.
98
+ open_browser: Whether to auto-open the browser.
99
+
100
+ Returns:
101
+ Tuple of (authorization_code, redirect_uri).
102
+
103
+ Raises:
104
+ RuntimeError: If the flow fails, times out, or user denies.
105
+ TimeoutError: If the callback is not received in time.
106
+ """
107
+ state = secrets.token_urlsafe(32)
108
+
109
+ server = HTTPServer(("127.0.0.1", port), _CallbackHandler)
110
+ actual_port = server.server_address[1]
111
+ redirect_uri = f"http://localhost:{actual_port}/callback"
112
+
113
+ server._expected_state = state # type: ignore[attr-defined]
114
+ server._auth_result = None # type: ignore[attr-defined]
115
+
116
+ auth_url = provider.authorize_url(redirect_uri, state)
117
+
118
+ # Run server in a thread so we can enforce a timeout
119
+ server_thread = threading.Thread(target=server.serve_forever, daemon=True)
120
+ server_thread.start()
121
+
122
+ if open_browser:
123
+ print(f"\nOpening browser for {provider.name} authentication...")
124
+ print(f"If it doesn't open, visit:\n {auth_url}\n")
125
+ webbrowser.open(auth_url)
126
+ else:
127
+ print(f"\nOpen this URL in your browser:\n {auth_url}\n")
128
+
129
+ # Wait for the callback
130
+ server_thread.join(timeout=timeout)
131
+
132
+ # Shut down if still running (timeout)
133
+ server.shutdown()
134
+ server.server_close()
135
+
136
+ result = server._auth_result # type: ignore[attr-defined]
137
+ if result is None:
138
+ raise TimeoutError(
139
+ f"OAuth callback not received within {timeout}s. "
140
+ "Make sure you complete the login in your browser."
141
+ )
142
+
143
+ status, value = result
144
+ if status == "error":
145
+ raise RuntimeError(f"OAuth flow failed: {value}")
146
+
147
+ return value, redirect_uri