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.
- {codeupipe-0.1.0 → codeupipe-0.10.0}/PKG-INFO +90 -6
- {codeupipe-0.1.0 → codeupipe-0.10.0}/README.md +89 -5
- codeupipe-0.10.0/codeupipe/__init__.py +96 -0
- codeupipe-0.10.0/codeupipe/auth/__init__.py +45 -0
- codeupipe-0.10.0/codeupipe/auth/_server.py +147 -0
- codeupipe-0.10.0/codeupipe/auth/credential.py +210 -0
- codeupipe-0.10.0/codeupipe/auth/hook.py +70 -0
- codeupipe-0.10.0/codeupipe/auth/provider.py +260 -0
- codeupipe-0.10.0/codeupipe/auth/proxy_token.py +190 -0
- codeupipe-0.10.0/codeupipe/auth/token_ledger.py +188 -0
- codeupipe-0.10.0/codeupipe/auth/token_vault.py +184 -0
- codeupipe-0.10.0/codeupipe/auth/vault_hook.py +91 -0
- codeupipe-0.10.0/codeupipe/cli/__init__.py +101 -0
- codeupipe-0.10.0/codeupipe/cli/__main__.py +5 -0
- codeupipe-0.10.0/codeupipe/cli/_bundle.py +80 -0
- codeupipe-0.10.0/codeupipe/cli/_registry.py +35 -0
- codeupipe-0.10.0/codeupipe/cli/_scaffold.py +299 -0
- codeupipe-0.10.0/codeupipe/cli/_templates.py +604 -0
- codeupipe-0.10.0/codeupipe/cli/commands/__init__.py +37 -0
- codeupipe-0.10.0/codeupipe/cli/commands/analysis_cmds.py +357 -0
- codeupipe-0.10.0/codeupipe/cli/commands/auth_cmds.py +189 -0
- codeupipe-0.10.0/codeupipe/cli/commands/connect_cmds.py +192 -0
- codeupipe-0.10.0/codeupipe/cli/commands/deploy_cmds.py +246 -0
- codeupipe-0.10.0/codeupipe/cli/commands/distribute_cmds.py +132 -0
- codeupipe-0.10.0/codeupipe/cli/commands/project_cmds.py +243 -0
- codeupipe-0.10.0/codeupipe/cli/commands/run_cmds.py +188 -0
- codeupipe-0.10.0/codeupipe/cli/commands/scaffold_cmds.py +61 -0
- codeupipe-0.10.0/codeupipe/cli/commands/vault_cmds.py +321 -0
- codeupipe-0.10.0/codeupipe/connect/__init__.py +22 -0
- codeupipe-0.10.0/codeupipe/connect/config.py +105 -0
- codeupipe-0.10.0/codeupipe/connect/discovery.py +128 -0
- codeupipe-0.10.0/codeupipe/connect/http.py +114 -0
- codeupipe-0.10.0/codeupipe/core/__init__.py +31 -0
- codeupipe-0.10.0/codeupipe/core/event.py +63 -0
- codeupipe-0.10.0/codeupipe/core/govern.py +244 -0
- codeupipe-0.10.0/codeupipe/core/payload.py +157 -0
- codeupipe-0.10.0/codeupipe/core/pipeline.py +764 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/state.py +40 -2
- codeupipe-0.10.0/codeupipe/deploy/__init__.py +62 -0
- codeupipe-0.10.0/codeupipe/deploy/adapter.py +47 -0
- codeupipe-0.10.0/codeupipe/deploy/apprunner.py +238 -0
- codeupipe-0.10.0/codeupipe/deploy/azure_container_apps.py +237 -0
- codeupipe-0.10.0/codeupipe/deploy/cloudrun.py +244 -0
- codeupipe-0.10.0/codeupipe/deploy/discovery.py +68 -0
- codeupipe-0.10.0/codeupipe/deploy/docker.py +357 -0
- codeupipe-0.10.0/codeupipe/deploy/fly.py +234 -0
- codeupipe-0.10.0/codeupipe/deploy/handlers.py +130 -0
- codeupipe-0.10.0/codeupipe/deploy/huggingface.py +225 -0
- codeupipe-0.10.0/codeupipe/deploy/init.py +1285 -0
- codeupipe-0.10.0/codeupipe/deploy/koyeb.py +250 -0
- codeupipe-0.10.0/codeupipe/deploy/manifest.py +115 -0
- codeupipe-0.10.0/codeupipe/deploy/netlify.py +259 -0
- codeupipe-0.10.0/codeupipe/deploy/oracle.py +300 -0
- codeupipe-0.10.0/codeupipe/deploy/railway.py +218 -0
- codeupipe-0.10.0/codeupipe/deploy/recipe.py +143 -0
- codeupipe-0.10.0/codeupipe/deploy/render.py +300 -0
- codeupipe-0.10.0/codeupipe/deploy/vercel.py +259 -0
- codeupipe-0.10.0/codeupipe/distribute/__init__.py +18 -0
- codeupipe-0.10.0/codeupipe/distribute/checkpoint.py +97 -0
- codeupipe-0.10.0/codeupipe/distribute/remote.py +57 -0
- codeupipe-0.10.0/codeupipe/distribute/source.py +68 -0
- codeupipe-0.10.0/codeupipe/distribute/worker.py +49 -0
- codeupipe-0.10.0/codeupipe/doctor.py +185 -0
- codeupipe-0.10.0/codeupipe/graph.py +92 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/assemble_doc_report.py +5 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_symbols.py +3 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/detect_drift.py +1 -0
- codeupipe-0.10.0/codeupipe/linter/scan_docs.py +106 -0
- codeupipe-0.10.0/codeupipe/marketplace/__init__.py +18 -0
- codeupipe-0.10.0/codeupipe/marketplace/index.py +165 -0
- codeupipe-0.10.0/codeupipe/observe.py +202 -0
- codeupipe-0.10.0/codeupipe/registry.py +289 -0
- codeupipe-0.10.0/codeupipe/runtime.py +186 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/testing.py +158 -4
- codeupipe-0.10.0/codeupipe/upgrade.py +128 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe.egg-info/PKG-INFO +90 -6
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe.egg-info/SOURCES.txt +91 -2
- {codeupipe-0.1.0 → codeupipe-0.10.0}/pyproject.toml +3 -1
- codeupipe-0.10.0/tests/test_auth.py +782 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_cli.py +4 -5
- codeupipe-0.10.0/tests/test_cli_new_commands.py +251 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_cli_smoke.py +1 -1
- codeupipe-0.10.0/tests/test_config_pipeline.py +235 -0
- codeupipe-0.10.0/tests/test_config_ring3.py +564 -0
- codeupipe-0.10.0/tests/test_connect.py +530 -0
- codeupipe-0.10.0/tests/test_cup_run.py +114 -0
- codeupipe-0.10.0/tests/test_deploy.py +2711 -0
- codeupipe-0.10.0/tests/test_discovery.py +196 -0
- codeupipe-0.10.0/tests/test_distribute.py +382 -0
- codeupipe-0.10.0/tests/test_distribute_cli.py +114 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_doc_check_cli.py +2 -1
- codeupipe-0.10.0/tests/test_doctor.py +139 -0
- codeupipe-0.10.0/tests/test_doctor_health.py +70 -0
- codeupipe-0.10.0/tests/test_execution_modes.py +463 -0
- codeupipe-0.10.0/tests/test_govern.py +549 -0
- codeupipe-0.10.0/tests/test_graph.py +178 -0
- codeupipe-0.10.0/tests/test_marketplace.py +584 -0
- codeupipe-0.10.0/tests/test_new_templates.py +44 -0
- codeupipe-0.10.0/tests/test_observe.py +573 -0
- codeupipe-0.10.0/tests/test_observe_taps.py +247 -0
- codeupipe-0.10.0/tests/test_proxy_token.py +226 -0
- codeupipe-0.10.0/tests/test_registry.py +232 -0
- codeupipe-0.10.0/tests/test_runtime.py +288 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_testing.py +125 -0
- codeupipe-0.10.0/tests/test_token_ledger.py +233 -0
- codeupipe-0.10.0/tests/test_token_vault.py +280 -0
- codeupipe-0.10.0/tests/test_upgrade.py +80 -0
- codeupipe-0.10.0/tests/test_vault_cli.py +500 -0
- codeupipe-0.10.0/tests/test_vault_hook.py +260 -0
- codeupipe-0.1.0/codeupipe/__init__.py +0 -39
- codeupipe-0.1.0/codeupipe/cli.py +0 -1502
- codeupipe-0.1.0/codeupipe/core/__init__.py +0 -20
- codeupipe-0.1.0/codeupipe/core/payload.py +0 -94
- codeupipe-0.1.0/codeupipe/core/pipeline.py +0 -231
- codeupipe-0.1.0/codeupipe/linter/scan_docs.py +0 -62
- {codeupipe-0.1.0 → codeupipe-0.10.0}/LICENSE +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/__init__.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/config.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/__init__.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/analyze.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/classify.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/classify_files.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/generate_export.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/generate_import.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/parse_config.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/filters/scan_project.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/pipelines/__init__.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/pipelines/export_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/pipelines/import_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/taps/__init__.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/converter/taps/conversion_log.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/filter.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/hook.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/stream_filter.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/tap.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/core/valve.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/__init__.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/assemble_report.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_bundle.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_index.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_naming.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_protocols.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_structure.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/check_tests.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/coverage_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/detect_orphans.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/doc_check_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/git_history.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/lint_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/map_coverage.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/report_gaps.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/report_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/resolve_refs.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/scan_components.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/scan_directory.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/linter/scan_tests.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/py.typed +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/utils/__init__.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe/utils/error_handling.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe.egg-info/dependency_links.txt +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe.egg-info/entry_points.txt +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/codeupipe.egg-info/top_level.txt +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/setup.cfg +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_assemble_doc_report.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_assemble_report.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_bundle.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_index.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_naming.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_protocols.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_structure.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_symbols.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_check_tests.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_core_edge_cases.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_coverage_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_detect_drift.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_detect_orphans.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_doc_check_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_docs_examples.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_error_handling.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_filter.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_git_history.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_hook.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_lint_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_map_coverage.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_mixed_stream_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_payload.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_real_world_pipelines.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_report_gaps.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_report_pipeline.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_resolve_refs.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_scan_components.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_scan_directory.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_scan_docs.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_scan_tests.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_state.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_stream_filter_run_protection.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_streaming.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_sync_support.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_tap.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_typed.py +0 -0
- {codeupipe-0.1.0 → codeupipe-0.10.0}/tests/test_unintended_usage.py +0 -0
- {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.
|
|
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=
|
|
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=
|
|
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
|
|
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=
|
|
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 #
|
|
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=
|
|
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=
|
|
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
|
|
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=
|
|
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 #
|
|
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">✓ 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">✗ 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
|