taskflow-meter 1.0.0__tar.gz → 1.2.0__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 (116) hide show
  1. taskflow_meter-1.0.0/README.md → taskflow_meter-1.2.0/PKG-INFO +95 -6
  2. taskflow_meter-1.0.0/PKG-INFO → taskflow_meter-1.2.0/README.md +54 -56
  3. taskflow_meter-1.2.0/docs/design.md +171 -0
  4. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/docs/guide.md +42 -4
  5. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/pyproject.toml +49 -35
  6. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/__init__.py +2 -1
  7. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/_version.py +2 -2
  8. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/sse.py +4 -1
  9. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/cli.py +63 -7
  10. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/listener.py +20 -1
  11. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/conf.py +6 -1
  12. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/base.py +4 -2
  13. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/persistence.py +5 -13
  14. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/migrations/env.py +17 -1
  15. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/source.py +15 -1
  16. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/events.py +21 -3
  17. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/fold.py +6 -1
  18. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/meter.py +4 -2
  19. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/models.py +20 -0
  20. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/amqp.py +4 -2
  21. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/base.py +22 -3
  22. taskflow_meter-1.2.0/taskflow_meter/transports/oslo_messaging.py +355 -0
  23. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/conformance/test_parity.py +4 -4
  24. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_cross_process.py +1 -1
  25. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_packaging.py +63 -6
  26. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/integration/test_attach.py +48 -1
  27. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/integration/test_collector.py +59 -1
  28. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/sqlalchemy/test_source.py +20 -0
  29. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_cli.py +94 -0
  30. taskflow_meter-1.2.0/tests/unit/transports/test_oslo_messaging.py +425 -0
  31. taskflow_meter-1.0.0/docs/PLAN.md +0 -439
  32. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/.gitignore +0 -0
  33. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/LICENSE +0 -0
  34. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/docs/releasing.md +0 -0
  35. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/__init__.py +0 -0
  36. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/asgi.py +0 -0
  37. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/dispatch.py +0 -0
  38. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/http.py +0 -0
  39. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/router.py +0 -0
  40. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/routes.py +0 -0
  41. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/serializers.py +0 -0
  42. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/service.py +0 -0
  43. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/wsgi.py +0 -0
  44. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/__init__.py +0 -0
  45. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/attachment.py +0 -0
  46. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/pipeline.py +0 -0
  47. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/progress.py +0 -0
  48. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/__init__.py +0 -0
  49. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/django.py +0 -0
  50. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/fastapi.py +0 -0
  51. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/flask.py +0 -0
  52. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/paste.py +0 -0
  53. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/pecan.py +0 -0
  54. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/__init__.py +0 -0
  55. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/memory.py +0 -0
  56. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/__init__.py +0 -0
  57. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/migrations/script.py.mako +0 -0
  58. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/migrations/versions/0001_initial.py +0 -0
  59. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/models.py +0 -0
  60. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/diff.py +0 -0
  61. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/poller.py +0 -0
  62. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/py.typed +0 -0
  63. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/states.py +0 -0
  64. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/__init__.py +0 -0
  65. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/http.py +0 -0
  66. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/memory.py +0 -0
  67. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/__init__.py +0 -0
  68. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/asgi_client.py +0 -0
  69. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/conformance/__init__.py +0 -0
  70. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/conformance/test_hosts.py +0 -0
  71. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/conftest.py +0 -0
  72. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/__init__.py +0 -0
  73. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_examples.py +0 -0
  74. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_migrations.py +0 -0
  75. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_serve.py +0 -0
  76. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_tooling.py +0 -0
  77. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/hosts.py +0 -0
  78. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/integration/__init__.py +0 -0
  79. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/__init__.py +0 -0
  80. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/__init__.py +0 -0
  81. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_asgi.py +0 -0
  82. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_http.py +0 -0
  83. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_router.py +0 -0
  84. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_routes.py +0 -0
  85. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_serializers.py +0 -0
  86. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_service.py +0 -0
  87. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_sse.py +0 -0
  88. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_wsgi.py +0 -0
  89. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/collect/__init__.py +0 -0
  90. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_attachment.py +0 -0
  91. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_listener.py +0 -0
  92. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_pipeline.py +0 -0
  93. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_progress.py +0 -0
  94. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/__init__.py +0 -0
  95. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_django.py +0 -0
  96. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_fastapi.py +0 -0
  97. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_flask.py +0 -0
  98. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_paste.py +0 -0
  99. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_pecan.py +0 -0
  100. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/__init__.py +0 -0
  101. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/sqlalchemy/__init__.py +0 -0
  102. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/test_base.py +0 -0
  103. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/test_memory.py +0 -0
  104. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/test_persistence.py +0 -0
  105. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_conf.py +0 -0
  106. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_diff.py +0 -0
  107. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_events.py +0 -0
  108. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_meter.py +0 -0
  109. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_models.py +0 -0
  110. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_poller.py +0 -0
  111. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_states.py +0 -0
  112. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/transports/__init__.py +0 -0
  113. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/transports/test_amqp.py +0 -0
  114. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/transports/test_http.py +0 -0
  115. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/transports/test_memory.py +0 -0
  116. {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/wsgi_client.py +0 -0
@@ -1,12 +1,59 @@
1
+ Metadata-Version: 2.5
2
+ Name: taskflow-meter
3
+ Version: 1.2.0
4
+ Summary: Monitoring interfaces (ASGI, WSGI, datasource) for OpenStack TaskFlow flow execution progress
5
+ Project-URL: Homepage, https://github.com/daipham3213/taskflow.meter
6
+ Project-URL: Source, https://github.com/daipham3213/taskflow.meter
7
+ Project-URL: Issues, https://github.com/daipham3213/taskflow.meter/issues
8
+ Project-URL: Changelog, https://github.com/daipham3213/taskflow.meter/releases
9
+ Author-email: Pham Le Gia Dai <daipham.3213@gmail.com>
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: asgi,monitoring,openstack,taskflow,workflow,wsgi
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: System :: Monitoring
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: oslo-config>=6.9.0
27
+ Requires-Dist: taskflow>=4.2.0
28
+ Provides-Extra: all
29
+ Requires-Dist: alembic>=1.2.0; extra == 'all'
30
+ Requires-Dist: kombu>=5.1.0; extra == 'all'
31
+ Requires-Dist: oslo-messaging>=6.0.0; extra == 'all'
32
+ Requires-Dist: sqlalchemy>=1.4.0; extra == 'all'
33
+ Provides-Extra: amqp
34
+ Requires-Dist: kombu>=5.1.0; extra == 'amqp'
35
+ Provides-Extra: oslo-messaging
36
+ Requires-Dist: oslo-messaging>=6.0.0; extra == 'oslo-messaging'
37
+ Provides-Extra: sqlalchemy
38
+ Requires-Dist: alembic>=1.2.0; extra == 'sqlalchemy'
39
+ Requires-Dist: sqlalchemy>=1.4.0; extra == 'sqlalchemy'
40
+ Description-Content-Type: text/markdown
41
+
1
42
  # taskflow-meter
2
43
 
44
+ [![ci](https://github.com/daipham3213/taskflow.meter/actions/workflows/ci.yml/badge.svg)](https://github.com/daipham3213/taskflow.meter/actions/workflows/ci.yml)
45
+ [![conformance](https://github.com/daipham3213/taskflow.meter/actions/workflows/conformance.yml/badge.svg)](https://github.com/daipham3213/taskflow.meter/actions/workflows/conformance.yml)
46
+ [![PyPI](https://img.shields.io/pypi/v/taskflow-meter.svg)](https://pypi.org/project/taskflow-meter/)
47
+ [![Python versions](https://img.shields.io/pypi/pyversions/taskflow-meter.svg)](https://pypi.org/project/taskflow-meter/)
48
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
49
+
3
50
  Monitoring interfaces — ASGI, WSGI, datasources, transports — for observing
4
51
  [OpenStack TaskFlow](https://opendev.org/openstack/taskflow) flow execution
5
52
  progress.
6
53
 
7
- > **Status: pre-alpha.** The packaging, tooling and CI skeleton are in place
8
- > (milestone M0). The design is specified in [`docs/PLAN.md`](docs/PLAN.md);
9
- > functionality lands milestone by milestone against it.
54
+ ```bash
55
+ pip install taskflow-meter
56
+ ```
10
57
 
11
58
  ## What it is for
12
59
 
@@ -73,9 +120,13 @@ polling anything.
73
120
 
74
121
  ```bash
75
122
  taskflow-meter upgrade --store-url postgresql://host/meter
76
- taskflow-meter collect --amqp-url amqp://broker// --store-url postgresql://host/meter
123
+ taskflow-meter collect --url amqp://broker// --store-url postgresql://host/meter
77
124
  taskflow-meter serve --store-url postgresql://host/meter
78
125
  ```
126
+
127
+ Inside OpenStack, `--transport oslo-messaging` puts the events on the
128
+ notification bus the service is already configured for, instead of opening a
129
+ broker connection of its own.
79
130
  One caveat for WSGI deployments: a synchronous worker holds a thread for as
80
131
  long as an SSE stream stays open, so use gevent or eventlet workers for
81
132
  streaming, or let clients poll `/events?since_seq=` instead.
@@ -156,8 +207,37 @@ serving an empty stream that cannot be told apart from silence.
156
207
 
157
208
  ## Requirements
158
209
 
159
- - Python 3.11+
160
- - taskflow 6.4.0+
210
+ - Python 3.10+
211
+ - taskflow 4.2.0+
212
+ - oslo.config 6.9.0+
213
+
214
+ Floors are deliberately low: this package is meant to be co-installed into
215
+ a service whose dependency versions it does not get to choose. They are the
216
+ oldest release of each library the suite actually passes against, not the
217
+ oldest that looks plausible -- a `lowest-direct` CI job installs exactly
218
+ these and runs the whole suite on them.
219
+
220
+ Everything else is optional, and only needed by the feature that imports it:
221
+
222
+ | Extra | Pulls in | Needed for |
223
+ | --- | --- | --- |
224
+ | `sqlalchemy` | SQLAlchemy 1.4+, alembic 1.2+ | The collector's own store |
225
+ | `amqp` | kombu 5.1+ | Publishing events to a broker |
226
+ | `oslo-messaging` | oslo.messaging 6.0+ | Publishing onto the service's own notification bus |
227
+ | `all` | all three | |
228
+
229
+ The contrib adapters declare no dependency on their hosts -- a deployment
230
+ mounting the meter in Django already has Django. They are tested against
231
+ Django 3.2, Flask 2.3.3, FastAPI 0.100, Pecan 1.4 and PasteDeploy 2.0.
232
+
233
+ ## Documentation
234
+
235
+ | | |
236
+ | --- | --- |
237
+ | [`docs/guide.md`](docs/guide.md) | Deploying it: configuration, every host, and how to read completion and current-task out of the API |
238
+ | [`docs/design.md`](docs/design.md) | How it works and why -- what taskflow does and does not record, and the rules the embedding code obeys |
239
+ | [`docs/releasing.md`](docs/releasing.md) | Cutting a release |
240
+ | [`CHANGELOG.md`](CHANGELOG.md) | What changed |
161
241
 
162
242
  ## Examples
163
243
 
@@ -186,6 +266,15 @@ uvx tox -e py312 # tests on one interpreter
186
266
  uvx tox # the whole matrix
187
267
  ```
188
268
 
269
+ CI also runs the suite with every declared dependency floor installed exactly,
270
+ which is the only job that checks those floors are real. To reproduce it:
271
+
272
+ ```bash
273
+ uv lock --python 3.10 --resolution lowest-direct
274
+ uv sync --python 3.10 --group dev --all-extras --resolution lowest-direct
275
+ uv run --frozen --no-sync pytest
276
+ ```
277
+
189
278
  Because the version is derived from git history, a shallow clone or a checkout
190
279
  with no tags will build as `0.0.0`. CI checks out with full history.
191
280
 
@@ -1,62 +1,18 @@
1
- Metadata-Version: 2.5
2
- Name: taskflow-meter
3
- Version: 1.0.0
4
- Summary: Monitoring interfaces (ASGI, WSGI, datasource) for OpenStack TaskFlow flow execution progress
5
- Project-URL: Homepage, https://github.com/daipham3213/taskflow-meter
6
- Project-URL: Source, https://github.com/daipham3213/taskflow-meter
7
- Project-URL: Issues, https://github.com/daipham3213/taskflow-meter/issues
8
- Project-URL: Changelog, https://github.com/daipham3213/taskflow-meter/releases
9
- Author-email: Pham Le Gia Dai <daipham.3213@gmail.com>
10
- License-Expression: Apache-2.0
11
- License-File: LICENSE
12
- Keywords: asgi,monitoring,openstack,taskflow,workflow,wsgi
13
- Classifier: Development Status :: 4 - Beta
14
- Classifier: Intended Audience :: Developers
15
- Classifier: Intended Audience :: System Administrators
16
- Classifier: Operating System :: OS Independent
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.13
21
- Classifier: Programming Language :: Python :: 3.14
22
- Classifier: Topic :: System :: Monitoring
23
- Classifier: Typing :: Typed
24
- Requires-Python: >=3.11
25
- Requires-Dist: oslo-cache>=3.0.0
26
- Requires-Dist: oslo-config>=9.0.0
27
- Requires-Dist: oslo-serialization>=2.18.0
28
- Requires-Dist: oslo-utils>=3.33.0
29
- Requires-Dist: stevedore>=1.20.0
30
- Requires-Dist: taskflow>=6.4.0
31
- Provides-Extra: all
32
- Requires-Dist: alembic>=1.13; extra == 'all'
33
- Requires-Dist: kombu>=5.0; extra == 'all'
34
- Requires-Dist: oslo-cache[dogpile]>=3.0.0; extra == 'all'
35
- Requires-Dist: oslo-cache[etcd3gw]>=3.0.0; extra == 'all'
36
- Requires-Dist: prometheus-client>=0.20; extra == 'all'
37
- Requires-Dist: sqlalchemy>=2.0; extra == 'all'
38
- Provides-Extra: amqp
39
- Requires-Dist: kombu>=5.0; extra == 'amqp'
40
- Provides-Extra: etcd
41
- Requires-Dist: oslo-cache[etcd3gw]>=3.0.0; extra == 'etcd'
42
- Provides-Extra: memcache
43
- Requires-Dist: oslo-cache[dogpile]>=3.0.0; extra == 'memcache'
44
- Provides-Extra: prometheus
45
- Requires-Dist: prometheus-client>=0.20; extra == 'prometheus'
46
- Provides-Extra: sqlalchemy
47
- Requires-Dist: alembic>=1.13; extra == 'sqlalchemy'
48
- Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
49
- Description-Content-Type: text/markdown
50
-
51
1
  # taskflow-meter
52
2
 
3
+ [![ci](https://github.com/daipham3213/taskflow.meter/actions/workflows/ci.yml/badge.svg)](https://github.com/daipham3213/taskflow.meter/actions/workflows/ci.yml)
4
+ [![conformance](https://github.com/daipham3213/taskflow.meter/actions/workflows/conformance.yml/badge.svg)](https://github.com/daipham3213/taskflow.meter/actions/workflows/conformance.yml)
5
+ [![PyPI](https://img.shields.io/pypi/v/taskflow-meter.svg)](https://pypi.org/project/taskflow-meter/)
6
+ [![Python versions](https://img.shields.io/pypi/pyversions/taskflow-meter.svg)](https://pypi.org/project/taskflow-meter/)
7
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
8
+
53
9
  Monitoring interfaces — ASGI, WSGI, datasources, transports — for observing
54
10
  [OpenStack TaskFlow](https://opendev.org/openstack/taskflow) flow execution
55
11
  progress.
56
12
 
57
- > **Status: pre-alpha.** The packaging, tooling and CI skeleton are in place
58
- > (milestone M0). The design is specified in [`docs/PLAN.md`](docs/PLAN.md);
59
- > functionality lands milestone by milestone against it.
13
+ ```bash
14
+ pip install taskflow-meter
15
+ ```
60
16
 
61
17
  ## What it is for
62
18
 
@@ -123,9 +79,13 @@ polling anything.
123
79
 
124
80
  ```bash
125
81
  taskflow-meter upgrade --store-url postgresql://host/meter
126
- taskflow-meter collect --amqp-url amqp://broker// --store-url postgresql://host/meter
82
+ taskflow-meter collect --url amqp://broker// --store-url postgresql://host/meter
127
83
  taskflow-meter serve --store-url postgresql://host/meter
128
84
  ```
85
+
86
+ Inside OpenStack, `--transport oslo-messaging` puts the events on the
87
+ notification bus the service is already configured for, instead of opening a
88
+ broker connection of its own.
129
89
  One caveat for WSGI deployments: a synchronous worker holds a thread for as
130
90
  long as an SSE stream stays open, so use gevent or eventlet workers for
131
91
  streaming, or let clients poll `/events?since_seq=` instead.
@@ -206,8 +166,37 @@ serving an empty stream that cannot be told apart from silence.
206
166
 
207
167
  ## Requirements
208
168
 
209
- - Python 3.11+
210
- - taskflow 6.4.0+
169
+ - Python 3.10+
170
+ - taskflow 4.2.0+
171
+ - oslo.config 6.9.0+
172
+
173
+ Floors are deliberately low: this package is meant to be co-installed into
174
+ a service whose dependency versions it does not get to choose. They are the
175
+ oldest release of each library the suite actually passes against, not the
176
+ oldest that looks plausible -- a `lowest-direct` CI job installs exactly
177
+ these and runs the whole suite on them.
178
+
179
+ Everything else is optional, and only needed by the feature that imports it:
180
+
181
+ | Extra | Pulls in | Needed for |
182
+ | --- | --- | --- |
183
+ | `sqlalchemy` | SQLAlchemy 1.4+, alembic 1.2+ | The collector's own store |
184
+ | `amqp` | kombu 5.1+ | Publishing events to a broker |
185
+ | `oslo-messaging` | oslo.messaging 6.0+ | Publishing onto the service's own notification bus |
186
+ | `all` | all three | |
187
+
188
+ The contrib adapters declare no dependency on their hosts -- a deployment
189
+ mounting the meter in Django already has Django. They are tested against
190
+ Django 3.2, Flask 2.3.3, FastAPI 0.100, Pecan 1.4 and PasteDeploy 2.0.
191
+
192
+ ## Documentation
193
+
194
+ | | |
195
+ | --- | --- |
196
+ | [`docs/guide.md`](docs/guide.md) | Deploying it: configuration, every host, and how to read completion and current-task out of the API |
197
+ | [`docs/design.md`](docs/design.md) | How it works and why -- what taskflow does and does not record, and the rules the embedding code obeys |
198
+ | [`docs/releasing.md`](docs/releasing.md) | Cutting a release |
199
+ | [`CHANGELOG.md`](CHANGELOG.md) | What changed |
211
200
 
212
201
  ## Examples
213
202
 
@@ -236,6 +225,15 @@ uvx tox -e py312 # tests on one interpreter
236
225
  uvx tox # the whole matrix
237
226
  ```
238
227
 
228
+ CI also runs the suite with every declared dependency floor installed exactly,
229
+ which is the only job that checks those floors are real. To reproduce it:
230
+
231
+ ```bash
232
+ uv lock --python 3.10 --resolution lowest-direct
233
+ uv sync --python 3.10 --group dev --all-extras --resolution lowest-direct
234
+ uv run --frozen --no-sync pytest
235
+ ```
236
+
239
237
  Because the version is derived from git history, a shallow clone or a checkout
240
238
  with no tags will build as `0.0.0`. CI checks out with full history.
241
239
 
@@ -0,0 +1,171 @@
1
+ # How taskflow-meter works, and why
2
+
3
+ The [guide](guide.md) covers deploying it. This covers the reasoning
4
+ underneath: what taskflow does and does not record, what follows from that,
5
+ and the rules the embedding code has to obey. It is here so the next person
6
+ to change something knows which parts are load-bearing.
7
+
8
+ ---
9
+
10
+ ## 1. What taskflow gives us
11
+
12
+ Everything else follows from these. They are properties of taskflow, verified
13
+ against it rather than assumed. The suite keeps them honest from two
14
+ directions: `tests/functional/test_packaging.py` pins the API names the
15
+ package relies on, and `tests/functional/test_cross_process.py` proves the
16
+ behaviour that matters most -- progress reported by a task in one process
17
+ being readable from another.
18
+
19
+ | Fact | Consequence |
20
+ | --- | --- |
21
+ | `engine.notifier` fires on flow state transitions, `engine.atom_notifier` on atom state transitions | A `Listener` sees state changes, and only state changes |
22
+ | `task.update_progress()` is handled by `TaskAction._on_update_progress`, which writes to storage and **never re-emits on `atom_notifier`** | A listener cannot see intra-task progress. Reading it live needs a separate tap on each atom's own notifier |
23
+ | `Task.TASK_EVENTS` is exactly `('update_progress',)` | That tap has one event to bind, and no others will appear |
24
+ | `storage.set_task_progress()` write-throughs to the persistence backend, as `meta['progress']` and `meta['progress_details']` | Fine-grained progress **is** durable, and readable from another process |
25
+ | `LogBook` has `created_at`/`updated_at`; `FlowDetail` and `AtomDetail` have neither | No per-atom start or end times exist to report. Observation times are stamped by the meter, and mean only when it looked |
26
+ | Flow DAG edges are not persisted -- a `FlowDetail` holds a flat set of atoms | Nothing read from persistence can draw the graph. Topology needs the in-process path |
27
+ | `Connection` exposes `get_logbooks()`, `get_flows_for_book()`, `get_atoms_for_flow()`, with no filtering or paging | Listing is a full scan |
28
+ | Progress callbacks are proxied back from remote workers | Worker-based engines report progress the same way |
29
+ | taskflow discovers backends through stevedore entry points | Our own plugins follow the same convention |
30
+
31
+ **The bottom line, and the reason this package can exist:** a deployment
32
+ already using a shared persistence backend can be monitored *including
33
+ per-task progress* with no changes to flow code. The in-process path is an
34
+ enhancement -- lower latency, plus the graph -- not a prerequisite.
35
+
36
+ The `memory` backend is the exception: per-process, and invisible from
37
+ anywhere else.
38
+
39
+ ## 2. Decisions, and what they cost
40
+
41
+ | Decision | Why | What it costs |
42
+ | --- | --- | --- |
43
+ | Read-only over taskflow persistence as the primary source | Monitors existing deployments untouched | Resolution is the poll interval; anything entered and left between two polls was never observable |
44
+ | Hand-rolled ASGI and WSGI callables, no web framework | Maximum embeddability: a service does not inherit Starlette because it wanted monitoring | Two implementations to keep identical, which is what the conformance suite is for |
45
+ | Both callables ship, rather than one plus a bridge | No `a2wsgi`-style shim in either direction | -- |
46
+ | Mountable at any sub-path, plus native adapters in `contrib/` | Standalone `serve` is one deployment mode, not the only one | Every link has to be built from a discovered prefix |
47
+ | oslo.config for configuration | What OpenStack operators expect; the service configures the meter in the file it already reads | -- |
48
+ | Completion derived from state, never from raw `progress` | taskflow sets progress to 1.0 on `REVERTED` as well as `SUCCESS`, so a mean of `progress` reports a fully reverted flow as complete | Two numbers in the payload instead of one |
49
+
50
+ ## 3. The emit side must never hurt the flow
51
+
52
+ The rule the whole in-process path rests on: **nothing monitoring does can
53
+ change a flow's outcome or its timing measurably.** Concretely:
54
+
55
+ - A publisher that raises is counted and logged, and the next batch is still
56
+ attempted. It cannot propagate into the engine.
57
+ - Delivery happens on its own thread behind a **bounded** queue, so a task
58
+ never waits on a publisher. Blocking would stall the flow; growing without
59
+ limit would take the process down later, for a reason nobody would connect
60
+ back to monitoring.
61
+ - A full queue drops the oldest events, counts the drops, and logs. Losing
62
+ old monitoring data beats losing the process.
63
+ - A listener whose callback raises logs and carries on, and the flow finishes
64
+ normally.
65
+
66
+ The receiving side makes the opposite trade, deliberately: a handler that
67
+ fails leaves the batch on the broker rather than acknowledging it. Silently
68
+ dropping monitoring data is worse than redelivering it.
69
+
70
+ ## 4. Embedding into somebody else's application
71
+
72
+ Three ways in, in increasing order of how much the host is involved:
73
+
74
+ 1. **Mount the raw callable.** Works in anything that can mount an ASGI or
75
+ WSGI app. Nothing from this package is needed.
76
+ 2. **A native adapter** from `contrib/`. The routes are registered in the
77
+ host's own router, so its authentication, middleware, error handling --
78
+ and, for FastAPI, its OpenAPI schema -- apply to them.
79
+ 3. **Hand-wire it.** `api.routes.build_routes()` returns the route table as
80
+ data; register the handlers yourself.
81
+
82
+ ### Mount-safety
83
+
84
+ - **Never trust `scope["path"]` alone.** Modern Starlette's `Mount` does not
85
+ rewrite `path`; it extends `root_path`. Older versions stripped `path`
86
+ instead. A server given `--root-path` never puts the prefix in `path` at
87
+ all. One rule covers all three: strip `root_path` from `path` **only when
88
+ it is genuinely a prefix**, otherwise use `path` unchanged. That is what
89
+ `api.http.split_path` does.
90
+ - **WSGI is unambiguous.** The server or dispatcher has already split
91
+ `SCRIPT_NAME` from `PATH_INFO`. Route on `PATH_INFO`, build links from
92
+ `SCRIPT_NAME`.
93
+ - **Links are built from the discovered prefix**, plus `X-Forwarded-Prefix`
94
+ when a proxy stripped one, never from a hard-coded `/api/v1`.
95
+ - **Never mutate the host's `scope` or `environ`.** Copy first.
96
+
97
+ Pecan is the awkward one: it does not rewrite `PATH_INFO`, so the mount point
98
+ is recovered by taking the remainder off the end.
99
+
100
+ ### Lifecycle, without lifespan
101
+
102
+ **A mounted ASGI app never receives the lifespan scope.** The host router
103
+ handles it at the root and does not forward it to a mount. So startup cannot
104
+ hang off lifespan alone, and `Meter` owns its own lifecycle instead:
105
+
106
+ 1. **Explicit, and preferred:** `meter.start()` / `meter.stop()`, or use it as
107
+ a context manager. A host with a lifespan of its own should call these
108
+ from it, or from `AppConfig.ready()`.
109
+ 2. **From lifespan**, when our ASGI app *is* the root app.
110
+ 3. **Lazily**, on the first request, with an `atexit` stop.
111
+
112
+ All three are idempotent and reference-counted, so starting from a host
113
+ lifespan *and* a first request is harmless.
114
+
115
+ ### Being a good citizen
116
+
117
+ - No module-level singletons. Every app is a factory over a `Meter`, so two
118
+ meters can coexist in one process.
119
+ - No CORS headers, no global exception handlers, no logging configuration, no
120
+ signal handlers. Those are the host's business.
121
+ - `HEAD` and `OPTIONS` answered correctly.
122
+ - An unknown path inside our own prefix returns a JSON 404 rather than
123
+ raising into the host.
124
+
125
+ ### Sync core, async edge
126
+
127
+ Handlers are plain synchronous functions returning a `MeterResponse`, so WSGI
128
+ and synchronous Django call them directly. The ASGI adapter offloads the
129
+ blocking datasource calls with `asyncio.to_thread` -- standard library, no
130
+ `anyio` dependency -- and feeds streams from an async generator.
131
+
132
+ WebSockets are declined with a clean close rather than implemented: SSE
133
+ already carries the stream, and it works over WSGI too.
134
+
135
+ **The WSGI streaming caveat**, which deployments need to know up front: a
136
+ synchronous WSGI worker holds one thread for as long as an SSE connection
137
+ stays open. Use gevent or eventlet workers for streaming, or let clients poll
138
+ `/events?since_seq=` instead.
139
+
140
+ ## 5. Multiple workers
141
+
142
+ N API workers each constructing a polling `Meter` means N pollers on the same
143
+ database. Two supported shapes:
144
+
145
+ - `Meter(poll=True)` for single-process or embedded use.
146
+ - `Meter(poll=False)` for API workers reading a store that a separate
147
+ `taskflow-meter collect` process keeps filled.
148
+
149
+ With `poll=False` over a taskflow backend there is no event history, so the
150
+ event and stream endpoints answer 501 and flow payloads omit their links --
151
+ rather than serving an empty stream that cannot be told apart from silence.
152
+
153
+ ## 6. Testing
154
+
155
+ The suite is built on the principle that a monitoring library which is only
156
+ tested against mocks has tested its own assumptions. So it runs real engines
157
+ (serial and parallel), a real sqlite logbook read from a second process, real
158
+ Pecan, Flask, Django and FastAPI applications, a real HTTP server, a real
159
+ kombu broker and a real oslo.messaging bus, and real alembic migrations
160
+ checked against the models.
161
+
162
+ | Tree | Subject |
163
+ | --- | --- |
164
+ | `tests/unit/<pkg>/test_<mod>.py` | One module, mirroring the package |
165
+ | `tests/functional/` | A behaviour across modules, including the cross-process one |
166
+ | `tests/integration/` | Real engines, and the collector deployment end to end |
167
+ | `tests/conformance/` | The ASGI and WSGI callables, and all six hosts, comparing bytes |
168
+
169
+ A unit test module must mirror its target or `tox -e pep8` fails on the
170
+ test-tree check. Anything that does not target a single module belongs in one
171
+ of the sibling trees.
@@ -4,6 +4,9 @@ How to deploy it into an OpenStack service, and how to answer the two
4
4
  questions people actually have: *how far along is this flow?* and *what is it
5
5
  doing right now?*
6
6
 
7
+ For why it is built the way it is -- what taskflow does and does not record,
8
+ and the rules an embedded app has to obey -- see [design.md](design.md).
9
+
7
10
  ---
8
11
 
9
12
  ## 1. Point it at a taskflow backend
@@ -186,6 +189,41 @@ The queue is durable and declared on publish, so a flow that starts before the
186
189
  collector does is not a lost run. Events are keyed on `(run_id, seq)`, so a
187
190
  collector that reconnects and redelivers writes nothing twice.
188
191
 
192
+ ### Or on the bus the service already has
193
+
194
+ Inside an OpenStack service, the notification bus is usually already
195
+ configured, already monitored, and already pointed at the right broker with
196
+ the right credentials. The oslo.messaging transport uses it rather than
197
+ opening a connection of its own:
198
+
199
+ ```bash
200
+ taskflow-meter collect --transport oslo-messaging --url rabbit://guest@broker// --store-url postgresql://user@host/meter
201
+ ```
202
+
203
+ ```python
204
+ from taskflow_meter.transports.oslo_messaging import OsloMessagingTransport
205
+
206
+ # No URL: the service's own transport_url decides.
207
+ publisher = OsloMessagingTransport(conf=CONF)
208
+ with attach(engine, publishers=[publisher]):
209
+ engine.run()
210
+ ```
211
+
212
+ These go out as **notifications**, not RPC -- fire-and-forget fanout, which
213
+ costs the flow nothing when no collector is running. They carry the
214
+ `taskflow_meter.events` event type on the `taskflow-meter` topic, so a
215
+ listener that also hears other traffic can pick them out. Several collectors
216
+ sharing the default pool share one queue, and each notification is handled
217
+ once rather than once per collector.
218
+
219
+ **One difference from the AMQP transport, and it decides which to pick.** A
220
+ notifier cannot declare the collector's queue -- the listener owns it -- and a
221
+ broker drops what it has nothing to route to. So events published before any
222
+ collector has *ever* run are lost. Start the collector once before the first
223
+ flow and the queue is durable from then on; restarts cost nothing. If flows
224
+ genuinely run before any collector exists, use the AMQP transport, which
225
+ declares its queue on every publish.
226
+
189
227
  Unlike the persistence datasource, which inherits taskflow's retention, this
190
228
  store keeps what it is told until told otherwise:
191
229
 
@@ -195,7 +233,7 @@ store.prune(before=time.time() - 30 * 86400)
195
233
 
196
234
  ---
197
235
 
198
- ## 3b. Watching from inside the process running the flow
236
+ ## 4. Watching from inside the process running the flow
199
237
 
200
238
  Everything above reads what taskflow persisted. If you control the code that
201
239
  runs the flow, attaching to the engine gets you two things reading cannot:
@@ -242,7 +280,7 @@ without limit would take the process down later for reasons nobody would
242
280
  connect back to monitoring. A publisher that raises is counted and logged,
243
281
  and the next batch is still attempted. None of it can fail a task.
244
282
 
245
- ## 4. How far along is this flow?
283
+ ## 5. How far along is this flow?
246
284
 
247
285
  Every flow payload carries a `completion` between 0 and 1:
248
286
 
@@ -311,7 +349,7 @@ another process within one poll interval.
311
349
 
312
350
  ---
313
351
 
314
- ## 5. What is it doing right now?
352
+ ## 6. What is it doing right now?
315
353
 
316
354
  `running_atoms` names the atoms currently executing. It is a list because
317
355
  unordered and graph flows run several at once, and it is empty between two
@@ -375,7 +413,7 @@ Two events are not flow activity and are worth handling:
375
413
 
376
414
  ---
377
415
 
378
- ## 6. What the meter cannot tell you
416
+ ## 7. What the meter cannot tell you
379
417
 
380
418
  Worth knowing before you go looking for it:
381
419