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.
Files changed (127) hide show
  1. {stemtrace-0.2.0 → stemtrace-0.2.2}/.bumpversion.toml +15 -1
  2. {stemtrace-0.2.0 → stemtrace-0.2.2}/.github/workflows/ci.yml +39 -0
  3. {stemtrace-0.2.0 → stemtrace-0.2.2}/CHANGELOG.md +13 -1
  4. {stemtrace-0.2.0 → stemtrace-0.2.2}/Makefile +3 -3
  5. {stemtrace-0.2.0 → stemtrace-0.2.2}/PKG-INFO +3 -2
  6. {stemtrace-0.2.0 → stemtrace-0.2.2}/README.md +1 -1
  7. {stemtrace-0.2.0 → stemtrace-0.2.2}/pyproject.toml +4 -1
  8. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/__init__.py +1 -1
  9. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/core/events.py +28 -2
  10. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/signals.py +75 -1
  11. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/api/routes.py +13 -4
  12. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/consumer.py +1 -0
  13. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/store.py +22 -1
  14. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/package.json +1 -1
  15. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/test_fastapi_integration.py +48 -16
  16. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_init.py +9 -3
  17. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_routes.py +40 -10
  18. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_signals.py +57 -0
  19. {stemtrace-0.2.0 → stemtrace-0.2.2}/uv.lock +1 -1
  20. {stemtrace-0.2.0 → stemtrace-0.2.2}/.dockerignore +0 -0
  21. {stemtrace-0.2.0 → stemtrace-0.2.2}/.github/dependabot.yml +0 -0
  22. {stemtrace-0.2.0 → stemtrace-0.2.2}/.github/workflows/e2e.yml +0 -0
  23. {stemtrace-0.2.0 → stemtrace-0.2.2}/.github/workflows/release.yml +0 -0
  24. {stemtrace-0.2.0 → stemtrace-0.2.2}/.gitignore +0 -0
  25. {stemtrace-0.2.0 → stemtrace-0.2.2}/.pre-commit-config.yaml +0 -0
  26. {stemtrace-0.2.0 → stemtrace-0.2.2}/.python-version +0 -0
  27. {stemtrace-0.2.0 → stemtrace-0.2.2}/CONTRIBUTING.md +0 -0
  28. {stemtrace-0.2.0 → stemtrace-0.2.2}/Dockerfile +0 -0
  29. {stemtrace-0.2.0 → stemtrace-0.2.2}/Dockerfile.e2e +0 -0
  30. {stemtrace-0.2.0 → stemtrace-0.2.2}/LICENSE +0 -0
  31. {stemtrace-0.2.0 → stemtrace-0.2.2}/build_ui.py +0 -0
  32. {stemtrace-0.2.0 → stemtrace-0.2.2}/docker-compose.e2e.yml +0 -0
  33. {stemtrace-0.2.0 → stemtrace-0.2.2}/docker-compose.yml +0 -0
  34. {stemtrace-0.2.0 → stemtrace-0.2.2}/docs/screenshots/task_details.png +0 -0
  35. {stemtrace-0.2.0 → stemtrace-0.2.2}/docs/screenshots/unregistered.png +0 -0
  36. {stemtrace-0.2.0 → stemtrace-0.2.2}/docs/screenshots/workflow.png +0 -0
  37. {stemtrace-0.2.0 → stemtrace-0.2.2}/examples/celery_app.py +0 -0
  38. {stemtrace-0.2.0 → stemtrace-0.2.2}/examples/fastapi_integration.py +0 -0
  39. {stemtrace-0.2.0 → stemtrace-0.2.2}/examples/with_auth.py +0 -0
  40. {stemtrace-0.2.0 → stemtrace-0.2.2}/progress.md +0 -0
  41. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/core/__init__.py +0 -0
  42. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/core/exceptions.py +0 -0
  43. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/core/graph.py +0 -0
  44. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/core/ports.py +0 -0
  45. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/__init__.py +0 -0
  46. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/bootsteps.py +0 -0
  47. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/config.py +0 -0
  48. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/scrubbing.py +0 -0
  49. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/transports/__init__.py +0 -0
  50. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/transports/memory.py +0 -0
  51. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/library/transports/redis.py +0 -0
  52. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/py.typed +0 -0
  53. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/__init__.py +0 -0
  54. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/__main__.py +0 -0
  55. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/api/__init__.py +0 -0
  56. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/api/schemas.py +0 -0
  57. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/api/websocket.py +0 -0
  58. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/fastapi/__init__.py +0 -0
  59. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/fastapi/auth.py +0 -0
  60. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/fastapi/extension.py +0 -0
  61. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/fastapi/router.py +0 -0
  62. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/__init__.py +0 -0
  63. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/biome.json +0 -0
  64. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/index.html +0 -0
  65. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/package-lock.json +0 -0
  66. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/playwright.config.ts +0 -0
  67. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/postcss.config.js +0 -0
  68. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/api/client.ts +0 -0
  69. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/api/queries.ts +0 -0
  70. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/DateRangePicker.tsx +0 -0
  71. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx +0 -0
  72. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx +0 -0
  73. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx +0 -0
  74. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/TaskStateBadge.tsx +0 -0
  75. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx +0 -0
  76. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx +0 -0
  77. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx +0 -0
  78. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx +0 -0
  79. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts +0 -0
  80. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/index.css +0 -0
  81. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/main.tsx +0 -0
  82. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/__root.tsx +0 -0
  83. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx +0 -0
  84. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/graphs.tsx +0 -0
  85. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/index.tsx +0 -0
  86. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx +0 -0
  87. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx +0 -0
  88. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/routes/workers.tsx +0 -0
  89. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/utils/format.ts +0 -0
  90. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/src/vite-env.d.ts +0 -0
  91. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts +0 -0
  92. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts +0 -0
  93. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/graphs.spec.ts +0 -0
  94. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/registry.spec.ts +0 -0
  95. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts +0 -0
  96. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/tasks.spec.ts +0 -0
  97. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tests/workers.spec.ts +0 -0
  98. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tsconfig.json +0 -0
  99. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/tsconfig.tsbuildinfo +0 -0
  100. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/frontend/vite.config.ts +0 -0
  101. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/ui/static.py +0 -0
  102. {stemtrace-0.2.0 → stemtrace-0.2.2}/src/stemtrace/server/websocket.py +0 -0
  103. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/__init__.py +0 -0
  104. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/conftest.py +0 -0
  105. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/e2e/__init__.py +0 -0
  106. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/e2e/conftest.py +0 -0
  107. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/e2e/tasks.py +0 -0
  108. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/e2e/test_api_e2e.py +0 -0
  109. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/e2e/test_workers_e2e.py +0 -0
  110. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/__init__.py +0 -0
  111. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/test_embedded_static.py +0 -0
  112. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/test_redis_transport.py +0 -0
  113. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/test_static_ui.py +0 -0
  114. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/integration/test_workers_refresh.py +0 -0
  115. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/__init__.py +0 -0
  116. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_auth.py +0 -0
  117. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_bootsteps.py +0 -0
  118. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_config.py +0 -0
  119. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_consumer.py +0 -0
  120. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_events.py +0 -0
  121. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_examples.py +0 -0
  122. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_exceptions.py +0 -0
  123. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_graph.py +0 -0
  124. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_scrubbing.py +0 -0
  125. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_store.py +0 -0
  126. {stemtrace-0.2.0 → stemtrace-0.2.2}/tests/unit/test_transports.py +0 -0
  127. {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.0"
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 = "[![PyPI version](https://img.shields.io/badge/pypi-v{current_version}-darklime)](https://pypi.org/project/stemtrace)"
47
+ replace = "[![PyPI version](https://img.shields.io/badge/pypi-v{new_version}-darklime)](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.0...HEAD
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: git add -A && git commit -m 'chore: release v$$NEW_VER' && make release"
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: git add -A && git commit -m 'chore: release v$$NEW_VER' && make release"
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: git add -A && git commit -m 'chore: release v$$NEW_VER' && make release"
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.0
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
- [![PyPI version](https://img.shields.io/pypi/v/stemtrace)](https://pypi.org/project/stemtrace/)
59
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.2.2-darklime)](https://pypi.org/project/stemtrace)
59
60
  [![Python](https://img.shields.io/pypi/pyversions/stemtrace.svg)](https://pypi.org/project/stemtrace/)
60
61
  [![CI](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml/badge.svg)](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
61
62
  [![codecov](https://codecov.io/gh/iansokolskyi/stemtrace/graph/badge.svg)](https://codecov.io/gh/iansokolskyi/stemtrace)
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Zero-infrastructure Celery task flow visualizer**
4
4
 
5
- [![PyPI version](https://img.shields.io/pypi/v/stemtrace)](https://pypi.org/project/stemtrace/)
5
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.2.2-darklime)](https://pypi.org/project/stemtrace)
6
6
  [![Python](https://img.shields.io/pypi/pyversions/stemtrace.svg)](https://pypi.org/project/stemtrace/)
7
7
  [![CI](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml/badge.svg)](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
8
8
  [![codecov](https://codecov.io/gh/iansokolskyi/stemtrace/graph/badge.svg)](https://codecov.io/gh/iansokolskyi/stemtrace)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stemtrace"
3
- version = "0.2.0"
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",
@@ -38,7 +38,7 @@ if TYPE_CHECKING:
38
38
  from celery import Celery
39
39
  from fastapi import FastAPI
40
40
 
41
- __version__ = "0.2.0"
41
+ __version__ = "0.2.2"
42
42
  __all__ = [
43
43
  "ConfigurationError",
44
44
  "StemtraceConfig",
@@ -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
- module = parts[0] if len(parts) > 1 else None
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,
@@ -141,6 +141,7 @@ class EventConsumer:
141
141
  hostname=event.hostname,
142
142
  pid=event.pid,
143
143
  tasks=event.registered_tasks,
144
+ task_definitions=event.task_definitions,
144
145
  event_timestamp=event.timestamp,
145
146
  )
146
147
  logger.info(
@@ -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
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stemtrace-ui",
3
3
  "private": true,
4
- "version": "0.2.0",
4
+ "version": "0.2.2",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -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() -> None:
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
- assert __version__ == "0.2.0"
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: