stemtrace 0.2.0__tar.gz → 0.2.1__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.1}/.bumpversion.toml +1 -1
- {stemtrace-0.2.0 → stemtrace-0.2.1}/.github/workflows/ci.yml +39 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/CHANGELOG.md +11 -1
- {stemtrace-0.2.0 → stemtrace-0.2.1}/PKG-INFO +2 -2
- {stemtrace-0.2.0 → stemtrace-0.2.1}/README.md +1 -1
- {stemtrace-0.2.0 → stemtrace-0.2.1}/pyproject.toml +1 -1
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/__init__.py +1 -1
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/core/events.py +28 -2
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/library/signals.py +75 -1
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/api/routes.py +13 -4
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/consumer.py +1 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/store.py +22 -1
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/package.json +1 -1
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/integration/test_fastapi_integration.py +48 -16
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_routes.py +40 -10
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_signals.py +57 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/uv.lock +1 -1
- {stemtrace-0.2.0 → stemtrace-0.2.1}/.dockerignore +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/.github/dependabot.yml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/.github/workflows/e2e.yml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/.github/workflows/release.yml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/.gitignore +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/.pre-commit-config.yaml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/.python-version +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/CONTRIBUTING.md +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/Dockerfile +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/Dockerfile.e2e +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/LICENSE +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/Makefile +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/build_ui.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/docker-compose.e2e.yml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/docker-compose.yml +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/docs/screenshots/task_details.png +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/docs/screenshots/unregistered.png +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/docs/screenshots/workflow.png +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/examples/celery_app.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/examples/fastapi_integration.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/examples/with_auth.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/progress.md +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/core/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/core/exceptions.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/core/graph.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/core/ports.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/library/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/library/bootsteps.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/library/config.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/library/scrubbing.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/library/transports/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/library/transports/memory.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/library/transports/redis.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/py.typed +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/__main__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/api/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/api/schemas.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/api/websocket.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/fastapi/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/fastapi/auth.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/fastapi/extension.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/fastapi/router.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/biome.json +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/index.html +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/package-lock.json +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/playwright.config.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/postcss.config.js +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/api/client.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/api/queries.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/DateRangePicker.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/TaskStateBadge.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/index.css +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/main.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/routes/__root.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/routes/graphs.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/routes/index.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/routes/workers.tsx +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/utils/format.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/vite-env.d.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tests/graphs.spec.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tests/registry.spec.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tests/tasks.spec.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tests/workers.spec.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tsconfig.json +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tsconfig.tsbuildinfo +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/vite.config.ts +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/static.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/websocket.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/conftest.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/e2e/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/e2e/conftest.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/e2e/tasks.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/e2e/test_api_e2e.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/e2e/test_workers_e2e.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/integration/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/integration/test_embedded_static.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/integration/test_redis_transport.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/integration/test_static_ui.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/integration/test_workers_refresh.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/__init__.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_auth.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_bootsteps.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_config.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_consumer.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_events.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_examples.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_exceptions.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_graph.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_init.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_scrubbing.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_store.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_transports.py +0 -0
- {stemtrace-0.2.0 → stemtrace-0.2.1}/tests/unit/test_websocket.py +0 -0
|
@@ -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,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-01-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Task Registry now includes task definition metadata from workers (module, signature, docstring, bound)
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- README: updated PyPI badge styling
|
|
17
|
+
- CI: upload Codecov coverage on `main`
|
|
18
|
+
|
|
10
19
|
## [0.2.0] - 2026-01-03
|
|
11
20
|
|
|
12
21
|
### Added
|
|
@@ -50,7 +59,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
50
59
|
- **E2E test suite**: Docker API tests + Playwright browser tests
|
|
51
60
|
- **Comprehensive test suite**: 350+ Python tests, 90%+ coverage
|
|
52
61
|
|
|
53
|
-
[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.2.
|
|
62
|
+
[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.2.1...HEAD
|
|
63
|
+
[0.2.1]: https://github.com/iansokolskyi/stemtrace/compare/v0.2.0...v0.2.1
|
|
54
64
|
[0.2.0]: https://github.com/iansokolskyi/stemtrace/compare/v0.1.1...v0.2.0
|
|
55
65
|
[0.1.1]: https://github.com/iansokolskyi/stemtrace/compare/v0.1.0...v0.1.1
|
|
56
66
|
[0.1.0]: https://github.com/iansokolskyi/stemtrace/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stemtrace
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -55,7 +55,7 @@ Description-Content-Type: text/markdown
|
|
|
55
55
|
|
|
56
56
|
**Zero-infrastructure Celery task flow visualizer**
|
|
57
57
|
|
|
58
|
-
[](https://pypi.org/project/stemtrace
|
|
58
|
+
[](https://pypi.org/project/stemtrace)
|
|
59
59
|
[](https://pypi.org/project/stemtrace/)
|
|
60
60
|
[](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
|
|
61
61
|
[](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
|
|
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)
|
|
@@ -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(
|
|
@@ -7,9 +7,9 @@ import pytest
|
|
|
7
7
|
from fastapi import FastAPI
|
|
8
8
|
from fastapi.testclient import TestClient
|
|
9
9
|
|
|
10
|
-
from stemtrace.core.events import TaskEvent, TaskState
|
|
10
|
+
from stemtrace.core.events import RegisteredTaskDefinition, TaskEvent, TaskState
|
|
11
11
|
from stemtrace.server.api.routes import create_api_router
|
|
12
|
-
from stemtrace.server.store import GraphStore
|
|
12
|
+
from stemtrace.server.store import GraphStore, WorkerRegistry
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@pytest.fixture
|
|
@@ -431,8 +431,6 @@ class TestTaskRegistryWithWorkers:
|
|
|
431
431
|
self, store: GraphStore, make_event: type
|
|
432
432
|
) -> None:
|
|
433
433
|
"""Task with executions AND registered by worker has 'active' status."""
|
|
434
|
-
from stemtrace.server.store import WorkerRegistry
|
|
435
|
-
|
|
436
434
|
worker_registry = WorkerRegistry()
|
|
437
435
|
worker_registry.register_worker(
|
|
438
436
|
hostname="worker-1",
|
|
@@ -457,10 +455,46 @@ class TestTaskRegistryWithWorkers:
|
|
|
457
455
|
assert task["status"] == "active"
|
|
458
456
|
assert "worker-1" in task["registered_by"]
|
|
459
457
|
|
|
458
|
+
def test_registry_includes_task_metadata_from_worker_registry(
|
|
459
|
+
self, store: GraphStore, make_event: type
|
|
460
|
+
) -> None:
|
|
461
|
+
"""Registry includes docstring/signature/bound when provided by workers."""
|
|
462
|
+
worker_registry = WorkerRegistry()
|
|
463
|
+
task_name = "myapp.tasks.process"
|
|
464
|
+
worker_registry.register_worker(
|
|
465
|
+
hostname="worker-1",
|
|
466
|
+
pid=12345,
|
|
467
|
+
tasks=[task_name],
|
|
468
|
+
task_definitions={
|
|
469
|
+
task_name: RegisteredTaskDefinition(
|
|
470
|
+
name=task_name,
|
|
471
|
+
module="myapp.tasks",
|
|
472
|
+
signature="(x, y)",
|
|
473
|
+
docstring="Process data.",
|
|
474
|
+
bound=True,
|
|
475
|
+
)
|
|
476
|
+
},
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Add execution so task is visible in registry
|
|
480
|
+
store.add_event(make_event.create("task-1", name=task_name))
|
|
481
|
+
|
|
482
|
+
app = FastAPI()
|
|
483
|
+
router = create_api_router(store, worker_registry=worker_registry)
|
|
484
|
+
app.include_router(router)
|
|
485
|
+
client = TestClient(app)
|
|
486
|
+
|
|
487
|
+
response = client.get("/api/tasks/registry")
|
|
488
|
+
data = response.json()
|
|
489
|
+
|
|
490
|
+
task = next(t for t in data["tasks"] if t["name"] == task_name)
|
|
491
|
+
assert task["module"] == "myapp.tasks"
|
|
492
|
+
assert task["signature"] == "(x, y)"
|
|
493
|
+
assert task["docstring"] == "Process data."
|
|
494
|
+
assert task["bound"] is True
|
|
495
|
+
|
|
460
496
|
def test_registry_never_run_status_with_worker(self, store: GraphStore) -> None:
|
|
461
497
|
"""Task registered by worker but never executed has 'never_run' status."""
|
|
462
|
-
from stemtrace.server.store import WorkerRegistry
|
|
463
|
-
|
|
464
498
|
worker_registry = WorkerRegistry()
|
|
465
499
|
worker_registry.register_worker(
|
|
466
500
|
hostname="worker-1",
|
|
@@ -488,8 +522,6 @@ class TestTaskRegistryWithWorkers:
|
|
|
488
522
|
self, store: GraphStore, make_event: type
|
|
489
523
|
) -> None:
|
|
490
524
|
"""Filter to show only never_run tasks."""
|
|
491
|
-
from stemtrace.server.store import WorkerRegistry
|
|
492
|
-
|
|
493
525
|
worker_registry = WorkerRegistry()
|
|
494
526
|
worker_registry.register_worker(
|
|
495
527
|
hostname="worker-1",
|
|
@@ -514,8 +546,6 @@ class TestTaskRegistryWithWorkers:
|
|
|
514
546
|
|
|
515
547
|
def test_registry_mixed_statuses(self, store: GraphStore, make_event: type) -> None:
|
|
516
548
|
"""Registry shows all three status types correctly."""
|
|
517
|
-
from stemtrace.server.store import WorkerRegistry
|
|
518
|
-
|
|
519
549
|
worker_registry = WorkerRegistry()
|
|
520
550
|
worker_registry.register_worker(
|
|
521
551
|
hostname="worker-1",
|
|
@@ -10,8 +10,10 @@ import pytest
|
|
|
10
10
|
from stemtrace.core.events import TaskState, WorkerEvent, WorkerEventType
|
|
11
11
|
from stemtrace.library.config import StemtraceConfig, set_config
|
|
12
12
|
from stemtrace.library.signals import (
|
|
13
|
+
_MAX_DOCSTRING_CHARS,
|
|
13
14
|
_extract_chord_info,
|
|
14
15
|
_extract_registered_tasks,
|
|
16
|
+
_extract_task_definitions,
|
|
15
17
|
_format_exception,
|
|
16
18
|
_format_traceback,
|
|
17
19
|
_get_hostname_and_pid,
|
|
@@ -211,6 +213,61 @@ class TestWorkerLifecycleSignals:
|
|
|
211
213
|
|
|
212
214
|
assert len(MemoryTransport.events) == 0
|
|
213
215
|
|
|
216
|
+
def test_extract_task_definitions_empty_without_app(self) -> None:
|
|
217
|
+
"""No sender.app returns empty task definitions mapping."""
|
|
218
|
+
sender = MagicMock()
|
|
219
|
+
sender.app = None
|
|
220
|
+
assert _extract_task_definitions(sender) == {}
|
|
221
|
+
|
|
222
|
+
def test_extract_task_definitions_empty_without_tasks_registry(self) -> None:
|
|
223
|
+
"""No app.tasks returns empty task definitions mapping."""
|
|
224
|
+
sender = SimpleNamespace(app=SimpleNamespace(tasks=None))
|
|
225
|
+
assert _extract_task_definitions(sender) == {}
|
|
226
|
+
|
|
227
|
+
def test_extract_task_definitions_truncates_docstring(self) -> None:
|
|
228
|
+
"""Docstrings longer than the limit are truncated with a marker."""
|
|
229
|
+
|
|
230
|
+
def run_impl() -> None:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
run_impl.__doc__ = "x" * (_MAX_DOCSTRING_CHARS + 50)
|
|
234
|
+
|
|
235
|
+
class TaskObj:
|
|
236
|
+
run = staticmethod(run_impl)
|
|
237
|
+
bind = True
|
|
238
|
+
|
|
239
|
+
sender = SimpleNamespace(app=SimpleNamespace(tasks={"my.task": TaskObj()}))
|
|
240
|
+
defs = _extract_task_definitions(sender)
|
|
241
|
+
assert "my.task" in defs
|
|
242
|
+
assert defs["my.task"].bound is True
|
|
243
|
+
assert defs["my.task"].docstring is not None
|
|
244
|
+
assert defs["my.task"].docstring.endswith("[truncated]")
|
|
245
|
+
assert len(defs["my.task"].docstring) > _MAX_DOCSTRING_CHARS
|
|
246
|
+
|
|
247
|
+
def test_extract_task_definitions_signature_best_effort(self) -> None:
|
|
248
|
+
"""Non-callable run attribute yields signature=None without raising."""
|
|
249
|
+
|
|
250
|
+
class TaskObj:
|
|
251
|
+
run = 123 # inspect.signature will raise TypeError
|
|
252
|
+
bind = False
|
|
253
|
+
|
|
254
|
+
sender = SimpleNamespace(app=SimpleNamespace(tasks={"my.bad": TaskObj()}))
|
|
255
|
+
defs = _extract_task_definitions(sender)
|
|
256
|
+
assert defs["my.bad"].signature is None
|
|
257
|
+
|
|
258
|
+
def test_extract_task_definitions_module_fallback_and_bind_guard(self) -> None:
|
|
259
|
+
"""Fallback module uses task_obj.__module__ and bind guard ignores non-bool."""
|
|
260
|
+
|
|
261
|
+
class TaskObj:
|
|
262
|
+
__module__ = "my.module"
|
|
263
|
+
run = None
|
|
264
|
+
bind = MagicMock()
|
|
265
|
+
|
|
266
|
+
sender = SimpleNamespace(app=SimpleNamespace(tasks={"my.task": TaskObj()}))
|
|
267
|
+
defs = _extract_task_definitions(sender)
|
|
268
|
+
assert defs["my.task"].module == "my.module"
|
|
269
|
+
assert defs["my.task"].bound is False
|
|
270
|
+
|
|
214
271
|
|
|
215
272
|
class TestTaskPrerun:
|
|
216
273
|
"""Tests for task_prerun signal handler."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx
RENAMED
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx
RENAMED
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx
RENAMED
|
File without changes
|
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx
RENAMED
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx
RENAMED
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx
RENAMED
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx
RENAMED
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx
RENAMED
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts
RENAMED
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stemtrace-0.2.0 → stemtrace-0.2.1}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|