whytrail 0.1.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 (179) hide show
  1. whytrail-0.1.0/.github/actions/whytrail-run/README.md +20 -0
  2. whytrail-0.1.0/.github/actions/whytrail-run/action.yml +50 -0
  3. whytrail-0.1.0/.github/workflows/ci.yml +333 -0
  4. whytrail-0.1.0/.github/workflows/docs.yml +51 -0
  5. whytrail-0.1.0/.github/workflows/publish.yml +50 -0
  6. whytrail-0.1.0/.gitignore +18 -0
  7. whytrail-0.1.0/CHANGELOG.md +423 -0
  8. whytrail-0.1.0/LICENSE +21 -0
  9. whytrail-0.1.0/PKG-INFO +185 -0
  10. whytrail-0.1.0/README.md +144 -0
  11. whytrail-0.1.0/benchmarks/test_overhead.py +58 -0
  12. whytrail-0.1.0/docs/adr/0001-whytrail-architecture.md +159 -0
  13. whytrail-0.1.0/docs/adr/0002-category-strategy.md +100 -0
  14. whytrail-0.1.0/docs/adr/0003-ecosystem-scale-triage.md +248 -0
  15. whytrail-0.1.0/docs/adr/0004-rename-to-whytrail.md +81 -0
  16. whytrail-0.1.0/docs/adr/0005-vscode-extension-scope.md +112 -0
  17. whytrail-0.1.0/docs/changelog.md +1 -0
  18. whytrail-0.1.0/docs/index.md +1 -0
  19. whytrail-0.1.0/docs/plugin-guide.md +256 -0
  20. whytrail-0.1.0/docs/testing-maturity.md +215 -0
  21. whytrail-0.1.0/examples/ex_cli_crash.py +15 -0
  22. whytrail-0.1.0/examples/ex_tracked_pipeline.py +35 -0
  23. whytrail-0.1.0/mkdocs.yml +83 -0
  24. whytrail-0.1.0/plugins/whytrail-aiohttp/README.md +16 -0
  25. whytrail-0.1.0/plugins/whytrail-aiohttp/pyproject.toml +18 -0
  26. whytrail-0.1.0/plugins/whytrail-aiohttp/src/whytrail_aiohttp/__init__.py +48 -0
  27. whytrail-0.1.0/plugins/whytrail-anthropic/README.md +15 -0
  28. whytrail-0.1.0/plugins/whytrail-anthropic/pyproject.toml +18 -0
  29. whytrail-0.1.0/plugins/whytrail-anthropic/src/whytrail_anthropic/__init__.py +61 -0
  30. whytrail-0.1.0/plugins/whytrail-asyncpg/README.md +15 -0
  31. whytrail-0.1.0/plugins/whytrail-asyncpg/pyproject.toml +18 -0
  32. whytrail-0.1.0/plugins/whytrail-asyncpg/src/whytrail_asyncpg/__init__.py +62 -0
  33. whytrail-0.1.0/plugins/whytrail-boto3/README.md +23 -0
  34. whytrail-0.1.0/plugins/whytrail-boto3/pyproject.toml +18 -0
  35. whytrail-0.1.0/plugins/whytrail-boto3/src/whytrail_boto3/__init__.py +62 -0
  36. whytrail-0.1.0/plugins/whytrail-celery/README.md +17 -0
  37. whytrail-0.1.0/plugins/whytrail-celery/pyproject.toml +15 -0
  38. whytrail-0.1.0/plugins/whytrail-celery/src/whytrail_celery/__init__.py +75 -0
  39. whytrail-0.1.0/plugins/whytrail-ddtrace/README.md +25 -0
  40. whytrail-0.1.0/plugins/whytrail-ddtrace/pyproject.toml +15 -0
  41. whytrail-0.1.0/plugins/whytrail-ddtrace/src/whytrail_ddtrace/__init__.py +64 -0
  42. whytrail-0.1.0/plugins/whytrail-django/README.md +31 -0
  43. whytrail-0.1.0/plugins/whytrail-django/pyproject.toml +15 -0
  44. whytrail-0.1.0/plugins/whytrail-django/src/whytrail_django/__init__.py +79 -0
  45. whytrail-0.1.0/plugins/whytrail-dramatiq/README.md +20 -0
  46. whytrail-0.1.0/plugins/whytrail-dramatiq/pyproject.toml +15 -0
  47. whytrail-0.1.0/plugins/whytrail-dramatiq/src/whytrail_dramatiq/__init__.py +77 -0
  48. whytrail-0.1.0/plugins/whytrail-fastapi/README.md +36 -0
  49. whytrail-0.1.0/plugins/whytrail-fastapi/pyproject.toml +24 -0
  50. whytrail-0.1.0/plugins/whytrail-fastapi/src/whytrail_fastapi/__init__.py +83 -0
  51. whytrail-0.1.0/plugins/whytrail-flask/README.md +26 -0
  52. whytrail-0.1.0/plugins/whytrail-flask/pyproject.toml +15 -0
  53. whytrail-0.1.0/plugins/whytrail-flask/src/whytrail_flask/__init__.py +65 -0
  54. whytrail-0.1.0/plugins/whytrail-google-cloud/README.md +18 -0
  55. whytrail-0.1.0/plugins/whytrail-google-cloud/pyproject.toml +18 -0
  56. whytrail-0.1.0/plugins/whytrail-google-cloud/src/whytrail_google_cloud/__init__.py +51 -0
  57. whytrail-0.1.0/plugins/whytrail-grpcio/README.md +13 -0
  58. whytrail-0.1.0/plugins/whytrail-grpcio/pyproject.toml +18 -0
  59. whytrail-0.1.0/plugins/whytrail-grpcio/src/whytrail_grpcio/__init__.py +43 -0
  60. whytrail-0.1.0/plugins/whytrail-httpx/README.md +15 -0
  61. whytrail-0.1.0/plugins/whytrail-httpx/pyproject.toml +18 -0
  62. whytrail-0.1.0/plugins/whytrail-httpx/src/whytrail_httpx/__init__.py +52 -0
  63. whytrail-0.1.0/plugins/whytrail-huggingface-hub/README.md +16 -0
  64. whytrail-0.1.0/plugins/whytrail-huggingface-hub/pyproject.toml +18 -0
  65. whytrail-0.1.0/plugins/whytrail-huggingface-hub/src/whytrail_huggingface_hub/__init__.py +44 -0
  66. whytrail-0.1.0/plugins/whytrail-jsonschema/README.md +26 -0
  67. whytrail-0.1.0/plugins/whytrail-jsonschema/pyproject.toml +18 -0
  68. whytrail-0.1.0/plugins/whytrail-jsonschema/src/whytrail_jsonschema/__init__.py +46 -0
  69. whytrail-0.1.0/plugins/whytrail-langchain/README.md +32 -0
  70. whytrail-0.1.0/plugins/whytrail-langchain/pyproject.toml +15 -0
  71. whytrail-0.1.0/plugins/whytrail-langchain/src/whytrail_langchain/__init__.py +244 -0
  72. whytrail-0.1.0/plugins/whytrail-marshmallow/README.md +18 -0
  73. whytrail-0.1.0/plugins/whytrail-marshmallow/pyproject.toml +18 -0
  74. whytrail-0.1.0/plugins/whytrail-marshmallow/src/whytrail_marshmallow/__init__.py +61 -0
  75. whytrail-0.1.0/plugins/whytrail-openai/README.md +15 -0
  76. whytrail-0.1.0/plugins/whytrail-openai/pyproject.toml +18 -0
  77. whytrail-0.1.0/plugins/whytrail-openai/src/whytrail_openai/__init__.py +68 -0
  78. whytrail-0.1.0/plugins/whytrail-pandas/README.md +41 -0
  79. whytrail-0.1.0/plugins/whytrail-pandas/pyproject.toml +18 -0
  80. whytrail-0.1.0/plugins/whytrail-pandas/src/whytrail_pandas/__init__.py +101 -0
  81. whytrail-0.1.0/plugins/whytrail-polars/README.md +16 -0
  82. whytrail-0.1.0/plugins/whytrail-polars/pyproject.toml +18 -0
  83. whytrail-0.1.0/plugins/whytrail-polars/src/whytrail_polars/__init__.py +89 -0
  84. whytrail-0.1.0/plugins/whytrail-prefect/README.md +19 -0
  85. whytrail-0.1.0/plugins/whytrail-prefect/pyproject.toml +15 -0
  86. whytrail-0.1.0/plugins/whytrail-prefect/src/whytrail_prefect/__init__.py +54 -0
  87. whytrail-0.1.0/plugins/whytrail-pydantic/README.md +26 -0
  88. whytrail-0.1.0/plugins/whytrail-pydantic/pyproject.toml +18 -0
  89. whytrail-0.1.0/plugins/whytrail-pydantic/src/whytrail_pydantic/__init__.py +58 -0
  90. whytrail-0.1.0/plugins/whytrail-pymongo/README.md +25 -0
  91. whytrail-0.1.0/plugins/whytrail-pymongo/pyproject.toml +18 -0
  92. whytrail-0.1.0/plugins/whytrail-pymongo/src/whytrail_pymongo/__init__.py +60 -0
  93. whytrail-0.1.0/plugins/whytrail-pytest/README.md +15 -0
  94. whytrail-0.1.0/plugins/whytrail-pytest/pyproject.toml +18 -0
  95. whytrail-0.1.0/plugins/whytrail-pytest/src/whytrail_pytest/plugin.py +53 -0
  96. whytrail-0.1.0/plugins/whytrail-pyyaml/README.md +21 -0
  97. whytrail-0.1.0/plugins/whytrail-pyyaml/pyproject.toml +18 -0
  98. whytrail-0.1.0/plugins/whytrail-pyyaml/src/whytrail_pyyaml/__init__.py +61 -0
  99. whytrail-0.1.0/plugins/whytrail-requests/README.md +24 -0
  100. whytrail-0.1.0/plugins/whytrail-requests/pyproject.toml +18 -0
  101. whytrail-0.1.0/plugins/whytrail-requests/src/whytrail_requests/__init__.py +85 -0
  102. whytrail-0.1.0/plugins/whytrail-rq/README.md +22 -0
  103. whytrail-0.1.0/plugins/whytrail-rq/pyproject.toml +22 -0
  104. whytrail-0.1.0/plugins/whytrail-rq/src/whytrail_rq/__init__.py +66 -0
  105. whytrail-0.1.0/plugins/whytrail-scrapy/README.md +17 -0
  106. whytrail-0.1.0/plugins/whytrail-scrapy/pyproject.toml +15 -0
  107. whytrail-0.1.0/plugins/whytrail-scrapy/src/whytrail_scrapy/__init__.py +61 -0
  108. whytrail-0.1.0/plugins/whytrail-sentry/README.md +29 -0
  109. whytrail-0.1.0/plugins/whytrail-sentry/pyproject.toml +15 -0
  110. whytrail-0.1.0/plugins/whytrail-sentry/src/whytrail_sentry/__init__.py +82 -0
  111. whytrail-0.1.0/plugins/whytrail-sqlalchemy/README.md +25 -0
  112. whytrail-0.1.0/plugins/whytrail-sqlalchemy/pyproject.toml +18 -0
  113. whytrail-0.1.0/plugins/whytrail-sqlalchemy/src/whytrail_sqlalchemy/__init__.py +75 -0
  114. whytrail-0.1.0/pyproject.toml +99 -0
  115. whytrail-0.1.0/scripts/new_plugin.py +244 -0
  116. whytrail-0.1.0/src/whytrail/__init__.py +210 -0
  117. whytrail-0.1.0/src/whytrail/_repr.py +27 -0
  118. whytrail-0.1.0/src/whytrail/cli/__init__.py +0 -0
  119. whytrail-0.1.0/src/whytrail/cli/__main__.py +74 -0
  120. whytrail-0.1.0/src/whytrail/core/__init__.py +0 -0
  121. whytrail-0.1.0/src/whytrail/core/explanation.py +187 -0
  122. whytrail-0.1.0/src/whytrail/core/graph.py +160 -0
  123. whytrail-0.1.0/src/whytrail/core/node.py +84 -0
  124. whytrail-0.1.0/src/whytrail/core/serialize.py +117 -0
  125. whytrail-0.1.0/src/whytrail/explainers/__init__.py +0 -0
  126. whytrail-0.1.0/src/whytrail/explainers/builtin.py +125 -0
  127. whytrail-0.1.0/src/whytrail/otel.py +87 -0
  128. whytrail-0.1.0/src/whytrail/propagation.py +104 -0
  129. whytrail-0.1.0/src/whytrail/protocols.py +48 -0
  130. whytrail-0.1.0/src/whytrail/py.typed +0 -0
  131. whytrail-0.1.0/src/whytrail/registry.py +107 -0
  132. whytrail-0.1.0/src/whytrail/runtime/__init__.py +0 -0
  133. whytrail-0.1.0/src/whytrail/runtime/capture.py +162 -0
  134. whytrail-0.1.0/src/whytrail/runtime/context.py +128 -0
  135. whytrail-0.1.0/src/whytrail/runtime/monitoring.py +154 -0
  136. whytrail-0.1.0/tests/conftest.py +21 -0
  137. whytrail-0.1.0/tests/integration/test_cli.py +73 -0
  138. whytrail-0.1.0/tests/integration/test_deep_trace.py +88 -0
  139. whytrail-0.1.0/tests/integration/test_otel.py +109 -0
  140. whytrail-0.1.0/tests/integration/test_propagation.py +55 -0
  141. whytrail-0.1.0/tests/integration/test_protocol_and_registry.py +101 -0
  142. whytrail-0.1.0/tests/integration/test_why_exceptions.py +110 -0
  143. whytrail-0.1.0/tests/integration/test_why_tracking.py +118 -0
  144. whytrail-0.1.0/tests/plugin_contract/test_aiohttp_plugin.py +74 -0
  145. whytrail-0.1.0/tests/plugin_contract/test_anthropic_plugin.py +64 -0
  146. whytrail-0.1.0/tests/plugin_contract/test_asyncpg_plugin.py +62 -0
  147. whytrail-0.1.0/tests/plugin_contract/test_boto3_plugin.py +61 -0
  148. whytrail-0.1.0/tests/plugin_contract/test_celery_plugin.py +75 -0
  149. whytrail-0.1.0/tests/plugin_contract/test_ddtrace_plugin.py +61 -0
  150. whytrail-0.1.0/tests/plugin_contract/test_django_plugin.py +111 -0
  151. whytrail-0.1.0/tests/plugin_contract/test_dramatiq_plugin.py +72 -0
  152. whytrail-0.1.0/tests/plugin_contract/test_fastapi_plugin.py +88 -0
  153. whytrail-0.1.0/tests/plugin_contract/test_flask_plugin.py +97 -0
  154. whytrail-0.1.0/tests/plugin_contract/test_google_cloud_plugin.py +61 -0
  155. whytrail-0.1.0/tests/plugin_contract/test_grpcio_plugin.py +76 -0
  156. whytrail-0.1.0/tests/plugin_contract/test_httpx_plugin.py +89 -0
  157. whytrail-0.1.0/tests/plugin_contract/test_huggingface_hub_plugin.py +66 -0
  158. whytrail-0.1.0/tests/plugin_contract/test_jsonschema_plugin.py +67 -0
  159. whytrail-0.1.0/tests/plugin_contract/test_langchain_plugin.py +94 -0
  160. whytrail-0.1.0/tests/plugin_contract/test_marshmallow_plugin.py +64 -0
  161. whytrail-0.1.0/tests/plugin_contract/test_openai_plugin.py +70 -0
  162. whytrail-0.1.0/tests/plugin_contract/test_pandas_plugin.py +61 -0
  163. whytrail-0.1.0/tests/plugin_contract/test_polars_plugin.py +57 -0
  164. whytrail-0.1.0/tests/plugin_contract/test_prefect_plugin.py +49 -0
  165. whytrail-0.1.0/tests/plugin_contract/test_pydantic_plugin.py +67 -0
  166. whytrail-0.1.0/tests/plugin_contract/test_pymongo_plugin.py +72 -0
  167. whytrail-0.1.0/tests/plugin_contract/test_pytest_plugin.py +70 -0
  168. whytrail-0.1.0/tests/plugin_contract/test_pyyaml_plugin.py +69 -0
  169. whytrail-0.1.0/tests/plugin_contract/test_redaction_fuzz.py +289 -0
  170. whytrail-0.1.0/tests/plugin_contract/test_requests_plugin.py +77 -0
  171. whytrail-0.1.0/tests/plugin_contract/test_rq_plugin.py +104 -0
  172. whytrail-0.1.0/tests/plugin_contract/test_scrapy_plugin.py +60 -0
  173. whytrail-0.1.0/tests/plugin_contract/test_sentry_plugin.py +114 -0
  174. whytrail-0.1.0/tests/plugin_contract/test_sqlalchemy_plugin.py +91 -0
  175. whytrail-0.1.0/tests/plugin_contract/test_web_concurrency.py +157 -0
  176. whytrail-0.1.0/tests/property/test_graph_invariants.py +57 -0
  177. whytrail-0.1.0/tests/unit/test_explanation.py +93 -0
  178. whytrail-0.1.0/tests/unit/test_graph.py +111 -0
  179. whytrail-0.1.0/tests/unit/test_node.py +38 -0
