stemtrace 0.2.0__tar.gz → 0.2.2__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.
- {stemtrace-0.2.0 → stemtrace-0.2.2}/.bumpversion.toml +15 -1
- {stemtrace-0.2.0 → stemtrace-0.2.2}/.github/workflows/ci.yml +39 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/CHANGELOG.md +13 -1
- {stemtrace-0.2.0 → stemtrace-0.2.2}/Makefile +3 -3
- {stemtrace-0.2.0 → stemtrace-0.2.2}/PKG-INFO +3 -2
- {stemtrace-0.2.0 → stemtrace-0.2.2}/README.md +1 -1
- {stemtrace-0.2.0 → stemtrace-0.2.2}/pyproject.toml +4 -1
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/__init__.py +1 -1
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/core/events.py +28 -2
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/signals.py +75 -1
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/api/routes.py +13 -4
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/consumer.py +1 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/store.py +22 -1
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/package.json +1 -1
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/test_fastapi_integration.py +48 -16
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_init.py +9 -3
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_routes.py +40 -10
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_signals.py +57 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/uv.lock +1 -1
- {stemtrace-0.2.0 → stemtrace-0.2.2}/.dockerignore +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/.github/dependabot.yml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/.github/workflows/e2e.yml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/.github/workflows/release.yml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/.gitignore +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/.pre-commit-config.yaml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/.python-version +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/CONTRIBUTING.md +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/Dockerfile +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/Dockerfile.e2e +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/LICENSE +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/build_ui.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/docker-compose.e2e.yml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/docker-compose.yml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/docs/screenshots/task_details.png +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/docs/screenshots/unregistered.png +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/docs/screenshots/workflow.png +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/examples/celery_app.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/examples/fastapi_integration.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/examples/with_auth.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/progress.md +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/core/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/core/exceptions.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/core/graph.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/core/ports.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/bootsteps.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/config.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/scrubbing.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/transports/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/transports/memory.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/transports/redis.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/py.typed +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/__main__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/api/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/api/schemas.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/api/websocket.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/fastapi/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/fastapi/auth.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/fastapi/extension.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/fastapi/router.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/biome.json +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/index.html +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/package-lock.json +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/playwright.config.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/postcss.config.js +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/api/client.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/api/queries.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/DateRangePicker.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/TaskStateBadge.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/index.css +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/main.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/__root.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/graphs.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/index.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/workers.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/utils/format.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/vite-env.d.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/graphs.spec.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/registry.spec.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/tasks.spec.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/workers.spec.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tsconfig.json +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tsconfig.tsbuildinfo +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/vite.config.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/static.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/websocket.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/conftest.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/e2e/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/e2e/conftest.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/e2e/tasks.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/e2e/test_api_e2e.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/e2e/test_workers_e2e.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/test_embedded_static.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/test_redis_transport.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/test_static_ui.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/test_workers_refresh.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_auth.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_bootsteps.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_config.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_consumer.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_events.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_examples.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_exceptions.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_graph.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_scrubbing.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_store.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_transports.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_websocket.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[tool.bumpversion]
|
|
2
|
-
current_version = "0.2.
|
|
2
|
+
current_version = "0.2.2"
|
|
3
3
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
4
4
|
serialize = ["{major}.{minor}.{patch}"]
|
|
5
5
|
commit = true
|
|
@@ -24,6 +24,14 @@ filename = "src/stemtrace/server/ui/frontend/package.json"
|
|
|
24
24
|
search = '"version": "{current_version}"'
|
|
25
25
|
replace = '"version": "{new_version}"'
|
|
26
26
|
|
|
27
|
+
# uv.lock - keep workspace lockfile in sync (uv stores the local project version)
|
|
28
|
+
[[tool.bumpversion.files]]
|
|
29
|
+
filename = "uv.lock"
|
|
30
|
+
search = """name = "stemtrace"
|
|
31
|
+
version = "{current_version}"""
|
|
32
|
+
replace = """name = "stemtrace"
|
|
33
|
+
version = "{new_version}"""
|
|
34
|
+
|
|
27
35
|
# CHANGELOG.md - insert new version header
|
|
28
36
|
[[tool.bumpversion.files]]
|
|
29
37
|
filename = "CHANGELOG.md"
|
|
@@ -31,3 +39,9 @@ search = "## [Unreleased]"
|
|
|
31
39
|
replace = """## [Unreleased]
|
|
32
40
|
|
|
33
41
|
## [{new_version}] - {now:%Y-%m-%d}"""
|
|
42
|
+
|
|
43
|
+
# README.md - keep PyPI badge in sync with version (avoid cached badge drift)
|
|
44
|
+
[[tool.bumpversion.files]]
|
|
45
|
+
filename = "README.md"
|
|
46
|
+
search = "[](https://pypi.org/project/stemtrace)"
|
|
47
|
+
replace = "[](https://pypi.org/project/stemtrace)"
|
|
@@ -3,6 +3,9 @@ name: CI
|
|
|
3
3
|
on:
|
|
4
4
|
pull_request:
|
|
5
5
|
branches: [main]
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
6
9
|
|
|
7
10
|
concurrency:
|
|
8
11
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
@@ -10,6 +13,7 @@ concurrency:
|
|
|
10
13
|
|
|
11
14
|
jobs:
|
|
12
15
|
lint:
|
|
16
|
+
if: github.event_name == 'pull_request'
|
|
13
17
|
runs-on: ubuntu-latest
|
|
14
18
|
steps:
|
|
15
19
|
- uses: actions/checkout@v6
|
|
@@ -34,6 +38,7 @@ jobs:
|
|
|
34
38
|
run: uv run ruff format --check src/ tests/
|
|
35
39
|
|
|
36
40
|
type-check:
|
|
41
|
+
if: github.event_name == 'pull_request'
|
|
37
42
|
runs-on: ubuntu-latest
|
|
38
43
|
steps:
|
|
39
44
|
- uses: actions/checkout@v6
|
|
@@ -55,6 +60,7 @@ jobs:
|
|
|
55
60
|
run: uv run mypy src/ --strict
|
|
56
61
|
|
|
57
62
|
test:
|
|
63
|
+
if: github.event_name == 'pull_request'
|
|
58
64
|
runs-on: ubuntu-latest
|
|
59
65
|
strategy:
|
|
60
66
|
matrix:
|
|
@@ -87,6 +93,7 @@ jobs:
|
|
|
87
93
|
fail_ci_if_error: false
|
|
88
94
|
|
|
89
95
|
frontend:
|
|
96
|
+
if: github.event_name == 'pull_request'
|
|
90
97
|
runs-on: ubuntu-latest
|
|
91
98
|
defaults:
|
|
92
99
|
run:
|
|
@@ -111,6 +118,7 @@ jobs:
|
|
|
111
118
|
run: npm run typecheck
|
|
112
119
|
|
|
113
120
|
build:
|
|
121
|
+
if: github.event_name == 'pull_request'
|
|
114
122
|
runs-on: ubuntu-latest
|
|
115
123
|
needs: [lint, type-check, test, frontend]
|
|
116
124
|
steps:
|
|
@@ -148,3 +156,34 @@ jobs:
|
|
|
148
156
|
with:
|
|
149
157
|
name: dist
|
|
150
158
|
path: dist/
|
|
159
|
+
|
|
160
|
+
coverage-main:
|
|
161
|
+
# Keep PR checks as the main gate, but ensure Codecov gets updated for the
|
|
162
|
+
# actual default-branch commit after merges (or direct pushes / releases).
|
|
163
|
+
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
|
164
|
+
runs-on: ubuntu-latest
|
|
165
|
+
steps:
|
|
166
|
+
- uses: actions/checkout@v6
|
|
167
|
+
|
|
168
|
+
- name: Set up Python
|
|
169
|
+
uses: actions/setup-python@v6
|
|
170
|
+
with:
|
|
171
|
+
python-version: "3.12"
|
|
172
|
+
|
|
173
|
+
- name: Install uv
|
|
174
|
+
uses: astral-sh/setup-uv@v7
|
|
175
|
+
with:
|
|
176
|
+
enable-cache: true
|
|
177
|
+
|
|
178
|
+
- name: Install dependencies
|
|
179
|
+
run: uv sync --all-extras
|
|
180
|
+
|
|
181
|
+
- name: Run tests (coverage only)
|
|
182
|
+
run: uv run pytest --cov=stemtrace --cov-branch --cov-report=xml --cov-fail-under=95
|
|
183
|
+
|
|
184
|
+
- name: Upload coverage (main)
|
|
185
|
+
uses: codecov/codecov-action@v5
|
|
186
|
+
with:
|
|
187
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
188
|
+
files: ./coverage.xml
|
|
189
|
+
fail_ci_if_error: false
|
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.2] - 2026-01-03
|
|
11
|
+
|
|
12
|
+
## [0.2.1] - 2026-01-03
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Task Registry now includes task definition metadata from workers (module, signature, docstring, bound)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- README: updated PyPI badge styling
|
|
19
|
+
- CI: upload Codecov coverage on `main`
|
|
20
|
+
|
|
10
21
|
## [0.2.0] - 2026-01-03
|
|
11
22
|
|
|
12
23
|
### Added
|
|
@@ -50,7 +61,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
50
61
|
- **E2E test suite**: Docker API tests + Playwright browser tests
|
|
51
62
|
- **Comprehensive test suite**: 350+ Python tests, 90%+ coverage
|
|
52
63
|
|
|
53
|
-
[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.2.
|
|
64
|
+
[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.2.1...HEAD
|
|
65
|
+
[0.2.1]: https://github.com/iansokolskyi/stemtrace/compare/v0.2.0...v0.2.1
|
|
54
66
|
[0.2.0]: https://github.com/iansokolskyi/stemtrace/compare/v0.1.1...v0.2.0
|
|
55
67
|
[0.1.1]: https://github.com/iansokolskyi/stemtrace/compare/v0.1.0...v0.1.1
|
|
56
68
|
[0.1.0]: https://github.com/iansokolskyi/stemtrace/releases/tag/v0.1.0
|
|
@@ -126,7 +126,7 @@ bump-patch:
|
|
|
126
126
|
@NEW_VER=$$(uv run bump-my-version show current_version); \
|
|
127
127
|
echo "✅ Version bumped to $$NEW_VER"; \
|
|
128
128
|
echo ""; \
|
|
129
|
-
echo "Next:
|
|
129
|
+
echo "Next: make release"
|
|
130
130
|
|
|
131
131
|
# Bump minor version (0.1.0 -> 0.2.0)
|
|
132
132
|
bump-minor:
|
|
@@ -134,7 +134,7 @@ bump-minor:
|
|
|
134
134
|
@NEW_VER=$$(uv run bump-my-version show current_version); \
|
|
135
135
|
echo "✅ Version bumped to $$NEW_VER"; \
|
|
136
136
|
echo ""; \
|
|
137
|
-
echo "Next:
|
|
137
|
+
echo "Next: make release"
|
|
138
138
|
|
|
139
139
|
# Bump major version (0.1.0 -> 1.0.0)
|
|
140
140
|
bump-major:
|
|
@@ -142,7 +142,7 @@ bump-major:
|
|
|
142
142
|
@NEW_VER=$$(uv run bump-my-version show current_version); \
|
|
143
143
|
echo "✅ Version bumped to $$NEW_VER"; \
|
|
144
144
|
echo ""; \
|
|
145
|
-
echo "Next:
|
|
145
|
+
echo "Next: make release"
|
|
146
146
|
|
|
147
147
|
# Tag and push to trigger release workflow
|
|
148
148
|
release:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stemtrace
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A lightweight Celery task flow visualizer
|
|
5
5
|
Project-URL: Homepage, https://github.com/iansokolskyi/stemtrace
|
|
6
6
|
Project-URL: Documentation, https://github.com/iansokolskyi/stemtrace#readme
|
|
@@ -8,6 +8,7 @@ Project-URL: Repository, https://github.com/iansokolskyi/stemtrace
|
|
|
8
8
|
Project-URL: Issues, https://github.com/iansokolskyi/stemtrace/issues
|
|
9
9
|
Project-URL: Changelog, https://github.com/iansokolskyi/stemtrace/blob/main/CHANGELOG.md
|
|
10
10
|
Author-email: Ian Sokolskyi <iansokolskyi@gmail.com>
|
|
11
|
+
Maintainer-email: Ian Sokolskyi <iansokolskyi@gmail.com>
|
|
11
12
|
License: MIT
|
|
12
13
|
License-File: LICENSE
|
|
13
14
|
Keywords: celery,monitoring,queue,task,visualization,workflow
|
|
@@ -55,7 +56,7 @@ Description-Content-Type: text/markdown
|
|
|
55
56
|
|
|
56
57
|
**Zero-infrastructure Celery task flow visualizer**
|
|
57
58
|
|
|
58
|
-
[](https://pypi.org/project/stemtrace)
|
|
59
60
|
[](https://pypi.org/project/stemtrace/)
|
|
60
61
|
[](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
|
|
61
62
|
[](https://codecov.io/gh/iansokolskyi/stemtrace)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Zero-infrastructure Celery task flow visualizer**
|
|
4
4
|
|
|
5
|
-
[](https://pypi.org/project/stemtrace)
|
|
6
6
|
[](https://pypi.org/project/stemtrace/)
|
|
7
7
|
[](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
|
|
8
8
|
[](https://codecov.io/gh/iansokolskyi/stemtrace)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "stemtrace"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "A lightweight Celery task flow visualizer"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -8,6 +8,9 @@ requires-python = ">=3.10"
|
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "Ian Sokolskyi", email = "iansokolskyi@gmail.com" },
|
|
10
10
|
]
|
|
11
|
+
maintainers = [
|
|
12
|
+
{ name = "Ian Sokolskyi", email = "iansokolskyi@gmail.com" },
|
|
13
|
+
]
|
|
11
14
|
keywords = ["celery", "task", "queue", "visualization", "monitoring", "workflow"]
|
|
12
15
|
classifiers = [
|
|
13
16
|
"Development Status :: 3 - Alpha",
|
|
@@ -4,7 +4,7 @@ from datetime import datetime
|
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class TaskState(str, Enum):
|
|
@@ -27,6 +27,30 @@ class WorkerEventType(str, Enum):
|
|
|
27
27
|
WORKER_SHUTDOWN = "worker_shutdown"
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
class RegisteredTaskDefinition(BaseModel):
|
|
31
|
+
"""Celery task definition metadata captured from workers.
|
|
32
|
+
|
|
33
|
+
This data is collected from a running worker's task registry and is used
|
|
34
|
+
to enrich the task registry API (`/api/tasks/registry`) with human-friendly
|
|
35
|
+
information like docstrings and signatures.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
name: Fully qualified task name (e.g., "myapp.tasks.add").
|
|
39
|
+
module: Python module where the task implementation lives.
|
|
40
|
+
signature: Human-readable call signature (best-effort).
|
|
41
|
+
docstring: Task documentation (best-effort).
|
|
42
|
+
bound: Whether the task is bound (`bind=True`) and receives `self`.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
model_config = ConfigDict(frozen=True)
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
module: str | None = None
|
|
49
|
+
signature: str | None = None
|
|
50
|
+
docstring: str | None = None
|
|
51
|
+
bound: bool = False
|
|
52
|
+
|
|
53
|
+
|
|
30
54
|
class TaskEvent(BaseModel):
|
|
31
55
|
"""Immutable task lifecycle event.
|
|
32
56
|
|
|
@@ -95,11 +119,13 @@ class WorkerEvent(BaseModel):
|
|
|
95
119
|
hostname: str
|
|
96
120
|
pid: int
|
|
97
121
|
timestamp: datetime
|
|
98
|
-
registered_tasks: list[str] =
|
|
122
|
+
registered_tasks: list[str] = Field(default_factory=list)
|
|
123
|
+
task_definitions: dict[str, RegisteredTaskDefinition] = Field(default_factory=dict)
|
|
99
124
|
shutdown_time: datetime | None = None
|
|
100
125
|
|
|
101
126
|
|
|
102
127
|
__all__ = [
|
|
128
|
+
"RegisteredTaskDefinition",
|
|
103
129
|
"TaskEvent",
|
|
104
130
|
"TaskState",
|
|
105
131
|
"WorkerEvent",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Celery signal handlers for task lifecycle events."""
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
import logging
|
|
4
5
|
import threading
|
|
5
6
|
import traceback as tb_module
|
|
@@ -17,7 +18,7 @@ from celery.signals import (
|
|
|
17
18
|
worker_shutdown,
|
|
18
19
|
)
|
|
19
20
|
|
|
20
|
-
from stemtrace.core.events import TaskEvent, TaskState
|
|
21
|
+
from stemtrace.core.events import RegisteredTaskDefinition, TaskEvent, TaskState
|
|
21
22
|
from stemtrace.core.ports import EventTransport
|
|
22
23
|
from stemtrace.library.config import get_config
|
|
23
24
|
from stemtrace.library.scrubbing import (
|
|
@@ -37,6 +38,9 @@ _transport: EventTransport | None = None
|
|
|
37
38
|
_pending_emitted: set[str] = set()
|
|
38
39
|
_pending_emitted_lock = threading.RLock()
|
|
39
40
|
|
|
41
|
+
# Guard against overly-large docstrings bloating broker events.
|
|
42
|
+
_MAX_DOCSTRING_CHARS = 4000
|
|
43
|
+
|
|
40
44
|
|
|
41
45
|
def _extract_chord_info(chord_attr: Any) -> tuple[str | None, str | None]:
|
|
42
46
|
"""Extract chord group_id and callback task_id from chord attribute.
|
|
@@ -495,6 +499,74 @@ def _extract_registered_tasks(sender: Any) -> list[str]:
|
|
|
495
499
|
return task_names
|
|
496
500
|
|
|
497
501
|
|
|
502
|
+
def _extract_task_definitions(sender: Any) -> dict[str, RegisteredTaskDefinition]:
|
|
503
|
+
"""Extract task metadata (docstring/signature/module/bound) from Celery app.
|
|
504
|
+
|
|
505
|
+
This runs on the worker in response to `worker_ready`, so we have access to
|
|
506
|
+
the actual Task objects. The server generally does not import user task code,
|
|
507
|
+
so this is the authoritative source for registry metadata.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
sender: Worker instance (WorkController) from worker_ready signal.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Mapping of task name to RegisteredTaskDefinition.
|
|
514
|
+
"""
|
|
515
|
+
definitions: dict[str, RegisteredTaskDefinition] = {}
|
|
516
|
+
|
|
517
|
+
app = getattr(sender, "app", None)
|
|
518
|
+
if app is None:
|
|
519
|
+
return definitions
|
|
520
|
+
|
|
521
|
+
tasks_registry = getattr(app, "tasks", None)
|
|
522
|
+
if tasks_registry is None:
|
|
523
|
+
return definitions
|
|
524
|
+
|
|
525
|
+
# tasks_registry is dict-like: {name: Task}
|
|
526
|
+
for name, task_obj in getattr(tasks_registry, "items", lambda: [])():
|
|
527
|
+
if not isinstance(name, str) or name.startswith("celery."):
|
|
528
|
+
continue
|
|
529
|
+
if task_obj is None:
|
|
530
|
+
continue
|
|
531
|
+
|
|
532
|
+
run_callable = getattr(task_obj, "run", None)
|
|
533
|
+
module: str | None = None
|
|
534
|
+
docstring: str | None = None
|
|
535
|
+
signature: str | None = None
|
|
536
|
+
|
|
537
|
+
# Best-effort: prefer module/docstring from the task implementation (run).
|
|
538
|
+
if run_callable is not None:
|
|
539
|
+
module = getattr(run_callable, "__module__", None)
|
|
540
|
+
docstring = inspect.getdoc(run_callable)
|
|
541
|
+
try:
|
|
542
|
+
signature = str(inspect.signature(run_callable))
|
|
543
|
+
except (TypeError, ValueError):
|
|
544
|
+
signature = None
|
|
545
|
+
|
|
546
|
+
# Fall back to task object/module if needed.
|
|
547
|
+
if module is None:
|
|
548
|
+
module = getattr(task_obj, "__module__", None)
|
|
549
|
+
|
|
550
|
+
# Bound tasks are declared via @app.task(bind=True). Celery exposes this
|
|
551
|
+
# on the Task instance as `bind`. Only treat an actual bool as authoritative
|
|
552
|
+
# to avoid MagicMock truthiness in tests.
|
|
553
|
+
bind_attr = getattr(task_obj, "bind", None)
|
|
554
|
+
bound = bind_attr if isinstance(bind_attr, bool) else False
|
|
555
|
+
|
|
556
|
+
if docstring is not None and len(docstring) > _MAX_DOCSTRING_CHARS:
|
|
557
|
+
docstring = docstring[:_MAX_DOCSTRING_CHARS] + "\n\n[truncated]"
|
|
558
|
+
|
|
559
|
+
definitions[name] = RegisteredTaskDefinition(
|
|
560
|
+
name=name,
|
|
561
|
+
module=module,
|
|
562
|
+
signature=signature,
|
|
563
|
+
docstring=docstring,
|
|
564
|
+
bound=bound,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
return definitions
|
|
568
|
+
|
|
569
|
+
|
|
498
570
|
def on_worker_ready(sender: Any, **_: Any) -> None:
|
|
499
571
|
"""Handle worker_ready signal - publish worker registration event.
|
|
500
572
|
|
|
@@ -509,6 +581,7 @@ def on_worker_ready(sender: Any, **_: Any) -> None:
|
|
|
509
581
|
try:
|
|
510
582
|
hostname, pid = _get_hostname_and_pid(sender)
|
|
511
583
|
registered_tasks = _extract_registered_tasks(sender)
|
|
584
|
+
task_definitions = _extract_task_definitions(sender)
|
|
512
585
|
|
|
513
586
|
# Publish worker lifecycle event using the global transport
|
|
514
587
|
# This goes to the same stream as task events for unified consumption
|
|
@@ -520,6 +593,7 @@ def on_worker_ready(sender: Any, **_: Any) -> None:
|
|
|
520
593
|
pid=pid,
|
|
521
594
|
timestamp=datetime.now(timezone.utc),
|
|
522
595
|
registered_tasks=registered_tasks,
|
|
596
|
+
task_definitions=task_definitions,
|
|
523
597
|
)
|
|
524
598
|
|
|
525
599
|
if _transport is not None:
|
|
@@ -310,16 +310,25 @@ def create_api_router(
|
|
|
310
310
|
if status and task_status.value != status:
|
|
311
311
|
continue
|
|
312
312
|
|
|
313
|
+
definition = (
|
|
314
|
+
worker_registry.get_task_definition(name) if worker_registry else None
|
|
315
|
+
)
|
|
316
|
+
|
|
313
317
|
parts = name.rsplit(".", 1)
|
|
314
|
-
|
|
318
|
+
fallback_module = parts[0] if len(parts) > 1 else None
|
|
319
|
+
module = (
|
|
320
|
+
definition.module
|
|
321
|
+
if definition and definition.module
|
|
322
|
+
else fallback_module
|
|
323
|
+
)
|
|
315
324
|
|
|
316
325
|
tasks.append(
|
|
317
326
|
RegisteredTaskResponse(
|
|
318
327
|
name=name,
|
|
319
328
|
module=module,
|
|
320
|
-
signature=None,
|
|
321
|
-
docstring=None,
|
|
322
|
-
bound=False,
|
|
329
|
+
signature=definition.signature if definition else None,
|
|
330
|
+
docstring=definition.docstring if definition else None,
|
|
331
|
+
bound=definition.bound if definition else False,
|
|
323
332
|
execution_count=execution_count,
|
|
324
333
|
registered_by=registered_by,
|
|
325
334
|
last_run=last_run,
|
|
@@ -13,7 +13,7 @@ from stemtrace.core.graph import NodeType, TaskGraph, TaskNode
|
|
|
13
13
|
from stemtrace.server.api.schemas import WorkerStatus
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
|
-
from stemtrace.core.events import TaskEvent
|
|
16
|
+
from stemtrace.core.events import RegisteredTaskDefinition, TaskEvent
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class WorkerInfo(BaseModel):
|
|
@@ -104,6 +104,7 @@ class WorkerRegistry:
|
|
|
104
104
|
def __init__(self) -> None:
|
|
105
105
|
"""Initialize empty worker registry."""
|
|
106
106
|
self._workers: dict[str, WorkerInfo] = {}
|
|
107
|
+
self._task_definitions_by_name: dict[str, RegisteredTaskDefinition] = {}
|
|
107
108
|
self._lock = threading.RLock()
|
|
108
109
|
|
|
109
110
|
def register_worker(
|
|
@@ -111,6 +112,7 @@ class WorkerRegistry:
|
|
|
111
112
|
hostname: str,
|
|
112
113
|
pid: int,
|
|
113
114
|
tasks: list[str],
|
|
115
|
+
task_definitions: dict[str, RegisteredTaskDefinition] | None = None,
|
|
114
116
|
event_timestamp: datetime | None = None,
|
|
115
117
|
) -> None:
|
|
116
118
|
"""Register a worker with its task list.
|
|
@@ -121,6 +123,9 @@ class WorkerRegistry:
|
|
|
121
123
|
hostname: Worker hostname.
|
|
122
124
|
pid: Worker process ID.
|
|
123
125
|
tasks: List of registered task names.
|
|
126
|
+
task_definitions: Optional mapping of task name to task definition
|
|
127
|
+
metadata (docstring/signature/module/bound). When provided, this
|
|
128
|
+
is used to enrich `/api/tasks/registry`.
|
|
124
129
|
event_timestamp: When the worker event occurred. Used for last_seen
|
|
125
130
|
to properly handle historical events from Redis replay.
|
|
126
131
|
"""
|
|
@@ -134,6 +139,10 @@ class WorkerRegistry:
|
|
|
134
139
|
# Fall back to now() for direct calls (e.g., tests)
|
|
135
140
|
timestamp = event_timestamp or datetime.now(timezone.utc)
|
|
136
141
|
|
|
142
|
+
# Update global task definitions map (best-effort, last-writer-wins).
|
|
143
|
+
if task_definitions:
|
|
144
|
+
self._task_definitions_by_name.update(task_definitions)
|
|
145
|
+
|
|
137
146
|
# If worker already exists, update it (restart scenario)
|
|
138
147
|
if worker_id in self._workers:
|
|
139
148
|
self._workers[worker_id].registered_tasks = tasks
|
|
@@ -151,6 +160,18 @@ class WorkerRegistry:
|
|
|
151
160
|
status=WorkerStatus.ONLINE,
|
|
152
161
|
)
|
|
153
162
|
|
|
163
|
+
def get_task_definition(self, name: str) -> RegisteredTaskDefinition | None:
|
|
164
|
+
"""Get task definition metadata by task name.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
name: Fully qualified task name.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
RegisteredTaskDefinition if known, otherwise None.
|
|
171
|
+
"""
|
|
172
|
+
with self._lock:
|
|
173
|
+
return self._task_definitions_by_name.get(name)
|
|
174
|
+
|
|
154
175
|
def mark_shutdown(self, hostname: str, pid: int) -> None:
|
|
155
176
|
"""Mark a worker as offline (shutdown).
|
|
156
177
|
|
|
@@ -8,12 +8,12 @@ import pytest
|
|
|
8
8
|
from fastapi import FastAPI
|
|
9
9
|
from fastapi.testclient import TestClient
|
|
10
10
|
|
|
11
|
-
from stemtrace.core.events import TaskEvent, TaskState
|
|
11
|
+
from stemtrace.core.events import RegisteredTaskDefinition, TaskEvent, TaskState
|
|
12
12
|
from stemtrace.server.api.schemas import WorkerStatus
|
|
13
13
|
from stemtrace.server.fastapi.auth import require_api_key, require_basic_auth
|
|
14
14
|
from stemtrace.server.fastapi.extension import StemtraceExtension
|
|
15
15
|
from stemtrace.server.fastapi.router import create_router
|
|
16
|
-
from stemtrace.server.store import GraphStore
|
|
16
|
+
from stemtrace.server.store import GraphStore, WorkerRegistry
|
|
17
17
|
from stemtrace.server.websocket import WebSocketManager
|
|
18
18
|
|
|
19
19
|
|
|
@@ -621,8 +621,6 @@ class TestWorkersEndpoint:
|
|
|
621
621
|
|
|
622
622
|
def test_workers_empty_registry(self) -> None:
|
|
623
623
|
"""Empty registry returns empty list."""
|
|
624
|
-
from stemtrace.server.store import WorkerRegistry
|
|
625
|
-
|
|
626
624
|
store = GraphStore()
|
|
627
625
|
worker_registry = WorkerRegistry()
|
|
628
626
|
router = create_router(store=store, worker_registry=worker_registry)
|
|
@@ -639,8 +637,6 @@ class TestWorkersEndpoint:
|
|
|
639
637
|
|
|
640
638
|
def test_workers_returns_registered_worker(self) -> None:
|
|
641
639
|
"""Registered worker appears in response."""
|
|
642
|
-
from stemtrace.server.store import WorkerRegistry
|
|
643
|
-
|
|
644
640
|
store = GraphStore()
|
|
645
641
|
worker_registry = WorkerRegistry()
|
|
646
642
|
worker_registry.register_worker(
|
|
@@ -665,8 +661,6 @@ class TestWorkersEndpoint:
|
|
|
665
661
|
|
|
666
662
|
def test_workers_by_hostname(self) -> None:
|
|
667
663
|
"""Filter workers by hostname."""
|
|
668
|
-
from stemtrace.server.store import WorkerRegistry
|
|
669
|
-
|
|
670
664
|
store = GraphStore()
|
|
671
665
|
worker_registry = WorkerRegistry()
|
|
672
666
|
worker_registry.register_worker("worker-1", 12345, ["tasks.add"])
|
|
@@ -686,8 +680,6 @@ class TestWorkersEndpoint:
|
|
|
686
680
|
|
|
687
681
|
def test_workers_shutdown_status(self) -> None:
|
|
688
682
|
"""Shutdown worker has offline status."""
|
|
689
|
-
from stemtrace.server.store import WorkerRegistry
|
|
690
|
-
|
|
691
683
|
store = GraphStore()
|
|
692
684
|
worker_registry = WorkerRegistry()
|
|
693
685
|
worker_registry.register_worker("worker-1", 12345, ["tasks.add"])
|
|
@@ -712,8 +704,6 @@ class TestRegistryWithWorkers:
|
|
|
712
704
|
|
|
713
705
|
def test_registry_shows_registered_by(self) -> None:
|
|
714
706
|
"""Tasks show which workers registered them."""
|
|
715
|
-
from stemtrace.server.store import WorkerRegistry
|
|
716
|
-
|
|
717
707
|
store = GraphStore()
|
|
718
708
|
worker_registry = WorkerRegistry()
|
|
719
709
|
worker_registry.register_worker(
|
|
@@ -735,10 +725,54 @@ class TestRegistryWithWorkers:
|
|
|
735
725
|
# Both workers should be listed (deduplicated and sorted)
|
|
736
726
|
assert sorted(add_task["registered_by"]) == ["worker-1", "worker-2"]
|
|
737
727
|
|
|
728
|
+
def test_registry_includes_task_metadata(self) -> None:
|
|
729
|
+
"""Registry includes docstring/signature/bound when provided by workers."""
|
|
730
|
+
store = GraphStore()
|
|
731
|
+
worker_registry = WorkerRegistry()
|
|
732
|
+
|
|
733
|
+
task_name = "tasks.add"
|
|
734
|
+
worker_registry.register_worker(
|
|
735
|
+
"worker-1",
|
|
736
|
+
12345,
|
|
737
|
+
[task_name],
|
|
738
|
+
task_definitions={
|
|
739
|
+
task_name: RegisteredTaskDefinition(
|
|
740
|
+
name=task_name,
|
|
741
|
+
module="tasks",
|
|
742
|
+
signature="(x, y)",
|
|
743
|
+
docstring="Add two numbers.",
|
|
744
|
+
bound=True,
|
|
745
|
+
)
|
|
746
|
+
},
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# Ensure the task is also observed (so it appears in the union set even if workers differ).
|
|
750
|
+
store.add_event(
|
|
751
|
+
TaskEvent(
|
|
752
|
+
task_id="task-1",
|
|
753
|
+
name=task_name,
|
|
754
|
+
state=TaskState.SUCCESS,
|
|
755
|
+
timestamp=datetime(2024, 1, 1, tzinfo=UTC),
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
router = create_router(store=store, worker_registry=worker_registry)
|
|
760
|
+
app = FastAPI()
|
|
761
|
+
app.include_router(router)
|
|
762
|
+
client = TestClient(app)
|
|
763
|
+
|
|
764
|
+
response = client.get("/api/tasks/registry")
|
|
765
|
+
assert response.status_code == 200
|
|
766
|
+
data = response.json()
|
|
767
|
+
|
|
768
|
+
add_task = next(t for t in data["tasks"] if t["name"] == task_name)
|
|
769
|
+
assert add_task["module"] == "tasks"
|
|
770
|
+
assert add_task["signature"] == "(x, y)"
|
|
771
|
+
assert add_task["docstring"] == "Add two numbers."
|
|
772
|
+
assert add_task["bound"] is True
|
|
773
|
+
|
|
738
774
|
def test_registry_no_duplicate_hostnames(self) -> None:
|
|
739
775
|
"""Same hostname from multiple restarts appears only once."""
|
|
740
|
-
from stemtrace.server.store import WorkerRegistry
|
|
741
|
-
|
|
742
776
|
store = GraphStore()
|
|
743
777
|
worker_registry = WorkerRegistry()
|
|
744
778
|
# Same hostname, different PIDs (simulating restarts)
|
|
@@ -760,8 +794,6 @@ class TestRegistryWithWorkers:
|
|
|
760
794
|
|
|
761
795
|
def test_registry_never_run_status_filter(self) -> None:
|
|
762
796
|
"""status=never_run shows only registered but never executed tasks."""
|
|
763
|
-
from stemtrace.server.store import WorkerRegistry
|
|
764
|
-
|
|
765
797
|
store = GraphStore()
|
|
766
798
|
# Add an executed task (not registered) - status: not_registered
|
|
767
799
|
store.add_event(
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Tests for public API."""
|
|
2
2
|
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as package_version
|
|
3
5
|
from unittest.mock import MagicMock
|
|
4
6
|
|
|
5
7
|
import pytest
|
|
@@ -22,7 +24,7 @@ from stemtrace.library.transports.memory import MemoryTransport
|
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
@pytest.fixture(autouse=True)
|
|
25
|
-
def cleanup() ->
|
|
27
|
+
def cleanup() -> object:
|
|
26
28
|
"""Clean up after each test."""
|
|
27
29
|
yield
|
|
28
30
|
disconnect_signals()
|
|
@@ -31,8 +33,12 @@ def cleanup() -> None:
|
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
def test_version() -> None:
|
|
34
|
-
"""Version is set."""
|
|
35
|
-
|
|
36
|
+
"""Version is set and matches installed package metadata."""
|
|
37
|
+
try:
|
|
38
|
+
installed = package_version("stemtrace")
|
|
39
|
+
except PackageNotFoundError as exc:
|
|
40
|
+
raise AssertionError("stemtrace package metadata not available") from exc
|
|
41
|
+
assert __version__ == installed
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
class TestInit:
|