hydroid-cli 0.2.0__py3-none-macosx_11_0_arm64.whl
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.
- hydroid_cli-0.2.0.data/scripts/hydroid +0 -0
- hydroid_cli-0.2.0.dist-info/METADATA +196 -0
- hydroid_cli-0.2.0.dist-info/RECORD +7 -0
- hydroid_cli-0.2.0.dist-info/WHEEL +4 -0
- hydroid_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- hydroid_cli-0.2.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +11 -0
- hydroid_cli-0.2.0.dist-info/sboms/hydroid.cyclonedx.json +7820 -0
|
Binary file
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hydroid-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Environment :: Console
|
|
6
|
+
Classifier: Framework :: FastAPI
|
|
7
|
+
Classifier: Framework :: AsyncIO
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Rust
|
|
10
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
13
|
+
Summary: Finds blocking calls reachable on the event loop in async FastAPI code.
|
|
14
|
+
Keywords: fastapi,asyncio,static-analysis,linter,event-loop
|
|
15
|
+
License-Expression: MIT
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
18
|
+
Project-URL: Documentation, https://github.com/agusmdev/hydroid/blob/main/docs/GUIDE.md
|
|
19
|
+
Project-URL: Repository, https://github.com/agusmdev/hydroid
|
|
20
|
+
|
|
21
|
+
# hydroid
|
|
22
|
+
|
|
23
|
+
Finds blocking calls that run on the event loop in async FastAPI (and any asyncio) code — at any
|
|
24
|
+
call depth, across modules, through callbacks, decorators, context managers, properties and
|
|
25
|
+
subclass overrides — and shows the whole chain.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
error[blocking-http]: `requests.api.get` blocks the event loop
|
|
29
|
+
--> app/main.py:12:12 in `app.main.get_user`
|
|
30
|
+
= reached from route GET /users/{user_id}: app.main.get_user (app/main.py:11)
|
|
31
|
+
= blocking chain (3 calls):
|
|
32
|
+
app/main.py:12:12 services.load -> app.services.load
|
|
33
|
+
app/services.py:7:22 fetch -> app.repo.fetch
|
|
34
|
+
app/repo.py:5:16 http_get -> requests.api.get
|
|
35
|
+
= help: use an async client (`httpx.AsyncClient`, `aiohttp`) or `await asyncio.to_thread(...)`
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**New here? Read the [usage guide](docs/GUIDE.md).** Adding it to CI, or letting a coding agent
|
|
39
|
+
set it up: [CI/CD and agent setup](#cicd-and-agent-setup).
|
|
40
|
+
|
|
41
|
+
Written in Rust on top of [ty](https://github.com/astral-sh/ty)'s semantic model (types, imports,
|
|
42
|
+
go-to-definition). Polar's server (1,854 files, 108k call sites) takes about as long as ty needs
|
|
43
|
+
to type-check it the first time (~10 s on 4 vCPUs), then ~0.35 s while nothing changed: extracted
|
|
44
|
+
facts are cached in `.hydroid_cache/`. `HYDROID_TIMINGS=1` prints where the time goes.
|
|
45
|
+
|
|
46
|
+
## Install and run
|
|
47
|
+
|
|
48
|
+
With [uv](https://docs.astral.sh/uv/), using the prebuilt binaries of the latest release
|
|
49
|
+
(macOS, Linux, Windows; no Rust needed):
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
uvx --from hydroid-cli --find-links https://github.com/agusmdev/hydroid/releases/expanded_assets/v0.1.0 hydroid .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or straight from the source on `main` (builds once with a Rust toolchain, then cached):
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
uvx --from git+https://github.com/agusmdev/hydroid hydroid .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
To install the `hydroid` command permanently, replace `uvx --from` with `uv tool install`
|
|
62
|
+
(e.g. `uv tool install git+https://github.com/agusmdev/hydroid`), then run `hydroid .` anywhere.
|
|
63
|
+
From a clone: `cargo install --path crates/hydroid`. More in the [guide](docs/GUIDE.md#1-install).
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
hydroid path/to/project # finds .venv / VIRTUAL_ENV like ty
|
|
67
|
+
hydroid . --python .venv --format sarif > hydroid.sarif
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Flag | |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `--python PATH` | virtualenv or interpreter to resolve imports (default: discovered) |
|
|
73
|
+
| `--follow-libs` | also analyze third-party function bodies |
|
|
74
|
+
| `--cpu` | also report CPU-bound calls (bcrypt, pbkdf2, scrypt) |
|
|
75
|
+
| `--strict` | report calls on the loop whose target is unknown, and fail on them |
|
|
76
|
+
| `--exclude GLOB` | skip files (repeatable) |
|
|
77
|
+
| `--no-cache` | do not read or write `.hydroid_cache/` |
|
|
78
|
+
| `--format human\|json\|sarif` | output |
|
|
79
|
+
|
|
80
|
+
Exit status: `0` clean, `1` findings (or unresolved calls with `--strict`), `2` error.
|
|
81
|
+
Suppress a line with `# hydroid: ignore`.
|
|
82
|
+
|
|
83
|
+
Configuration in `pyproject.toml` (flags win):
|
|
84
|
+
|
|
85
|
+
```toml
|
|
86
|
+
[tool.hydroid]
|
|
87
|
+
python = ".venv"
|
|
88
|
+
exclude = ["migrations/**"]
|
|
89
|
+
strict = false
|
|
90
|
+
|
|
91
|
+
[[tool.hydroid.sink]] # your own blocking functions (defining qualified names)
|
|
92
|
+
category = "sdk"
|
|
93
|
+
advice = "use the async client"
|
|
94
|
+
functions = ["acme.client.Client.fetch"]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## CI/CD and agent setup
|
|
98
|
+
|
|
99
|
+
hydroid exits `1` when it finds blocking calls, so it can gate a build as is. On GitHub,
|
|
100
|
+
`--format sarif` puts each blocking chain on the pull request through code scanning:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
# .github/workflows/hydroid.yml
|
|
104
|
+
name: hydroid
|
|
105
|
+
on: [push, pull_request]
|
|
106
|
+
jobs:
|
|
107
|
+
hydroid:
|
|
108
|
+
runs-on: ubuntu-latest
|
|
109
|
+
permissions:
|
|
110
|
+
security-events: write
|
|
111
|
+
steps:
|
|
112
|
+
- uses: actions/checkout@v4
|
|
113
|
+
- uses: astral-sh/setup-uv@v6
|
|
114
|
+
- run: uv sync # hydroid needs the project's dependencies installed
|
|
115
|
+
- run: >-
|
|
116
|
+
uvx --from hydroid-cli --find-links https://github.com/agusmdev/hydroid/releases/expanded_assets/v0.1.0
|
|
117
|
+
hydroid . --format sarif > hydroid.sarif || [ $? -eq 1 ]
|
|
118
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
119
|
+
with:
|
|
120
|
+
sarif_file: hydroid.sarif
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`|| [ $? -eq 1 ]` reports findings without failing the job (exit `2`, an error, still fails it);
|
|
124
|
+
drop it and the upload step to fail the build instead.
|
|
125
|
+
|
|
126
|
+
[docs/AGENT_SETUP.md](docs/AGENT_SETUP.md) has:
|
|
127
|
+
|
|
128
|
+
- **a prompt to paste into a coding agent** (Claude Code, Cursor, Codex…) that installs hydroid
|
|
129
|
+
in your repository, configures `[tool.hydroid]`, triages the existing findings (fix,
|
|
130
|
+
`# hydroid: ignore` with a reason, or report to you) and adds the CI job;
|
|
131
|
+
- configs for GitLab CI, pre-commit, monorepos, projects not using uv, and other CI systems;
|
|
132
|
+
- a path for adopting it on a code base with existing findings: report-only first, then gate.
|
|
133
|
+
|
|
134
|
+
## How it works
|
|
135
|
+
|
|
136
|
+
1. **Facts** (`crates/hydroid_ty`, the only crate that touches ty): every function body is walked
|
|
137
|
+
in parallel; each call is resolved to its definitions through ty (types, then
|
|
138
|
+
go-to-definition), including implicit calls (`with`, `for` and comprehensions, properties
|
|
139
|
+
and their setters, descriptors, operators like `x[k]` and `k in x` on user classes,
|
|
140
|
+
`__getattr__`, iterators' `__next__`, constructors with `__post_init__` and pydantic
|
|
141
|
+
validators, builtins and `contextlib` helpers that iterate, enter or call their arguments like
|
|
142
|
+
`sorted(xs, key=f)` or `stack.enter_context(cm)`, `singledispatch` implementations). Callables passed as arguments are recorded as flows
|
|
143
|
+
(`to_thread(f)`, `Depends(dep)`, `on_startup=[f]`, decorators), and so are callables stored
|
|
144
|
+
on `self` and returned by functions. When ty does not know a value's type because it comes
|
|
145
|
+
from an unannotated function, the class that function returns is used. The facts of an unchanged project are reused from `.hydroid_cache/` (keyed by the
|
|
146
|
+
binary, the options, the installed packages and every source file's contents).
|
|
147
|
+
2. **Analysis** (`crates/hydroid_core`, plain graph code): where code runs is a property of the
|
|
148
|
+
call, so "does calling `f` block?" is computed once per function by a backwards BFS from
|
|
149
|
+
blocking functions — shortest witness chains, no recursion, any depth. Diagnostics are the
|
|
150
|
+
call sites inside loop code (`async def` bodies, sync callbacks scheduled on the loop) whose
|
|
151
|
+
callee blocks. Offload APIs cut the graph; callbacks are attributed to the call site passing
|
|
152
|
+
them; overrides are found by class-hierarchy analysis (test doubles only stand in for real
|
|
153
|
+
classes in test code); calling a generator function runs nothing until the generator is
|
|
154
|
+
iterated; a decorated function is reached through its decorator's wrappers only when they call
|
|
155
|
+
it. Starting an event loop (`asyncio.run`) is only reported when loop code does it directly:
|
|
156
|
+
sync bridges reaching it almost always check for a running loop first.
|
|
157
|
+
3. **Catalog** (`crates/hydroid_core/catalog.toml`): blocking functions, offload APIs, loop
|
|
158
|
+
callback APIs and FastAPI entry points, by defining qualified name. A test checks every name
|
|
159
|
+
exists in a real environment.
|
|
160
|
+
|
|
161
|
+
Sync `def` endpoints and dependencies are not reported: FastAPI runs them in a threadpool.
|
|
162
|
+
|
|
163
|
+
## Limits
|
|
164
|
+
|
|
165
|
+
Python is dynamic: calls through `getattr`, untyped parameters, or containers of callables cannot
|
|
166
|
+
always be resolved. They are never silently dropped — `--strict` lists every one reachable on the
|
|
167
|
+
loop. Not modeled yet: `getattr(obj, "name")`, containers of callables (`HANDLERS[kind]()`),
|
|
168
|
+
metaclasses, and `.pyi`-only libraries under `--follow-libs`. A change to any source file re-extracts the whole
|
|
169
|
+
project (no per-file incremental cache yet).
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
cargo test --workspace # needs `uv`; syncs tests/fixtures/.venv from uv.lock
|
|
175
|
+
scripts/corpus.sh [names] # Polar, mealie, lnbits, dispatch, litellm, open-webui, template
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Fixture cases live in `tests/fixtures/cases/*`; expectations are inline comments:
|
|
179
|
+
`# expect: time.sleep via route GET /x`, `# expect: *` (any blocking function), `# expect-unresolved`.
|
|
180
|
+
|
|
181
|
+
`tests/adversarial/` is a suite of cases written to break hydroid, split into a training half
|
|
182
|
+
(required to pass, by `cargo test`) and a held-out half that is only scored:
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
scripts/hillclimb/score.py --split train # misses and false positives, per case
|
|
186
|
+
scripts/hillclimb/score.py --split holdout --quiet # aggregate only: don't tune on it
|
|
187
|
+
scripts/hillclimb/polar_time.sh target/release/hydroid 5
|
|
188
|
+
uv run --with anthropic scripts/hillclimb/adversary.py --count 5 # Claude proposes new cases
|
|
189
|
+
scripts/hillclimb/oracle.py PROJECT report.json # naive syntactic scan: candidate misses
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
ty's crates are pinned to exact versions (`=0.0.15`); they have no API stability promise.
|
|
193
|
+
The bundled typeshed stubs are Apache-2.0; hydroid is MIT.
|
|
194
|
+
|
|
195
|
+
Releases: [docs/RELEASING.md](docs/RELEASING.md).
|
|
196
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
hydroid_cli-0.2.0.data/scripts/hydroid,sha256=xYeGz_9kvqPb_UZdTBZVT-F8OG2xHw3sOC-mfbTMCLU,26604064
|
|
2
|
+
hydroid_cli-0.2.0.dist-info/METADATA,sha256=aBT5l-BB0OK_aTa0uZ_7O6DryzRalL_VO0cIVEABvOM,9416
|
|
3
|
+
hydroid_cli-0.2.0.dist-info/WHEEL,sha256=U927pSLvWHh5eaqcN5v72CdMm-HUWF344fKLe8HhSOU,102
|
|
4
|
+
hydroid_cli-0.2.0.dist-info/licenses/LICENSE,sha256=UCIcT_2F8ZS7HU1Jssodt7jfJ2HAS4GRLbynLWhLMBM,1065
|
|
5
|
+
hydroid_cli-0.2.0.dist-info/licenses/THIRD_PARTY_NOTICES.md,sha256=iegYl2khF69I5QFo7NpkZ3swHKUIFCipPTUGbtl5zHA,607
|
|
6
|
+
hydroid_cli-0.2.0.dist-info/sboms/hydroid.cyclonedx.json,sha256=Doenf7-yjk9EQxdwmv1jScGQxjegIq4bFTKQ7g5DcX4,255027
|
|
7
|
+
hydroid_cli-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agusmdev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
hydroid links the following and ships them inside its binary:
|
|
4
|
+
|
|
5
|
+
- **ruff / ty crates** (`ruff_db`, `ruff_python_ast`, `ty_python_semantic`, …) — MIT,
|
|
6
|
+
Copyright (c) 2022 Charles Marsh. <https://github.com/astral-sh/ruff>
|
|
7
|
+
- **typeshed** stub files, bundled by `ty_vendored` — Apache License 2.0,
|
|
8
|
+
Copyright the typeshed contributors. <https://github.com/python/typeshed>
|
|
9
|
+
Full license text: <https://github.com/python/typeshed/blob/main/LICENSE>
|
|
10
|
+
- Other Rust dependencies are listed in `Cargo.lock`; their licenses (MIT and/or Apache-2.0)
|
|
11
|
+
can be listed with `cargo license`.
|