stemtrace 0.3.2__tar.gz → 0.3.4__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.3.2 → stemtrace-0.3.4}/.bumpversion.toml +1 -1
- {stemtrace-0.3.2 → stemtrace-0.3.4}/.github/workflows/ci.yml +1 -1
- {stemtrace-0.3.2 → stemtrace-0.3.4}/.github/workflows/e2e.yml +4 -4
- {stemtrace-0.3.2 → stemtrace-0.3.4}/.github/workflows/release.yml +10 -6
- {stemtrace-0.3.2 → stemtrace-0.3.4}/CHANGELOG.md +15 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/PKG-INFO +6 -2
- {stemtrace-0.3.2 → stemtrace-0.3.4}/README.md +5 -1
- {stemtrace-0.3.2 → stemtrace-0.3.4}/pyproject.toml +1 -1
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/__init__.py +10 -1
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/library/config.py +6 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/api/routes.py +57 -4
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/api/websocket.py +6 -2
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/extension.py +6 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/login_routes.py +21 -4
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/router.py +2 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/package-lock.json +313 -96
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/package.json +5 -5
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_form_login.py +104 -6
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_init.py +2 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_routes.py +141 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/uv.lock +1 -1
- {stemtrace-0.3.2 → stemtrace-0.3.4}/.dockerignore +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/.github/dependabot.yml +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/.gitignore +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/.pre-commit-config.yaml +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/.python-version +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/CONTRIBUTING.md +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/Dockerfile +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/Dockerfile.e2e +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/LICENSE +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/Makefile +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/build_ui.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/docker-compose.e2e.rabbitmq.yml +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/docker-compose.e2e.yml +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/docker-compose.rabbitmq.yml +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/docker-compose.yml +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/docs/screenshots/task_details.png +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/docs/screenshots/unregistered.png +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/docs/screenshots/workflow.png +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/examples/celery_app.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/examples/fastapi_integration.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/examples/with_auth.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/examples/with_login.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/progress.md +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/scripts/wait_for_http.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/core/__init__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/core/events.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/core/exceptions.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/core/graph.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/core/ports.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/library/__init__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/library/bootsteps.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/library/scrubbing.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/library/signals.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/library/transports/__init__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/library/transports/memory.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/library/transports/rabbitmq.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/library/transports/redis.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/py.typed +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/__init__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/__main__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/api/__init__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/api/schemas.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/consumer.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/__init__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/auth.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/form_auth.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/store.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/__init__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/biome.json +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/index.html +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/playwright.config.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/postcss.config.js +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/api/client.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/api/queries.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/DateRangePicker.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TaskStateBadge.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/index.css +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/main.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/__root.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/graphs.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/index.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/workers.tsx +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/utils/format.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/vite-env.d.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/graphs.spec.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/registry.spec.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/tasks.spec.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/workers.spec.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tsconfig.json +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tsconfig.tsbuildinfo +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/vite.config.ts +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/static.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/ui/templates/login.html +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/src/stemtrace/server/websocket.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/__init__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/conftest.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/e2e/__init__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/e2e/conftest.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/e2e/tasks.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/e2e/test_api_e2e.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/e2e/test_workers_e2e.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/integration/__init__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/integration/test_embedded_static.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/integration/test_fastapi_integration.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/integration/test_rabbitmq_transport.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/integration/test_redis_transport.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/integration/test_static_ui.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/integration/test_workers_refresh.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/__init__.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_auth.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_bootsteps.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_config.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_consumer.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_events.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_examples.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_exceptions.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_form_auth.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_graph.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_scrubbing.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_signals.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_store.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_transports.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_ui_static_prefix.py +0 -0
- {stemtrace-0.3.2 → stemtrace-0.3.4}/tests/unit/test_websocket.py +0 -0
|
@@ -24,7 +24,7 @@ jobs:
|
|
|
24
24
|
- uses: actions/checkout@v6
|
|
25
25
|
|
|
26
26
|
- name: Set up Docker Buildx
|
|
27
|
-
uses: docker/setup-buildx-action@
|
|
27
|
+
uses: docker/setup-buildx-action@v4
|
|
28
28
|
|
|
29
29
|
- name: Select compose file and Celery env
|
|
30
30
|
run: |
|
|
@@ -91,7 +91,7 @@ jobs:
|
|
|
91
91
|
- uses: actions/checkout@v6
|
|
92
92
|
|
|
93
93
|
- name: Set up Docker Buildx
|
|
94
|
-
uses: docker/setup-buildx-action@
|
|
94
|
+
uses: docker/setup-buildx-action@v4
|
|
95
95
|
|
|
96
96
|
- name: Select compose file and Celery env
|
|
97
97
|
run: |
|
|
@@ -167,7 +167,7 @@ jobs:
|
|
|
167
167
|
PLAYWRIGHT_BASE_URL: http://localhost:8000
|
|
168
168
|
|
|
169
169
|
- name: Upload Playwright report
|
|
170
|
-
uses: actions/upload-artifact@
|
|
170
|
+
uses: actions/upload-artifact@v7
|
|
171
171
|
if: always()
|
|
172
172
|
with:
|
|
173
173
|
name: playwright-report-${{ matrix.broker }}
|
|
@@ -175,7 +175,7 @@ jobs:
|
|
|
175
175
|
retention-days: 7
|
|
176
176
|
|
|
177
177
|
- name: Upload test results
|
|
178
|
-
uses: actions/upload-artifact@
|
|
178
|
+
uses: actions/upload-artifact@v7
|
|
179
179
|
if: failure()
|
|
180
180
|
with:
|
|
181
181
|
name: playwright-test-results-${{ matrix.broker }}
|
|
@@ -74,7 +74,7 @@ jobs:
|
|
|
74
74
|
run: twine check dist/*
|
|
75
75
|
|
|
76
76
|
- name: Upload dist artifacts
|
|
77
|
-
uses: actions/upload-artifact@
|
|
77
|
+
uses: actions/upload-artifact@v7
|
|
78
78
|
with:
|
|
79
79
|
name: dist
|
|
80
80
|
path: dist/
|
|
@@ -87,7 +87,7 @@ jobs:
|
|
|
87
87
|
id-token: write
|
|
88
88
|
steps:
|
|
89
89
|
- name: Download dist artifacts
|
|
90
|
-
uses: actions/download-artifact@
|
|
90
|
+
uses: actions/download-artifact@v8
|
|
91
91
|
with:
|
|
92
92
|
name: dist
|
|
93
93
|
path: dist/
|
|
@@ -104,8 +104,11 @@ jobs:
|
|
|
104
104
|
steps:
|
|
105
105
|
- uses: actions/checkout@v6
|
|
106
106
|
|
|
107
|
+
- name: Set up QEMU
|
|
108
|
+
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
|
109
|
+
|
|
107
110
|
- name: Set up Docker Buildx
|
|
108
|
-
uses: docker/setup-buildx-action@
|
|
111
|
+
uses: docker/setup-buildx-action@v4
|
|
109
112
|
|
|
110
113
|
- name: Log in to GitHub Container Registry
|
|
111
114
|
uses: docker/login-action@v3
|
|
@@ -116,7 +119,7 @@ jobs:
|
|
|
116
119
|
|
|
117
120
|
- name: Extract metadata
|
|
118
121
|
id: meta
|
|
119
|
-
uses: docker/metadata-action@
|
|
122
|
+
uses: docker/metadata-action@v6
|
|
120
123
|
with:
|
|
121
124
|
images: ghcr.io/${{ github.repository }}
|
|
122
125
|
tags: |
|
|
@@ -124,10 +127,11 @@ jobs:
|
|
|
124
127
|
type=raw,value=latest
|
|
125
128
|
|
|
126
129
|
- name: Build and push Docker image
|
|
127
|
-
uses: docker/build-push-action@
|
|
130
|
+
uses: docker/build-push-action@v7
|
|
128
131
|
with:
|
|
129
132
|
context: .
|
|
130
133
|
push: true
|
|
134
|
+
platforms: linux/amd64,linux/arm64
|
|
131
135
|
tags: ${{ steps.meta.outputs.tags }}
|
|
132
136
|
labels: ${{ steps.meta.outputs.labels }}
|
|
133
137
|
cache-from: type=gha
|
|
@@ -142,7 +146,7 @@ jobs:
|
|
|
142
146
|
- uses: actions/checkout@v6
|
|
143
147
|
|
|
144
148
|
- name: Download dist artifacts
|
|
145
|
-
uses: actions/download-artifact@
|
|
149
|
+
uses: actions/download-artifact@v8
|
|
146
150
|
with:
|
|
147
151
|
name: dist
|
|
148
152
|
path: dist/
|
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.4] - 2026-06-25
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- WebSocket: deny unauthenticated connections during the handshake instead of accepting and then closing them. The accept-then-close path half-opened the connection, so when an unauthenticated client (e.g. a port scanner) aborted mid-handshake, uvicorn's legacy `websockets` implementation ran `close()` on a protocol whose `transfer_data_task` was never set, raising `AttributeError: 'WebSocketProtocol' object has no attribute 'transfer_data_task'`. Closing before `accept()` rejects the handshake (HTTP 403) without opening the protocol object.
|
|
14
|
+
- Form login: build the login form action and redirects as relative paths instead of absolute URLs, and derive the session cookie's `Secure` flag from `X-Forwarded-Proto`. Behind a TLS-terminating proxy that doesn't forward the scheme to the ASGI scope (e.g. uvicorn without `--forwarded-allow-ips`), absolute URLs were downgraded to `http://`, so the browser posted the login form over http, the proxy answered `301 -> https`, the POST body was dropped, and login silently failed (with a mixed-content warning). stemtrace now works behind such a proxy without requiring forwarded-headers configuration.
|
|
15
|
+
|
|
16
|
+
## [0.3.3] - 2026-03-20
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Node alias from arguments: derive graph node display name from task args/kwargs via `node_alias_from_arguments` config option (thanks @ohadmata for the idea — #46, #48)
|
|
20
|
+
- Docker: multi-architecture image builds (amd64 + arm64) (thanks @ohadmata — #51)
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Graph: order task nodes before synthetic container nodes (GROUP/CHORD) for correct rendering (thanks @codyburrito — #52)
|
|
24
|
+
|
|
10
25
|
## [0.3.2] - 2026-02-28
|
|
11
26
|
|
|
12
27
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stemtrace
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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
|
|
@@ -56,7 +56,7 @@ Description-Content-Type: text/markdown
|
|
|
56
56
|
|
|
57
57
|
**Zero-infrastructure Celery task flow visualizer**
|
|
58
58
|
|
|
59
|
-
[](https://pypi.org/project/stemtrace)
|
|
60
60
|
[](https://pypi.org/project/stemtrace/)
|
|
61
61
|
[](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
|
|
62
62
|
[](https://codecov.io/gh/iansokolskyi/stemtrace)
|
|
@@ -250,6 +250,10 @@ stemtrace.init_worker(
|
|
|
250
250
|
scrub_sensitive_data=True, # Scrub passwords, API keys, etc.
|
|
251
251
|
additional_sensitive_keys=frozenset({"my_secret"}), # Add custom keys
|
|
252
252
|
safe_keys=frozenset({"public_key"}), # Never scrub these keys
|
|
253
|
+
|
|
254
|
+
# UI display
|
|
255
|
+
node_alias_from_arguments="operator_type", # Use kwargs["operator_type"] as node name
|
|
256
|
+
# node_alias_from_arguments="0", # Or use args[0] (digit string = positional index)
|
|
253
257
|
)
|
|
254
258
|
|
|
255
259
|
# Introspection (after init)
|
|
@@ -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)
|
|
@@ -196,6 +196,10 @@ stemtrace.init_worker(
|
|
|
196
196
|
scrub_sensitive_data=True, # Scrub passwords, API keys, etc.
|
|
197
197
|
additional_sensitive_keys=frozenset({"my_secret"}), # Add custom keys
|
|
198
198
|
safe_keys=frozenset({"public_key"}), # Never scrub these keys
|
|
199
|
+
|
|
200
|
+
# UI display
|
|
201
|
+
node_alias_from_arguments="operator_type", # Use kwargs["operator_type"] as node name
|
|
202
|
+
# node_alias_from_arguments="0", # Or use args[0] (digit string = positional index)
|
|
199
203
|
)
|
|
200
204
|
|
|
201
205
|
# Introspection (after init)
|
|
@@ -43,7 +43,7 @@ if TYPE_CHECKING:
|
|
|
43
43
|
from celery import Celery
|
|
44
44
|
from fastapi import FastAPI
|
|
45
45
|
|
|
46
|
-
__version__ = "0.3.
|
|
46
|
+
__version__ = "0.3.4"
|
|
47
47
|
__all__ = [
|
|
48
48
|
"ConfigurationError",
|
|
49
49
|
"StemtraceConfig",
|
|
@@ -93,6 +93,7 @@ def init_worker(
|
|
|
93
93
|
additional_sensitive_keys: frozenset[str] | None = None,
|
|
94
94
|
safe_keys: frozenset[str] | None = None,
|
|
95
95
|
max_data_size: int = 10240,
|
|
96
|
+
node_alias_from_arguments: str | None = None,
|
|
96
97
|
) -> None:
|
|
97
98
|
"""Initialize stemtrace for Celery worker instrumentation.
|
|
98
99
|
|
|
@@ -107,6 +108,9 @@ def init_worker(
|
|
|
107
108
|
additional_sensitive_keys: Extra keys to treat as sensitive.
|
|
108
109
|
safe_keys: Keys to never scrub (overrides sensitive).
|
|
109
110
|
max_data_size: Maximum size in bytes for serialized data (default: 10KB).
|
|
111
|
+
node_alias_from_arguments: Derive node display name from task arguments.
|
|
112
|
+
Pass a digit string to use args[index], a string key for kwargs[key],
|
|
113
|
+
or None to use the task name (default).
|
|
110
114
|
|
|
111
115
|
Raises:
|
|
112
116
|
ConfigurationError: If no broker URL can be determined.
|
|
@@ -130,6 +134,7 @@ def init_worker(
|
|
|
130
134
|
additional_sensitive_keys=additional_sensitive_keys or frozenset(),
|
|
131
135
|
safe_keys=safe_keys or frozenset(),
|
|
132
136
|
max_data_size=max_data_size,
|
|
137
|
+
node_alias_from_arguments=node_alias_from_arguments,
|
|
133
138
|
)
|
|
134
139
|
set_config(config)
|
|
135
140
|
|
|
@@ -153,6 +158,7 @@ def init_app(
|
|
|
153
158
|
login_password: str | None = None,
|
|
154
159
|
login_secret: str | None = None,
|
|
155
160
|
login_ttl_seconds: int = 86400,
|
|
161
|
+
node_alias_from_arguments: str | None = None,
|
|
156
162
|
) -> StemtraceExtension:
|
|
157
163
|
"""Initialize stemtrace as a FastAPI extension.
|
|
158
164
|
|
|
@@ -174,6 +180,8 @@ def init_app(
|
|
|
174
180
|
STEMTRACE_LOGIN_SECRET env var. If still missing, a random secret is
|
|
175
181
|
generated (sessions invalidated on app restart).
|
|
176
182
|
login_ttl_seconds: Session TTL in seconds (default: 24 hours).
|
|
183
|
+
node_alias_from_arguments: Derive graph node display name from task arguments.
|
|
184
|
+
Digit string for args[index], string for kwargs[key], or None (default).
|
|
177
185
|
|
|
178
186
|
Returns:
|
|
179
187
|
The initialized StemtraceExtension instance.
|
|
@@ -230,6 +238,7 @@ def init_app(
|
|
|
230
238
|
max_nodes=max_nodes,
|
|
231
239
|
auth_dependency=auth_dependency,
|
|
232
240
|
form_auth_config=form_auth_config,
|
|
241
|
+
node_alias_from_arguments=node_alias_from_arguments,
|
|
233
242
|
)
|
|
234
243
|
extension.init_app(fastapi_app, prefix=prefix)
|
|
235
244
|
|
|
@@ -16,6 +16,9 @@ class StemtraceConfig(BaseModel):
|
|
|
16
16
|
scrub_sensitive_data: Whether to scrub sensitive keys.
|
|
17
17
|
additional_sensitive_keys: Extra keys to treat as sensitive.
|
|
18
18
|
safe_keys: Keys to never scrub (overrides sensitive).
|
|
19
|
+
node_alias_from_arguments: Derive graph node display name from task
|
|
20
|
+
arguments. Pass a string key to use kwargs[key], a digit string
|
|
21
|
+
to use args[index], or None to use the task name (default).
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
model_config = ConfigDict(frozen=True)
|
|
@@ -34,6 +37,9 @@ class StemtraceConfig(BaseModel):
|
|
|
34
37
|
additional_sensitive_keys: frozenset[str] = Field(default_factory=frozenset)
|
|
35
38
|
safe_keys: frozenset[str] = Field(default_factory=frozenset)
|
|
36
39
|
|
|
40
|
+
# UI display options
|
|
41
|
+
node_alias_from_arguments: str | None = None
|
|
42
|
+
|
|
37
43
|
|
|
38
44
|
_config: StemtraceConfig | None = None
|
|
39
45
|
|
|
@@ -123,9 +123,40 @@ def _node_to_response(node: TaskNode) -> TaskNodeResponse:
|
|
|
123
123
|
)
|
|
124
124
|
|
|
125
125
|
|
|
126
|
+
def _resolve_node_alias(node: TaskNode, key: str | None) -> str:
|
|
127
|
+
"""Resolve display name for a graph node from task arguments.
|
|
128
|
+
|
|
129
|
+
When a key is configured, attempts to extract a value from the first
|
|
130
|
+
event's args (if key is a digit string) or kwargs (if key is a regular
|
|
131
|
+
string). Falls back to the node's registered task name.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
node: Task node containing events with args/kwargs.
|
|
135
|
+
key: Digit string for args[index], string for kwargs[key],
|
|
136
|
+
or None to use the task name.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Resolved display name for the node.
|
|
140
|
+
"""
|
|
141
|
+
if key is None or not node.events:
|
|
142
|
+
return node.name
|
|
143
|
+
|
|
144
|
+
event = node.events[0]
|
|
145
|
+
|
|
146
|
+
if key.isdigit():
|
|
147
|
+
index = int(key)
|
|
148
|
+
if isinstance(event.args, list) and index < len(event.args):
|
|
149
|
+
return str(event.args[index])
|
|
150
|
+
elif isinstance(event.kwargs, dict) and key in event.kwargs:
|
|
151
|
+
return str(event.kwargs[key])
|
|
152
|
+
|
|
153
|
+
return node.name
|
|
154
|
+
|
|
155
|
+
|
|
126
156
|
def _node_to_graph_response(
|
|
127
157
|
node: TaskNode,
|
|
128
158
|
all_nodes: dict[str, TaskNode] | None = None,
|
|
159
|
+
node_alias_key: str | None = None,
|
|
129
160
|
) -> GraphNodeResponse:
|
|
130
161
|
"""Convert TaskNode to graph response model.
|
|
131
162
|
|
|
@@ -134,6 +165,7 @@ def _node_to_graph_response(
|
|
|
134
165
|
Args:
|
|
135
166
|
node: Task node from graph store.
|
|
136
167
|
all_nodes: All nodes dict for child lookup.
|
|
168
|
+
node_alias_key: Key to resolve display name from task arguments.
|
|
137
169
|
|
|
138
170
|
Returns:
|
|
139
171
|
GraphNodeResponse with timing from children.
|
|
@@ -159,15 +191,25 @@ def _node_to_graph_response(
|
|
|
159
191
|
if first_seen and last_updated and first_seen != last_updated:
|
|
160
192
|
duration_ms = int((last_updated - first_seen).total_seconds() * 1000)
|
|
161
193
|
|
|
194
|
+
# UI traversal is order-sensitive when callbacks can appear as both direct
|
|
195
|
+
# children and CHORD-linked nodes. Prefer TASK children first so callback
|
|
196
|
+
# branches render before synthetic containers.
|
|
197
|
+
children = node.children
|
|
198
|
+
if all_nodes is not None:
|
|
199
|
+
children = sorted(
|
|
200
|
+
node.children,
|
|
201
|
+
key=lambda c: getattr(all_nodes.get(c), "node_type", None) != "TASK",
|
|
202
|
+
)
|
|
203
|
+
|
|
162
204
|
return GraphNodeResponse(
|
|
163
205
|
task_id=node.task_id,
|
|
164
|
-
name=node
|
|
206
|
+
name=_resolve_node_alias(node, node_alias_key),
|
|
165
207
|
state=node.state,
|
|
166
208
|
node_type=node.node_type,
|
|
167
209
|
group_id=node.group_id,
|
|
168
210
|
chord_id=node.chord_id,
|
|
169
211
|
parent_id=node.parent_id,
|
|
170
|
-
children=
|
|
212
|
+
children=children,
|
|
171
213
|
duration_ms=duration_ms,
|
|
172
214
|
first_seen=first_seen,
|
|
173
215
|
last_updated=last_updated,
|
|
@@ -289,6 +331,7 @@ def create_api_router(
|
|
|
289
331
|
ws_manager: WebSocketManager | None = None,
|
|
290
332
|
worker_registry: WorkerRegistry | None = None,
|
|
291
333
|
broker_url: str | None = None,
|
|
334
|
+
node_alias_from_arguments: str | None = None,
|
|
292
335
|
) -> APIRouter:
|
|
293
336
|
"""Create REST API router with task and graph endpoints.
|
|
294
337
|
|
|
@@ -298,6 +341,8 @@ def create_api_router(
|
|
|
298
341
|
ws_manager: Optional WebSocket manager.
|
|
299
342
|
worker_registry: Optional worker registry for lifecycle tracking.
|
|
300
343
|
broker_url: Optional Celery broker URL for on-demand inspection.
|
|
344
|
+
node_alias_from_arguments: Key to derive node display name from task
|
|
345
|
+
arguments. Digit string for args[index], string for kwargs[key].
|
|
301
346
|
|
|
302
347
|
Returns:
|
|
303
348
|
Configured API router.
|
|
@@ -562,7 +607,12 @@ def create_api_router(
|
|
|
562
607
|
for child in store.get_children(root.task_id):
|
|
563
608
|
all_nodes[child.task_id] = child
|
|
564
609
|
return GraphListResponse(
|
|
565
|
-
graphs=[
|
|
610
|
+
graphs=[
|
|
611
|
+
_node_to_graph_response(
|
|
612
|
+
r, all_nodes, node_alias_key=node_alias_from_arguments
|
|
613
|
+
)
|
|
614
|
+
for r in roots
|
|
615
|
+
],
|
|
566
616
|
total=total,
|
|
567
617
|
limit=limit,
|
|
568
618
|
offset=offset,
|
|
@@ -586,7 +636,10 @@ def create_api_router(
|
|
|
586
636
|
return GraphResponse(
|
|
587
637
|
root_id=root_id,
|
|
588
638
|
nodes={
|
|
589
|
-
tid: _node_to_graph_response(
|
|
639
|
+
tid: _node_to_graph_response(
|
|
640
|
+
n, all_nodes, node_alias_key=node_alias_from_arguments
|
|
641
|
+
)
|
|
642
|
+
for tid, n in graph.items()
|
|
590
643
|
},
|
|
591
644
|
)
|
|
592
645
|
|
|
@@ -33,8 +33,12 @@ def create_websocket_router(
|
|
|
33
33
|
secret=form_auth_config.secret,
|
|
34
34
|
expected_username=form_auth_config.username,
|
|
35
35
|
):
|
|
36
|
-
#
|
|
37
|
-
|
|
36
|
+
# Deny the handshake without completing it. Accepting first and
|
|
37
|
+
# then closing half-opens the connection; if the unauthenticated
|
|
38
|
+
# client aborts during that dance, uvicorn's legacy websockets
|
|
39
|
+
# impl runs close() on a protocol whose transfer_data_task was
|
|
40
|
+
# never set and raises AttributeError. Closing before accept lets
|
|
41
|
+
# Starlette reject the handshake (HTTP 403) without opening it.
|
|
38
42
|
await websocket.close(code=1008)
|
|
39
43
|
return
|
|
40
44
|
|
|
@@ -67,6 +67,7 @@ class StemtraceExtension:
|
|
|
67
67
|
max_nodes: int = 10000,
|
|
68
68
|
auth_dependency: Any = None,
|
|
69
69
|
form_auth_config: FormAuthConfig | None = None,
|
|
70
|
+
node_alias_from_arguments: str | None = None,
|
|
70
71
|
) -> None:
|
|
71
72
|
"""Initialize extension with broker and transport configuration.
|
|
72
73
|
|
|
@@ -81,6 +82,8 @@ class StemtraceExtension:
|
|
|
81
82
|
max_nodes: Maximum number of nodes to keep in memory.
|
|
82
83
|
auth_dependency: Optional FastAPI dependency applied to all routes for authentication.
|
|
83
84
|
form_auth_config: Optional cookie-session configuration used to protect WebSocket.
|
|
85
|
+
node_alias_from_arguments: Key to derive graph node display name from task
|
|
86
|
+
arguments. Digit string for args[index], string for kwargs[key].
|
|
84
87
|
"""
|
|
85
88
|
self._broker_url = broker_url
|
|
86
89
|
self._transport_url = transport_url or broker_url
|
|
@@ -103,6 +106,8 @@ class StemtraceExtension:
|
|
|
103
106
|
worker_registry=self._worker_registry,
|
|
104
107
|
)
|
|
105
108
|
|
|
109
|
+
self._node_alias_from_arguments = node_alias_from_arguments
|
|
110
|
+
|
|
106
111
|
self._store.add_listener(self._ws_manager.queue_event)
|
|
107
112
|
|
|
108
113
|
@staticmethod
|
|
@@ -141,6 +146,7 @@ class StemtraceExtension:
|
|
|
141
146
|
broker_url=self._broker_url,
|
|
142
147
|
auth_dependency=self._auth_dependency,
|
|
143
148
|
form_auth_config=self._form_auth_config,
|
|
149
|
+
node_alias_from_arguments=self._node_alias_from_arguments,
|
|
144
150
|
)
|
|
145
151
|
|
|
146
152
|
if self._serve_ui:
|
|
@@ -40,6 +40,20 @@ def _safe_next(next_path: str | None, *, default: str) -> str:
|
|
|
40
40
|
return next_path
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def _request_is_https(request: Request) -> bool:
|
|
44
|
+
"""Best-effort HTTPS detection that survives a TLS-terminating proxy.
|
|
45
|
+
|
|
46
|
+
Falls back to the ``X-Forwarded-Proto`` header when the ASGI scheme is
|
|
47
|
+
``http`` because proxy headers weren't applied to the scope (e.g. uvicorn
|
|
48
|
+
without ``--forwarded-allow-ips``). The first value of a comma-separated
|
|
49
|
+
list is the original client scheme.
|
|
50
|
+
"""
|
|
51
|
+
if request.url.scheme == "https":
|
|
52
|
+
return True
|
|
53
|
+
forwarded = request.headers.get("x-forwarded-proto", "")
|
|
54
|
+
return forwarded.split(",")[0].strip().lower() == "https"
|
|
55
|
+
|
|
56
|
+
|
|
43
57
|
def _login_page_html(*, action_path: str, error: str | None, next_path: str) -> str:
|
|
44
58
|
"""Render the login page HTML from a template."""
|
|
45
59
|
escaped_title = html.escape(_DEFAULT_TITLE, quote=True)
|
|
@@ -92,7 +106,10 @@ def create_login_router(
|
|
|
92
106
|
next_path = _safe_next(
|
|
93
107
|
request.query_params.get("next"), default=default_next_path
|
|
94
108
|
)
|
|
95
|
-
|
|
109
|
+
# Relative path (not an absolute URL): the browser submits using the
|
|
110
|
+
# page's own scheme/host, so a TLS-terminating proxy that doesn't
|
|
111
|
+
# forward the scheme can't downgrade the form action to http.
|
|
112
|
+
action_path = request.url_for("stemtrace_login_submit").path
|
|
96
113
|
rendered_html = _login_page_html(
|
|
97
114
|
action_path=action_path, error=error, next_path=next_path
|
|
98
115
|
)
|
|
@@ -125,7 +142,7 @@ def create_login_router(
|
|
|
125
142
|
qs = {"error": "Invalid username or password"}
|
|
126
143
|
if next_param:
|
|
127
144
|
qs["next"] = next_param
|
|
128
|
-
login_url =
|
|
145
|
+
login_url = request.url_for("stemtrace_login_page").path
|
|
129
146
|
return RedirectResponse(
|
|
130
147
|
url=login_url + "?" + urllib.parse.urlencode(qs),
|
|
131
148
|
status_code=303,
|
|
@@ -141,7 +158,7 @@ def create_login_router(
|
|
|
141
158
|
value=config.create_session_cookie_value(),
|
|
142
159
|
httponly=True,
|
|
143
160
|
samesite="lax",
|
|
144
|
-
secure=request
|
|
161
|
+
secure=_request_is_https(request),
|
|
145
162
|
path=config.cookie_path,
|
|
146
163
|
)
|
|
147
164
|
return response
|
|
@@ -149,7 +166,7 @@ def create_login_router(
|
|
|
149
166
|
@router.post("/logout", include_in_schema=False, name="stemtrace_logout")
|
|
150
167
|
async def logout(request: Request) -> RedirectResponse:
|
|
151
168
|
"""Clear the session cookie and redirect to login."""
|
|
152
|
-
login_url =
|
|
169
|
+
login_url = request.url_for("stemtrace_login_page").path
|
|
153
170
|
response = RedirectResponse(url=login_url, status_code=303)
|
|
154
171
|
response.delete_cookie(key=config.cookie_name, path=config.cookie_path)
|
|
155
172
|
return response
|
|
@@ -24,6 +24,7 @@ def create_router(
|
|
|
24
24
|
broker_url: str | None = None,
|
|
25
25
|
auth_dependency: Any = None,
|
|
26
26
|
form_auth_config: FormAuthConfig | None = None,
|
|
27
|
+
node_alias_from_arguments: str | None = None,
|
|
27
28
|
) -> APIRouter:
|
|
28
29
|
"""Create API router. For embedded consumer, use StemtraceExtension."""
|
|
29
30
|
if store is None:
|
|
@@ -40,6 +41,7 @@ def create_router(
|
|
|
40
41
|
ws_manager,
|
|
41
42
|
worker_registry,
|
|
42
43
|
broker_url=broker_url,
|
|
44
|
+
node_alias_from_arguments=node_alias_from_arguments,
|
|
43
45
|
)
|
|
44
46
|
ws_router = create_websocket_router(ws_manager, form_auth_config=form_auth_config)
|
|
45
47
|
|