@@ -0,0 +1,20 @@
1
+ # whytrail run (GitHub Action)
2
+
3
+ Wraps `whytrail run` (see `src/whytrail/cli/__main__.py`) so a CI job's failure
4
+ output shows a causal explanation instead of a bare traceback -- the same
5
+ CLI a developer would run locally, packaged for a workflow step.
6
+
7
+ ```yaml
8
+ - uses: <owner>/whytrail/.github/actions/whytrail-run@main
9
+ with:
10
+ script: scripts/nightly_import.py
11
+ graph: "true" # optional: include a Mermaid graph
12
+ extra-packages: whytrail-requests # optional: plugin distributions the script needs
13
+ ```
14
+
15
+ On an uncaught exception, the step fails (matching `whytrail run`'s own exit
16
+ code) and the explanation appears in the job log at the point of failure --
17
+ no extra step needed to surface it.
18
+
19
+ Inputs: `script` (required), `args`, `python-version` (default `3.x`),
20
+ `graph` (default `false`), `extra-packages`.
@@ -0,0 +1,50 @@
1
+ name: "whytrail run"
2
+ description: "Run a Python script and explain any uncaught exception with whytrail instead of a bare traceback."
3
+ branding:
4
+ icon: "help-circle"
5
+ color: "orange"
6
+
7
+ inputs:
8
+ script:
9
+ description: "Path to the Python script to run"
10
+ required: true
11
+ args:
12
+ description: "Arguments to pass to the script"
13
+ required: false
14
+ default: ""
15
+ python-version:
16
+ description: "Python version to set up before running"
17
+ required: false
18
+ default: "3.x"
19
+ graph:
20
+ description: "Include a Mermaid provenance graph in the explanation output"
21
+ required: false
22
+ default: "false"
23
+ extra-packages:
24
+ description: "Space-separated extra packages to install alongside whytrail (e.g. plugin distributions your script needs, like whytrail-requests)"
25
+ required: false
26
+ default: ""
27
+
28
+ runs:
29
+ using: "composite"
30
+ steps:
31
+ - uses: actions/setup-python@v5
32
+ with:
33
+ python-version: ${{ inputs.python-version }}
34
+
35
+ - name: Install whytrail
36
+ shell: bash
37
+ run: |
38
+ pip install whytrail
39
+ if [ -n "${{ inputs.extra-packages }}" ]; then
40
+ pip install ${{ inputs.extra-packages }}
41
+ fi
42
+
43
+ - name: Run script with whytrail
44
+ shell: bash
45
+ run: |
46
+ FLAGS=""
47
+ if [ "${{ inputs.graph }}" = "true" ]; then
48
+ FLAGS="--graph"
49
+ fi
50
+ whytrail run $FLAGS "${{ inputs.script }}" -- ${{ inputs.args }}
@@ -0,0 +1,333 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, windows-latest, macos-latest]
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ runs-on: ${{ matrix.os }}
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+
23
+ - name: Install whytrail (core + dev)
24
+ run: pip install -e ".[dev]"
25
+
26
+ - name: Type-check with mypy --strict
27
+ run: mypy
28
+
29
+ - name: Run core test suite
30
+ run: pytest tests -q
31
+
32
+ plugin-contract-tests:
33
+ # Each plugin has its own third-party dependency (pandas, sqlalchemy,
34
+ # fastapi, ...); installing all of them into the core matrix above
35
+ # would make every core-only change pay for every plugin's install
36
+ # time. Kept separate so a plugin dependency bump can't block a
37
+ # core-only PR, and vice versa.
38
+ runs-on: ubuntu-latest
39
+ strategy:
40
+ fail-fast: false
41
+ matrix:
42
+ plugin:
43
+ - whytrail-requests
44
+ - whytrail-pytest
45
+ - whytrail-sentry
46
+ - whytrail-pandas
47
+ - whytrail-sqlalchemy
48
+ - whytrail-fastapi
49
+ - whytrail-django
50
+ - whytrail-celery
51
+ - whytrail-langchain
52
+ - whytrail-pydantic
53
+ - whytrail-boto3
54
+ - whytrail-httpx
55
+ - whytrail-openai
56
+ - whytrail-anthropic
57
+ - whytrail-asyncpg
58
+ - whytrail-pymongo
59
+ - whytrail-marshmallow
60
+ - whytrail-jsonschema
61
+ - whytrail-polars
62
+ - whytrail-rq
63
+ - whytrail-dramatiq
64
+ - whytrail-flask
65
+ - whytrail-pyyaml
66
+ - whytrail-grpcio
67
+ - whytrail-scrapy
68
+ - whytrail-aiohttp
69
+ - whytrail-huggingface-hub
70
+ - whytrail-google-cloud
71
+ - whytrail-ddtrace
72
+ - whytrail-prefect
73
+ steps:
74
+ - uses: actions/checkout@v4
75
+
76
+ - uses: actions/setup-python@v5
77
+ with:
78
+ python-version: "3.12"
79
+
80
+ - name: Install whytrail + plugin under test + test deps
81
+ run: |
82
+ pip install -e ".[dev]"
83
+ # [test] is requested for every plugin uniformly; pip just warns
84
+ # ("does not provide the extra") and continues for the ~28 that
85
+ # don't declare one. The two that do (whytrail-fastapi needs the
86
+ # real fastapi package + httpx's TestClient; whytrail-rq needs
87
+ # fakeredis) are test-only tools neither plugin depends on at
88
+ # runtime, so they don't belong in the plugin's own
89
+ # `dependencies` -- found when both failed on the first real CI
90
+ # run with pytest exit 5 ("no tests collected"), since their
91
+ # test files' module-level `pytest.importorskip` guards turned a
92
+ # missing test-only dependency into a silent zero-test skip
93
+ # rather than a visible failure.
94
+ pip install -e "./plugins/${{ matrix.plugin }}[test]"
95
+
96
+ - name: Run this plugin's contract tests
97
+ run: |
98
+ # Strip the "whytrail-" prefix, then convert any remaining
99
+ # dashes to underscores -- needed for plugins like
100
+ # whytrail-google-cloud and whytrail-huggingface-hub, whose test
101
+ # files (test_google_cloud_plugin.py,
102
+ # test_huggingface_hub_plugin.py) use underscores throughout.
103
+ # A prefix-only strip would look for a literal dash in the
104
+ # filename and silently match zero tests.
105
+ name="${{ matrix.plugin }}"
106
+ test_name="${name#whytrail-}"
107
+ test_name="${test_name//-/_}"
108
+ pytest "tests/plugin_contract/test_${test_name}_plugin.py" -q
109
+
110
+ plugin-version-matrix:
111
+ # docs/testing-maturity.md gap #1: every plugin above installs
112
+ # whatever `pip` resolves today, never the specific floor each
113
+ # pyproject.toml claims to support. Checking that floor for real
114
+ # found real version-compatibility bugs in two rounds: first locally
115
+ # in this sandbox (Windows, before this job existed), then again --
116
+ # differently -- on the actual ubuntu-latest runners this job
117
+ # targets, the first time it ever executed for real. Neither round
118
+ # alone would have found everything the other did; several "confirmed
119
+ # fine" conclusions from local-only testing turned out wrong once a
120
+ # real CI run actually exercised each plugin's contract tests against
121
+ # its pinned floor, not just "does it install and import."
122
+ #
123
+ # No 3.13 wheel / build failure (found locally, still holds on Linux):
124
+ # - pydantic==2.0.3: pydantic-core fails to build from source
125
+ # (ForwardRef._evaluate() signature changed). First working: 2.8.0.
126
+ # - pyyaml==6.0: source build fails (Cython/setuptools mismatch).
127
+ # First working: 6.0.2.
128
+ # - grpcio==1.60.0: no 3.13 wheel on any platform. First working: 1.66.2.
129
+ # - pandas==2.0.0: no 3.13 wheel. First working: 2.2.3.
130
+ #
131
+ # Genuine Python 3.13 runtime incompatibility in the dependency's own
132
+ # code, not a packaging gap:
133
+ # - sqlalchemy==2.0.0: TypingOnly assertion predates two dunders
134
+ # 3.13 added to every class. First working: 2.0.36.
135
+ # - asyncpg==0.29.0: pgproto.c calls a C API function with the
136
+ # pre-3.13 argument count. First working: 0.30.0.
137
+ # - sentry-sdk==2.0.0: copies frame locals via copy.copy(), which
138
+ # breaks on 3.13's FrameLocalsProxy (PEP 667) -- passed a local
139
+ # install-and-import check, only failed when its actual capture
140
+ # path ran against a real exception in a real CI run. First
141
+ # working, found by bisection: 2.11.0.
142
+ # - prefect==2.14.0-2.20.0: not fixable within the 2.x line at all --
143
+ # `GatherTaskGroup` (prefect's own asyncio.TaskGroup subclass)
144
+ # doesn't implement an abstract method 3.13's asyncio added,
145
+ # independent of which pydantic/griffe versions are present. The
146
+ # floor moved to the 3.x line entirely: 3.7.8 (earlier 3.x
147
+ # releases have their own pydantic-version-drift issues below).
148
+ #
149
+ # Standard-library removal, unrelated to any single dependency version:
150
+ # - marshmallow==3.0.0: imports `distutils.version.LooseVersion`,
151
+ # removed in 3.12 (PEP 632). First working, by bisection: 3.15.0.
152
+ #
153
+ # Transitive-dependency drift -- the floor's own unpinned/loosely-pinned
154
+ # sub-dependency moved on and broke it, nothing to do with Python 3.13:
155
+ # - requests==2.20.0: imports `urllib3.packages.six.moves`, a shim
156
+ # urllib3 2.0 removed. First working, by bisection: 2.25.0.
157
+ # - flask==2.3.0: testing.py reads `werkzeug.__version__`, which
158
+ # werkzeug 3.x removed. First working: 2.3.3.
159
+ # - prefect (see above): also hit a `pydantic.ConfigWrapper`
160
+ # TypeError against pydantic's newest 2.x releases, and separately
161
+ # a `griffe.Docstring` import removed by a later griffe release --
162
+ # three independent drift issues layered on the same plugin.
163
+ #
164
+ # Plugin/test written against a newer version of the SDK's own API
165
+ # than the stated floor actually provides -- not a Python-version or
166
+ # transitive-drift issue, just an old client library:
167
+ # - ddtrace==2.0.0: `ddtrace.trace.tracer` (a module-level singleton
168
+ # the test imports) didn't exist yet. First working: 2.20.0.
169
+ # - openai==1.0.0: exception `.body` shape at this version doesn't
170
+ # match what the plugin reads. First working, by bisection: 1.30.0.
171
+ # - anthropic==0.30.0: same shape issue. First working: 0.34.0.
172
+ # - huggingface_hub==0.20.0: `huggingface_hub.errors` (the module the
173
+ # plugin imports) didn't exist yet, and even once it did (0.22+),
174
+ # `hf_raise_for_status()`'s response-shape handling didn't match
175
+ # until the 1.x line. First working: 1.0.0 -- a major-version
176
+ # jump, not a patch bump, the same way prefect needed one.
177
+ #
178
+ # Not independently version-testable the way this job assumes:
179
+ # - whytrail-fastapi tests build a real FastAPI app, so `starlette`
180
+ # alone isn't a meaningful axis to pin -- forcing an old starlette
181
+ # floor (0.36.0) against whatever fastapi the test-tools step
182
+ # installs breaks on an unrelated TestClient API mismatch the
183
+ # moment the two are versioned independently. `fastapi` is the
184
+ # pinned dependency instead (floor 0.115.0, the oldest version
185
+ # confirmed to both resolve a starlette satisfying the plugin's
186
+ # real >=0.36 floor *and* pass); the test-tools step below installs
187
+ # only httpx for this plugin now, not fastapi.
188
+ #
189
+ # Every other plugin's stated floor (boto3, httpx, django, celery,
190
+ # dramatiq, rq, langchain-core, google-api-core, pytest, jsonschema,
191
+ # scrapy, polars, aiohttp, pymongo) was checked the same way on real
192
+ # ubuntu-latest CI and confirmed to already work -- not assumed, just
193
+ # not (yet) found broken. See docs/testing-maturity.md for why "found
194
+ # broken so far" and "confirmed working" are different-strength claims,
195
+ # and CHANGELOG.md for the full account of both rounds of findings.
196
+ #
197
+ # This job pins each plugin's *actual* Python-3.13-compatible floor
198
+ # (not the aspirational one in pyproject.toml, which is still correct
199
+ # for older Python versions) against the latest release, so a future
200
+ # dependency bump that reintroduces a gap like this fails CI instead
201
+ # of being discovered by a user.
202
+ # Each entry below is fully explicit (plugin, dependency, floor,
203
+ # dependency-version) rather than a `plugin`x`dependency-version`
204
+ # cross-product via a separate `include:` list. The cross-product
205
+ # form was tried first and is wrong: `include` entries only merge
206
+ # into a base matrix when they share a key with it, and these entries
207
+ # share no key with a bare `dependency-version: [floor, latest]` axis
208
+ # -- so instead of the intended 60 jobs (30 plugins x 2 versions),
209
+ # only 2 generic jobs ran, both missing plugin context entirely. Not
210
+ # caught by local `mkdocs`/`mypy`/`pytest` verification, since none
211
+ # of those parse this workflow file -- only found by watching the
212
+ # first real push actually run it and seeing "plugin-version-matrix
213
+ # (floor)" and "(latest)" instead of 60 named jobs.
214
+ runs-on: ubuntu-latest
215
+ strategy:
216
+ fail-fast: false
217
+ matrix:
218
+ include:
219
+ - { plugin: whytrail-pydantic, dependency: pydantic, floor: "2.8.0", dependency-version: floor }
220
+ - { plugin: whytrail-pydantic, dependency: pydantic, floor: "2.8.0", dependency-version: latest }
221
+ - { plugin: whytrail-sqlalchemy, dependency: sqlalchemy, floor: "2.0.36", dependency-version: floor }
222
+ - { plugin: whytrail-sqlalchemy, dependency: sqlalchemy, floor: "2.0.36", dependency-version: latest }
223
+ - { plugin: whytrail-asyncpg, dependency: asyncpg, floor: "0.30.0", dependency-version: floor }
224
+ - { plugin: whytrail-asyncpg, dependency: asyncpg, floor: "0.30.0", dependency-version: latest }
225
+ - { plugin: whytrail-pyyaml, dependency: pyyaml, floor: "6.0.2", dependency-version: floor }
226
+ - { plugin: whytrail-pyyaml, dependency: pyyaml, floor: "6.0.2", dependency-version: latest }
227
+ - { plugin: whytrail-grpcio, dependency: grpcio, floor: "1.66.2", dependency-version: floor }
228
+ - { plugin: whytrail-grpcio, dependency: grpcio, floor: "1.66.2", dependency-version: latest }
229
+ - { plugin: whytrail-ddtrace, dependency: ddtrace, floor: "2.20.0", dependency-version: floor }
230
+ - { plugin: whytrail-ddtrace, dependency: ddtrace, floor: "2.20.0", dependency-version: latest }
231
+ - { plugin: whytrail-pandas, dependency: pandas, floor: "2.2.3", dependency-version: floor }
232
+ - { plugin: whytrail-pandas, dependency: pandas, floor: "2.2.3", dependency-version: latest }
233
+ - { plugin: whytrail-marshmallow, dependency: marshmallow, floor: "3.15.0", dependency-version: floor }
234
+ - { plugin: whytrail-marshmallow, dependency: marshmallow, floor: "3.15.0", dependency-version: latest }
235
+ - { plugin: whytrail-requests, dependency: requests, floor: "2.25.0", dependency-version: floor }
236
+ - { plugin: whytrail-requests, dependency: requests, floor: "2.25.0", dependency-version: latest }
237
+ - { plugin: whytrail-prefect, dependency: prefect, floor: "3.7.8", dependency-version: floor }
238
+ - { plugin: whytrail-prefect, dependency: prefect, floor: "3.7.8", dependency-version: latest }
239
+ - { plugin: whytrail-polars, dependency: polars, floor: "0.20.0", dependency-version: floor }
240
+ - { plugin: whytrail-polars, dependency: polars, floor: "0.20.0", dependency-version: latest }
241
+ - { plugin: whytrail-aiohttp, dependency: aiohttp, floor: "3.9.0", dependency-version: floor }
242
+ - { plugin: whytrail-aiohttp, dependency: aiohttp, floor: "3.9.0", dependency-version: latest }
243
+ - { plugin: whytrail-boto3, dependency: boto3, floor: "1.34.0", dependency-version: floor }
244
+ - { plugin: whytrail-boto3, dependency: boto3, floor: "1.34.0", dependency-version: latest }
245
+ - { plugin: whytrail-httpx, dependency: httpx, floor: "0.27.0", dependency-version: floor }
246
+ - { plugin: whytrail-httpx, dependency: httpx, floor: "0.27.0", dependency-version: latest }
247
+ - { plugin: whytrail-flask, dependency: flask, floor: "2.3.3", dependency-version: floor }
248
+ - { plugin: whytrail-flask, dependency: flask, floor: "2.3.3", dependency-version: latest }
249
+ - { plugin: whytrail-django, dependency: django, floor: "4.2.0", dependency-version: floor }
250
+ - { plugin: whytrail-django, dependency: django, floor: "4.2.0", dependency-version: latest }
251
+ - { plugin: whytrail-celery, dependency: celery, floor: "5.3.0", dependency-version: floor }
252
+ - { plugin: whytrail-celery, dependency: celery, floor: "5.3.0", dependency-version: latest }
253
+ - { plugin: whytrail-dramatiq, dependency: dramatiq, floor: "1.14.0", dependency-version: floor }
254
+ - { plugin: whytrail-dramatiq, dependency: dramatiq, floor: "1.14.0", dependency-version: latest }
255
+ - { plugin: whytrail-rq, dependency: rq, floor: "1.15.0", dependency-version: floor }
256
+ - { plugin: whytrail-rq, dependency: rq, floor: "1.15.0", dependency-version: latest }
257
+ - { plugin: whytrail-sentry, dependency: sentry-sdk, floor: "2.11.0", dependency-version: floor }
258
+ - { plugin: whytrail-sentry, dependency: sentry-sdk, floor: "2.11.0", dependency-version: latest }
259
+ - { plugin: whytrail-langchain, dependency: langchain-core, floor: "0.2.0", dependency-version: floor }
260
+ - { plugin: whytrail-langchain, dependency: langchain-core, floor: "0.2.0", dependency-version: latest }
261
+ - { plugin: whytrail-huggingface-hub, dependency: huggingface_hub, floor: "1.0.0", dependency-version: floor }
262
+ - { plugin: whytrail-huggingface-hub, dependency: huggingface_hub, floor: "1.0.0", dependency-version: latest }
263
+ - { plugin: whytrail-google-cloud, dependency: google-api-core, floor: "2.15.0", dependency-version: floor }
264
+ - { plugin: whytrail-google-cloud, dependency: google-api-core, floor: "2.15.0", dependency-version: latest }
265
+ - { plugin: whytrail-anthropic, dependency: anthropic, floor: "0.34.0", dependency-version: floor }
266
+ - { plugin: whytrail-anthropic, dependency: anthropic, floor: "0.34.0", dependency-version: latest }
267
+ - { plugin: whytrail-openai, dependency: openai, floor: "1.30.0", dependency-version: floor }
268
+ - { plugin: whytrail-openai, dependency: openai, floor: "1.30.0", dependency-version: latest }
269
+ - { plugin: whytrail-pytest, dependency: pytest, floor: "7.0.0", dependency-version: floor }
270
+ - { plugin: whytrail-pytest, dependency: pytest, floor: "7.0.0", dependency-version: latest }
271
+ - { plugin: whytrail-jsonschema, dependency: jsonschema, floor: "4.0.0", dependency-version: floor }
272
+ - { plugin: whytrail-jsonschema, dependency: jsonschema, floor: "4.0.0", dependency-version: latest }
273
+ - { plugin: whytrail-scrapy, dependency: scrapy, floor: "2.8.0", dependency-version: floor }
274
+ - { plugin: whytrail-scrapy, dependency: scrapy, floor: "2.8.0", dependency-version: latest }
275
+ - { plugin: whytrail-fastapi, dependency: fastapi, floor: "0.115.0", dependency-version: floor }
276
+ - { plugin: whytrail-fastapi, dependency: fastapi, floor: "0.115.0", dependency-version: latest }
277
+ - { plugin: whytrail-pymongo, dependency: pymongo, floor: "4.0.0", dependency-version: floor }
278
+ - { plugin: whytrail-pymongo, dependency: pymongo, floor: "4.0.0", dependency-version: latest }
279
+ steps:
280
+ - uses: actions/checkout@v4
281
+
282
+ - uses: actions/setup-python@v5
283
+ with:
284
+ python-version: "3.13"
285
+
286
+ - name: Install whytrail + plugin (without its dependency pin)
287
+ run: |
288
+ pip install -e ".[dev]"
289
+ pip install -e "./plugins/${{ matrix.plugin }}" --no-deps
290
+
291
+ - name: Install this plugin's test-only tools, if any
292
+ # --no-deps above (deliberately, so the next step controls the
293
+ # dependency-under-test's exact version) also blocks pip from
294
+ # resolving [test]-extra packages via the normal
295
+ # "pkg[test]" syntax, since --no-deps skips all dependency
296
+ # metadata, not just the core `dependencies` list -- so these
297
+ # are installed directly by name instead, same tools as
298
+ # plugin-contract-tests' [test] extra above.
299
+ # whytrail-fastapi installs only httpx here, not fastapi itself --
300
+ # fastapi is the dependency under test for this plugin (see the
301
+ # entries above and the note by them), handled by the next step.
302
+ run: |
303
+ case "${{ matrix.plugin }}" in
304
+ whytrail-fastapi) pip install httpx ;;
305
+ whytrail-rq) pip install fakeredis ;;
306
+ esac
307
+
308
+ - name: Install the dependency at the version under test
309
+ run: |
310
+ if [ "${{ matrix.dependency-version }}" = "floor" ]; then
311
+ pip install "${{ matrix.dependency }}==${{ matrix.floor }}"
312
+ else
313
+ pip install --upgrade "${{ matrix.dependency }}"
314
+ fi
315
+
316
+ - name: Run this plugin's contract tests
317
+ run: |
318
+ name="${{ matrix.plugin }}"
319
+ test_name="${name#whytrail-}"
320
+ test_name="${test_name//-/_}"
321
+ pytest "tests/plugin_contract/test_${test_name}_plugin.py" -q
322
+
323
+ benchmarks:
324
+ runs-on: ubuntu-latest
325
+ steps:
326
+ - uses: actions/checkout@v4
327
+ - uses: actions/setup-python@v5
328
+ with:
329
+ python-version: "3.12"
330
+ - run: pip install -e ".[dev]"
331
+ - name: Run performance benchmarks (informational, not a merge gate yet)
332
+ run: pytest benchmarks/ --benchmark-only
333
+ continue-on-error: true
@@ -0,0 +1,51 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "docs/**"
8
+ - "mkdocs.yml"
9
+ - "README.md"
10
+ - "CHANGELOG.md"
11
+ - ".github/workflows/docs.yml"
12
+ workflow_dispatch:
13
+
14
+ permissions:
15
+ contents: read
16
+ pages: write
17
+ id-token: write
18
+
19
+ concurrency:
20
+ group: pages
21
+ cancel-in-progress: false
22
+
23
+ jobs:
24
+ build:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: "3.12"
32
+
33
+ - name: Install docs dependencies
34
+ run: pip install -e ".[docs]"
35
+
36
+ - name: Build with mkdocs --strict
37
+ run: mkdocs build --strict
38
+
39
+ - uses: actions/upload-pages-artifact@v3
40
+ with:
41
+ path: site
42
+
43
+ deploy:
44
+ needs: build
45
+ runs-on: ubuntu-latest
46
+ environment:
47
+ name: github-pages
48
+ url: ${{ steps.deployment.outputs.page_url }}
49
+ steps:
50
+ - id: deployment
51
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,50 @@
1
+ name: Publish to PyPI
2
+
3
+ # Triggers on a GitHub Release being published, not on every push -- a
4
+ # release is a deliberate, human action, and PyPI publishes are
5
+ # effectively irreversible (a version number can never be reused, even
6
+ # if yanked). Tag the release vX.Y.Z to match pyproject.toml's version.
7
+ on:
8
+ release:
9
+ types: [published]
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Install build tooling
22
+ run: pip install build twine
23
+
24
+ - name: Build sdist and wheel
25
+ run: python -m build
26
+
27
+ - name: Validate metadata
28
+ run: twine check dist/*
29
+
30
+ - uses: actions/upload-artifact@v4
31
+ with:
32
+ name: dist
33
+ path: dist/
34
+
35
+ publish:
36
+ needs: build
37
+ runs-on: ubuntu-latest
38
+ environment: pypi
39
+ permissions:
40
+ # id-token: write is what makes this "trusted publishing" -- PyPI
41
+ # verifies this workflow's OIDC identity directly, no API token
42
+ # stored as a repo secret, nothing to rotate or leak.
43
+ id-token: write
44
+ steps:
45
+ - uses: actions/download-artifact@v4
46
+ with:
47
+ name: dist
48
+ path: dist/
49
+
50
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .hypothesis/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ htmlcov/
15
+ *.egg
16
+ site/
17
+ .claude/
18
+ .playwright-mcp/