taskflow-meter 1.1.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.
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/PKG-INFO +16 -9
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/README.md +9 -3
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/pyproject.toml +8 -7
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/_version.py +2 -2
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/api/sse.py +4 -1
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/listener.py +20 -1
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/base.py +4 -2
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/persistence.py +5 -13
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/migrations/env.py +17 -1
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/source.py +15 -1
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/events.py +21 -3
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/fold.py +6 -1
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/meter.py +4 -2
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/models.py +20 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/base.py +6 -3
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/integration/test_attach.py +47 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/datasource/sqlalchemy/test_source.py +20 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/.gitignore +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/LICENSE +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/docs/design.md +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/docs/guide.md +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/docs/releasing.md +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/api/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/api/asgi.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/api/dispatch.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/api/http.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/api/router.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/api/routes.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/api/serializers.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/api/service.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/api/wsgi.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/cli.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/attachment.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/pipeline.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/progress.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/conf.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/django.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/fastapi.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/flask.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/paste.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/pecan.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/memory.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/migrations/script.py.mako +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/migrations/versions/0001_initial.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/models.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/diff.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/poller.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/py.typed +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/states.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/amqp.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/http.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/memory.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/oslo_messaging.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/asgi_client.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/conformance/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/conformance/test_hosts.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/conformance/test_parity.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/conftest.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/functional/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/functional/test_cross_process.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/functional/test_examples.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/functional/test_migrations.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/functional/test_packaging.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/functional/test_serve.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/functional/test_tooling.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/hosts.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/integration/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/integration/test_collector.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/api/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/api/test_asgi.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/api/test_http.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/api/test_router.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/api/test_routes.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/api/test_serializers.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/api/test_service.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/api/test_sse.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/api/test_wsgi.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/collect/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_attachment.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_listener.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_pipeline.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_progress.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/contrib/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_django.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_fastapi.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_flask.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_paste.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_pecan.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/datasource/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/datasource/sqlalchemy/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/datasource/test_base.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/datasource/test_memory.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/datasource/test_persistence.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/test_cli.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/test_conf.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/test_diff.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/test_events.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/test_meter.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/test_models.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/test_poller.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/test_states.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/transports/__init__.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/transports/test_amqp.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/transports/test_http.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/transports/test_memory.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/transports/test_oslo_messaging.py +0 -0
- {taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/wsgi_client.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: taskflow-meter
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Monitoring interfaces (ASGI, WSGI, datasource) for OpenStack TaskFlow flow execution progress
|
|
5
|
-
Project-URL: Homepage, https://github.com/daipham3213/taskflow
|
|
6
|
-
Project-URL: Source, https://github.com/daipham3213/taskflow
|
|
7
|
-
Project-URL: Issues, https://github.com/daipham3213/taskflow
|
|
8
|
-
Project-URL: Changelog, https://github.com/daipham3213/taskflow
|
|
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
9
|
Author-email: Pham Le Gia Dai <daipham.3213@gmail.com>
|
|
10
10
|
License-Expression: Apache-2.0
|
|
11
11
|
License-File: LICENSE
|
|
@@ -15,13 +15,14 @@ Classifier: Intended Audience :: Developers
|
|
|
15
15
|
Classifier: Intended Audience :: System Administrators
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
22
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
23
|
Classifier: Topic :: System :: Monitoring
|
|
23
24
|
Classifier: Typing :: Typed
|
|
24
|
-
Requires-Python: >=3.
|
|
25
|
+
Requires-Python: >=3.10
|
|
25
26
|
Requires-Dist: oslo-config>=6.9.0
|
|
26
27
|
Requires-Dist: taskflow>=4.2.0
|
|
27
28
|
Provides-Extra: all
|
|
@@ -40,6 +41,12 @@ Description-Content-Type: text/markdown
|
|
|
40
41
|
|
|
41
42
|
# taskflow-meter
|
|
42
43
|
|
|
44
|
+
[](https://github.com/daipham3213/taskflow.meter/actions/workflows/ci.yml)
|
|
45
|
+
[](https://github.com/daipham3213/taskflow.meter/actions/workflows/conformance.yml)
|
|
46
|
+
[](https://pypi.org/project/taskflow-meter/)
|
|
47
|
+
[](https://pypi.org/project/taskflow-meter/)
|
|
48
|
+
[](LICENSE)
|
|
49
|
+
|
|
43
50
|
Monitoring interfaces — ASGI, WSGI, datasources, transports — for observing
|
|
44
51
|
[OpenStack TaskFlow](https://opendev.org/openstack/taskflow) flow execution
|
|
45
52
|
progress.
|
|
@@ -200,7 +207,7 @@ serving an empty stream that cannot be told apart from silence.
|
|
|
200
207
|
|
|
201
208
|
## Requirements
|
|
202
209
|
|
|
203
|
-
- Python 3.
|
|
210
|
+
- Python 3.10+
|
|
204
211
|
- taskflow 4.2.0+
|
|
205
212
|
- oslo.config 6.9.0+
|
|
206
213
|
|
|
@@ -263,8 +270,8 @@ CI also runs the suite with every declared dependency floor installed exactly,
|
|
|
263
270
|
which is the only job that checks those floors are real. To reproduce it:
|
|
264
271
|
|
|
265
272
|
```bash
|
|
266
|
-
uv lock --python 3.
|
|
267
|
-
uv sync --python 3.
|
|
273
|
+
uv lock --python 3.10 --resolution lowest-direct
|
|
274
|
+
uv sync --python 3.10 --group dev --all-extras --resolution lowest-direct
|
|
268
275
|
uv run --frozen --no-sync pytest
|
|
269
276
|
```
|
|
270
277
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# taskflow-meter
|
|
2
2
|
|
|
3
|
+
[](https://github.com/daipham3213/taskflow.meter/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/daipham3213/taskflow.meter/actions/workflows/conformance.yml)
|
|
5
|
+
[](https://pypi.org/project/taskflow-meter/)
|
|
6
|
+
[](https://pypi.org/project/taskflow-meter/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
3
9
|
Monitoring interfaces — ASGI, WSGI, datasources, transports — for observing
|
|
4
10
|
[OpenStack TaskFlow](https://opendev.org/openstack/taskflow) flow execution
|
|
5
11
|
progress.
|
|
@@ -160,7 +166,7 @@ serving an empty stream that cannot be told apart from silence.
|
|
|
160
166
|
|
|
161
167
|
## Requirements
|
|
162
168
|
|
|
163
|
-
- Python 3.
|
|
169
|
+
- Python 3.10+
|
|
164
170
|
- taskflow 4.2.0+
|
|
165
171
|
- oslo.config 6.9.0+
|
|
166
172
|
|
|
@@ -223,8 +229,8 @@ CI also runs the suite with every declared dependency floor installed exactly,
|
|
|
223
229
|
which is the only job that checks those floors are real. To reproduce it:
|
|
224
230
|
|
|
225
231
|
```bash
|
|
226
|
-
uv lock --python 3.
|
|
227
|
-
uv sync --python 3.
|
|
232
|
+
uv lock --python 3.10 --resolution lowest-direct
|
|
233
|
+
uv sync --python 3.10 --group dev --all-extras --resolution lowest-direct
|
|
228
234
|
uv run --frozen --no-sync pytest
|
|
229
235
|
```
|
|
230
236
|
|
|
@@ -7,7 +7,7 @@ name = "taskflow-meter"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "Monitoring interfaces (ASGI, WSGI, datasource) for OpenStack TaskFlow flow execution progress"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
"Intended Audience :: System Administrators",
|
|
21
21
|
"Operating System :: OS Independent",
|
|
22
22
|
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
23
24
|
"Programming Language :: Python :: 3.11",
|
|
24
25
|
"Programming Language :: Python :: 3.12",
|
|
25
26
|
"Programming Language :: Python :: 3.13",
|
|
@@ -51,10 +52,10 @@ all = [
|
|
|
51
52
|
taskflow-meter = "taskflow_meter.cli:main"
|
|
52
53
|
|
|
53
54
|
[project.urls]
|
|
54
|
-
Homepage = "https://github.com/daipham3213/taskflow
|
|
55
|
-
Source = "https://github.com/daipham3213/taskflow
|
|
56
|
-
Issues = "https://github.com/daipham3213/taskflow
|
|
57
|
-
Changelog = "https://github.com/daipham3213/taskflow
|
|
55
|
+
Homepage = "https://github.com/daipham3213/taskflow.meter"
|
|
56
|
+
Source = "https://github.com/daipham3213/taskflow.meter"
|
|
57
|
+
Issues = "https://github.com/daipham3213/taskflow.meter/issues"
|
|
58
|
+
Changelog = "https://github.com/daipham3213/taskflow.meter/releases"
|
|
58
59
|
|
|
59
60
|
# Plugin entry points are declared as their modules land -- one pointing at a
|
|
60
61
|
# module that does not exist yet breaks discovery for the whole group.
|
|
@@ -139,7 +140,7 @@ include = [
|
|
|
139
140
|
|
|
140
141
|
[tool.ruff]
|
|
141
142
|
line-length = 79
|
|
142
|
-
target-version = "
|
|
143
|
+
target-version = "py310"
|
|
143
144
|
src = ["."]
|
|
144
145
|
|
|
145
146
|
[tool.ruff.lint]
|
|
@@ -174,7 +175,7 @@ force-single-line = true
|
|
|
174
175
|
docstring-code-format = true
|
|
175
176
|
|
|
176
177
|
[tool.mypy]
|
|
177
|
-
python_version = "3.
|
|
178
|
+
python_version = "3.10"
|
|
178
179
|
strict = true
|
|
179
180
|
warn_unreachable = true
|
|
180
181
|
files = ["taskflow_meter", "tests", "tools", "examples"]
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 2, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -218,5 +218,8 @@ async def aiter_frames(
|
|
|
218
218
|
await asyncio.sleep(interval)
|
|
219
219
|
else:
|
|
220
220
|
# Sleep, but wake immediately if the client leaves.
|
|
221
|
-
|
|
221
|
+
# asyncio.TimeoutError, spelled out: it is the builtin from
|
|
222
|
+
# 3.11 on, but a class of its own before that, so the bare
|
|
223
|
+
# name catches nothing on 3.10 and the sleep raises.
|
|
224
|
+
with contextlib.suppress(asyncio.TimeoutError):
|
|
222
225
|
await asyncio.wait_for(stop.wait(), interval)
|
|
@@ -28,8 +28,11 @@ import time
|
|
|
28
28
|
from collections.abc import Callable
|
|
29
29
|
from typing import Any
|
|
30
30
|
|
|
31
|
+
from taskflow import states
|
|
31
32
|
from taskflow.listeners import base as tf_listeners
|
|
33
|
+
from taskflow.types import failure as tf_failure
|
|
32
34
|
|
|
35
|
+
from taskflow_meter import models
|
|
33
36
|
from taskflow_meter.events import Event
|
|
34
37
|
from taskflow_meter.events import EventKind
|
|
35
38
|
from taskflow_meter.events import SequenceAllocator
|
|
@@ -100,9 +103,25 @@ class MeterListener(tf_listeners.Listener):
|
|
|
100
103
|
) -> None:
|
|
101
104
|
extra: dict[str, Any] = {}
|
|
102
105
|
if "result" in details:
|
|
106
|
+
result = details["result"]
|
|
103
107
|
# Results are arbitrary application objects and are not
|
|
104
108
|
# ours to copy around; their presence is the useful part.
|
|
105
|
-
extra["has_result"] =
|
|
109
|
+
extra["has_result"] = result is not None
|
|
110
|
+
# Except when the result *is* the failure, which is the one
|
|
111
|
+
# thing anybody watching a flow needs the detail of. Rendered
|
|
112
|
+
# in the same shape the persistence datasource reports, so a
|
|
113
|
+
# client cannot tell which producer it is reading.
|
|
114
|
+
if isinstance(result, tf_failure.Failure):
|
|
115
|
+
# Which one it is follows from the state, not from the
|
|
116
|
+
# intention: taskflow does not put an intention on these
|
|
117
|
+
# notifications, and only a revert that itself failed
|
|
118
|
+
# reaches REVERT_FAILURE.
|
|
119
|
+
key = (
|
|
120
|
+
"revert_failure"
|
|
121
|
+
if state == states.REVERT_FAILURE
|
|
122
|
+
else "failure"
|
|
123
|
+
)
|
|
124
|
+
extra[key] = models.failure_dict(result)
|
|
106
125
|
self._safely(
|
|
107
126
|
EventKind.ATOM_STATE,
|
|
108
127
|
state=state,
|
|
@@ -24,12 +24,14 @@ import abc
|
|
|
24
24
|
from collections.abc import Iterable
|
|
25
25
|
from dataclasses import dataclass
|
|
26
26
|
from types import TracebackType
|
|
27
|
-
from typing import
|
|
27
|
+
from typing import TypeVar
|
|
28
28
|
|
|
29
29
|
from taskflow_meter.events import Event
|
|
30
30
|
from taskflow_meter.models import AtomSnapshot
|
|
31
31
|
from taskflow_meter.models import FlowSnapshot
|
|
32
32
|
|
|
33
|
+
_D = TypeVar("_D", bound="DataSource")
|
|
34
|
+
|
|
33
35
|
#: Flows returned by a single unqualified listing.
|
|
34
36
|
DEFAULT_FLOW_LIMIT = 50
|
|
35
37
|
|
|
@@ -96,7 +98,7 @@ class DataSource(abc.ABC):
|
|
|
96
98
|
def stop(self) -> None: # noqa: B027 - optional hook, not abstract
|
|
97
99
|
"""Release it again. Idempotent, and safe to call unstarted."""
|
|
98
100
|
|
|
99
|
-
def __enter__(self) ->
|
|
101
|
+
def __enter__(self: _D) -> _D:
|
|
100
102
|
self.start()
|
|
101
103
|
return self
|
|
102
104
|
|
|
@@ -46,6 +46,7 @@ from taskflow import exceptions as tf_exc
|
|
|
46
46
|
from taskflow.persistence import backends as tf_backends
|
|
47
47
|
from taskflow.persistence import models as tf_models
|
|
48
48
|
|
|
49
|
+
from taskflow_meter import models
|
|
49
50
|
from taskflow_meter.datasource.base import DEFAULT_EVENT_LIMIT
|
|
50
51
|
from taskflow_meter.datasource.base import DEFAULT_FLOW_LIMIT
|
|
51
52
|
from taskflow_meter.datasource.base import DataSource
|
|
@@ -281,8 +282,10 @@ def _atom_snapshot(detail: Any) -> AtomSnapshot:
|
|
|
281
282
|
intention=detail.intention,
|
|
282
283
|
progress=float(meta.get(_META_PROGRESS) or 0.0),
|
|
283
284
|
progress_details=meta.get(_META_PROGRESS_DETAILS),
|
|
284
|
-
failure=
|
|
285
|
-
revert_failure=
|
|
285
|
+
failure=models.failure_dict(detail.failure),
|
|
286
|
+
revert_failure=models.failure_dict(
|
|
287
|
+
getattr(detail, "revert_failure", None)
|
|
288
|
+
),
|
|
286
289
|
# Carrying results would mean copying arbitrary application data
|
|
287
290
|
# into a monitoring path, so only their presence is reported. A
|
|
288
291
|
# task that returns None is indistinguishable from one that
|
|
@@ -292,17 +295,6 @@ def _atom_snapshot(detail: Any) -> AtomSnapshot:
|
|
|
292
295
|
)
|
|
293
296
|
|
|
294
297
|
|
|
295
|
-
def _failure_dict(failure: Any) -> dict[str, Any] | None:
|
|
296
|
-
"""Render a taskflow Failure, tolerating anything that is not one."""
|
|
297
|
-
if failure is None:
|
|
298
|
-
return None
|
|
299
|
-
try:
|
|
300
|
-
return dict(failure.to_dict())
|
|
301
|
-
except Exception:
|
|
302
|
-
LOG.warning("could not serialise a failure", exc_info=True)
|
|
303
|
-
return {"unserialisable": repr(failure)}
|
|
304
|
-
|
|
305
|
-
|
|
306
298
|
def _epoch(when: Any) -> float | None:
|
|
307
299
|
"""Convert a datetime to a float, or give up quietly."""
|
|
308
300
|
try:
|
{taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/migrations/env.py
RENAMED
|
@@ -30,6 +30,17 @@ from taskflow_meter.datasource.sqlalchemy.models import metadata
|
|
|
30
30
|
config = context.config
|
|
31
31
|
target_metadata = metadata
|
|
32
32
|
|
|
33
|
+
#: Default name of alembic's own bookkeeping table. Overridable,
|
|
34
|
+
#: because this schema may share a database with a host service that has
|
|
35
|
+
#: an alembic tree of its own -- and two trees writing one
|
|
36
|
+
#: ``alembic_version`` row each mistake the other's revision for a
|
|
37
|
+
#: missing one.
|
|
38
|
+
DEFAULT_VERSION_TABLE = "alembic_version"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _version_table() -> str:
|
|
42
|
+
return config.get_main_option("version_table", DEFAULT_VERSION_TABLE)
|
|
43
|
+
|
|
33
44
|
|
|
34
45
|
def run_migrations_offline() -> None:
|
|
35
46
|
context.configure(
|
|
@@ -37,6 +48,7 @@ def run_migrations_offline() -> None:
|
|
|
37
48
|
target_metadata=target_metadata,
|
|
38
49
|
literal_binds=True,
|
|
39
50
|
dialect_opts={"paramstyle": "named"},
|
|
51
|
+
version_table=_version_table(),
|
|
40
52
|
)
|
|
41
53
|
with context.begin_transaction():
|
|
42
54
|
context.run_migrations()
|
|
@@ -57,7 +69,11 @@ def run_migrations_online() -> None:
|
|
|
57
69
|
|
|
58
70
|
|
|
59
71
|
def _run(connection: Any) -> None:
|
|
60
|
-
context.configure(
|
|
72
|
+
context.configure(
|
|
73
|
+
connection=connection,
|
|
74
|
+
target_metadata=target_metadata,
|
|
75
|
+
version_table=_version_table(),
|
|
76
|
+
)
|
|
61
77
|
with context.begin_transaction():
|
|
62
78
|
context.run_migrations()
|
|
63
79
|
|
{taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/source.py
RENAMED
|
@@ -352,11 +352,23 @@ def _event_from_row(row: Any) -> Event:
|
|
|
352
352
|
)
|
|
353
353
|
|
|
354
354
|
|
|
355
|
-
def upgrade(
|
|
355
|
+
def upgrade(
|
|
356
|
+
url: str,
|
|
357
|
+
revision: str = "head",
|
|
358
|
+
*,
|
|
359
|
+
version_table: str | None = None,
|
|
360
|
+
) -> None:
|
|
356
361
|
"""Bring a database up to date, the way a deployment should.
|
|
357
362
|
|
|
358
363
|
Imported lazily: alembic is only needed by whoever runs migrations,
|
|
359
364
|
not by every process that reads the results.
|
|
365
|
+
|
|
366
|
+
``version_table`` renames alembic's bookkeeping table, which matters
|
|
367
|
+
when this schema shares a database with a host service that migrates
|
|
368
|
+
its own: both would otherwise keep their revision in
|
|
369
|
+
``alembic_version``, and each would read the other's as a revision it
|
|
370
|
+
has never heard of. Pass the same name on every upgrade of a given
|
|
371
|
+
database -- changing it later looks exactly like an unmigrated one.
|
|
360
372
|
"""
|
|
361
373
|
from alembic import command
|
|
362
374
|
from alembic.config import Config
|
|
@@ -364,4 +376,6 @@ def upgrade(url: str, revision: str = "head") -> None:
|
|
|
364
376
|
config = Config()
|
|
365
377
|
config.set_main_option("script_location", str(MIGRATIONS))
|
|
366
378
|
config.set_main_option("sqlalchemy.url", url)
|
|
379
|
+
if version_table is not None:
|
|
380
|
+
config.set_main_option("version_table", version_table)
|
|
367
381
|
command.upgrade(config, revision)
|
|
@@ -19,14 +19,32 @@ datasources, the API and any transport only ever handle one vocabulary.
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
+
import sys
|
|
22
23
|
import threading
|
|
23
24
|
from collections.abc import Mapping
|
|
24
25
|
from dataclasses import asdict
|
|
25
26
|
from dataclasses import dataclass
|
|
26
27
|
from dataclasses import field
|
|
27
|
-
from enum import StrEnum
|
|
28
28
|
from typing import Any
|
|
29
|
-
from typing import
|
|
29
|
+
from typing import TypeVar
|
|
30
|
+
|
|
31
|
+
if sys.version_info >= (3, 11):
|
|
32
|
+
from enum import StrEnum
|
|
33
|
+
else:
|
|
34
|
+
from enum import Enum
|
|
35
|
+
|
|
36
|
+
class StrEnum(str, Enum):
|
|
37
|
+
"""As much of 3.11's ``StrEnum`` as this module relies on.
|
|
38
|
+
|
|
39
|
+
A plain ``str, Enum`` mixin renders as ``EventKind.FLOW_STATE``
|
|
40
|
+
under ``str()``, and the value is what goes on the wire.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __str__(self) -> str:
|
|
44
|
+
return str.__str__(self)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_E = TypeVar("_E", bound="Event")
|
|
30
48
|
|
|
31
49
|
|
|
32
50
|
class EventKind(StrEnum):
|
|
@@ -77,7 +95,7 @@ class Event:
|
|
|
77
95
|
return data
|
|
78
96
|
|
|
79
97
|
@classmethod
|
|
80
|
-
def from_dict(cls, data: Mapping[str, Any]) ->
|
|
98
|
+
def from_dict(cls: type[_E], data: Mapping[str, Any]) -> _E:
|
|
81
99
|
"""Rebuild from :meth:`to_dict` output.
|
|
82
100
|
|
|
83
101
|
Unknown keys are rejected rather than dropped -- a transport
|
|
@@ -55,7 +55,12 @@ def fold(flow: FlowSnapshot, event: Event) -> FlowSnapshot:
|
|
|
55
55
|
flow = replace(flow, observed_at=max(flow.observed_at, event.ts))
|
|
56
56
|
|
|
57
57
|
if event.kind is EventKind.FLOW_STATE:
|
|
58
|
-
|
|
58
|
+
# The name arrives on these events too, and has to be taken from
|
|
59
|
+
# one of them when the run was seeded by something else: the
|
|
60
|
+
# in-process producer emits the graph before anything runs, and a
|
|
61
|
+
# structure event carries no name to seed from.
|
|
62
|
+
name = _optional_str(event.details.get("flow_name")) or flow.name
|
|
63
|
+
return replace(flow, state=event.state, name=name)
|
|
59
64
|
|
|
60
65
|
if event.kind in (EventKind.ATOM_STATE, EventKind.ATOM_PROGRESS):
|
|
61
66
|
if event.atom_name is None:
|
|
@@ -42,7 +42,7 @@ import logging
|
|
|
42
42
|
import threading
|
|
43
43
|
from collections.abc import AsyncIterator
|
|
44
44
|
from types import TracebackType
|
|
45
|
-
from typing import
|
|
45
|
+
from typing import TypeVar
|
|
46
46
|
|
|
47
47
|
from taskflow_meter.datasource.base import DEFAULT_EVENT_LIMIT
|
|
48
48
|
from taskflow_meter.datasource.base import DEFAULT_FLOW_LIMIT
|
|
@@ -58,6 +58,8 @@ from taskflow_meter.poller import Poller
|
|
|
58
58
|
|
|
59
59
|
LOG = logging.getLogger(__name__)
|
|
60
60
|
|
|
61
|
+
_M = TypeVar("_M", bound="Meter")
|
|
62
|
+
|
|
61
63
|
|
|
62
64
|
class Meter:
|
|
63
65
|
"""Owns a source, a store, a poller, and their lifecycle."""
|
|
@@ -180,7 +182,7 @@ class Meter:
|
|
|
180
182
|
self._holders = 0
|
|
181
183
|
self._stop_locked()
|
|
182
184
|
|
|
183
|
-
def __enter__(self) ->
|
|
185
|
+
def __enter__(self: _M) -> _M:
|
|
184
186
|
self.start()
|
|
185
187
|
return self
|
|
186
188
|
|
|
@@ -20,12 +20,15 @@ producer it is talking to.
|
|
|
20
20
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
+
import logging
|
|
23
24
|
from dataclasses import dataclass
|
|
24
25
|
from dataclasses import field
|
|
25
26
|
from typing import Any
|
|
26
27
|
|
|
27
28
|
from taskflow_meter import states
|
|
28
29
|
|
|
30
|
+
LOG = logging.getLogger(__name__)
|
|
31
|
+
|
|
29
32
|
TASK = "task"
|
|
30
33
|
RETRY = "retry"
|
|
31
34
|
|
|
@@ -141,3 +144,20 @@ class FlowSnapshot:
|
|
|
141
144
|
|
|
142
145
|
def _clamp(value: float, low: float = 0.0, high: float = 1.0) -> float:
|
|
143
146
|
return max(low, min(high, value))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def failure_dict(failure: Any) -> dict[str, Any] | None:
|
|
150
|
+
"""Render a taskflow ``Failure``, tolerating anything that is not one.
|
|
151
|
+
|
|
152
|
+
Lives here because both producers report failures and a client must
|
|
153
|
+
not have to tell which one it is reading: the persistence datasource
|
|
154
|
+
renders what taskflow stored, the listener renders what taskflow
|
|
155
|
+
handed it as it happened, and this is the one shape either way.
|
|
156
|
+
"""
|
|
157
|
+
if failure is None:
|
|
158
|
+
return None
|
|
159
|
+
try:
|
|
160
|
+
return dict(failure.to_dict())
|
|
161
|
+
except Exception:
|
|
162
|
+
LOG.warning("could not serialise a failure", exc_info=True)
|
|
163
|
+
return {"unserialisable": repr(failure)}
|
|
@@ -25,10 +25,13 @@ from collections.abc import Mapping
|
|
|
25
25
|
from collections.abc import Sequence
|
|
26
26
|
from types import TracebackType
|
|
27
27
|
from typing import Any
|
|
28
|
-
from typing import
|
|
28
|
+
from typing import TypeVar
|
|
29
29
|
|
|
30
30
|
from taskflow_meter.events import Event
|
|
31
31
|
|
|
32
|
+
_P = TypeVar("_P", bound="Publisher")
|
|
33
|
+
_S = TypeVar("_S", bound="Subscriber")
|
|
34
|
+
|
|
32
35
|
|
|
33
36
|
def to_payload(events: Sequence[Event]) -> dict[str, Any]:
|
|
34
37
|
"""The envelope every transport that leaves the process sends.
|
|
@@ -64,7 +67,7 @@ class Publisher(abc.ABC):
|
|
|
64
67
|
def publish(self, events: Sequence[Event]) -> None:
|
|
65
68
|
"""Send a batch. Never called with an empty one."""
|
|
66
69
|
|
|
67
|
-
def __enter__(self) ->
|
|
70
|
+
def __enter__(self: _P) -> _P:
|
|
68
71
|
self.start()
|
|
69
72
|
return self
|
|
70
73
|
|
|
@@ -108,7 +111,7 @@ class Subscriber(abc.ABC):
|
|
|
108
111
|
rather than being trapped inside one.
|
|
109
112
|
"""
|
|
110
113
|
|
|
111
|
-
def __enter__(self) ->
|
|
114
|
+
def __enter__(self: _S) -> _S:
|
|
112
115
|
self.start()
|
|
113
116
|
return self
|
|
114
117
|
|
|
@@ -327,3 +327,50 @@ def test_nothing_is_left_registered_on_the_tasks() -> None:
|
|
|
327
327
|
|
|
328
328
|
assert seen == []
|
|
329
329
|
assert threading.active_count() <= before
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@pytest.mark.integration
|
|
333
|
+
def test_the_flow_keeps_its_name_behind_the_structure_event() -> None:
|
|
334
|
+
# The graph is emitted before anything runs, so it is the event a run
|
|
335
|
+
# gets seeded from -- and it carries no flow name. The name arrives on
|
|
336
|
+
# every state event after it, and has to be taken from one of those or
|
|
337
|
+
# a client is told the flow is called "".
|
|
338
|
+
flow = linear_flow.Flow("named-flow").add(ProgressingTask("only"))
|
|
339
|
+
engine = engines.load(flow)
|
|
340
|
+
|
|
341
|
+
with attach(engine) as watched:
|
|
342
|
+
engine.run()
|
|
343
|
+
assert watched.flush(5.0)
|
|
344
|
+
assert watched.store is not None
|
|
345
|
+
snapshot = watched.store.get_flow(watched.run_id)
|
|
346
|
+
|
|
347
|
+
assert snapshot is not None
|
|
348
|
+
assert snapshot.name == "named-flow"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@pytest.mark.integration
|
|
352
|
+
def test_a_failed_task_reports_why_it_failed() -> None:
|
|
353
|
+
# The state says a task failed; the failure says what of. Reading
|
|
354
|
+
# persistence has always reported this, and watching the engine has
|
|
355
|
+
# the failure in hand as it happens.
|
|
356
|
+
class Exploding(task.Task):
|
|
357
|
+
def execute(self) -> None:
|
|
358
|
+
raise ValueError("no appliance answered")
|
|
359
|
+
|
|
360
|
+
flow = linear_flow.Flow("demo").add(Exploding("boom"))
|
|
361
|
+
engine = engines.load(flow)
|
|
362
|
+
|
|
363
|
+
with attach(engine) as watched:
|
|
364
|
+
with pytest.raises(ValueError, match="no appliance answered"):
|
|
365
|
+
engine.run()
|
|
366
|
+
assert watched.flush(5.0)
|
|
367
|
+
assert watched.store is not None
|
|
368
|
+
atoms = watched.store.get_atoms(watched.run_id)
|
|
369
|
+
|
|
370
|
+
assert atoms is not None
|
|
371
|
+
(atom,) = atoms
|
|
372
|
+
assert atom.failure is not None
|
|
373
|
+
assert "ValueError" in atom.failure["exc_type_names"]
|
|
374
|
+
assert "no appliance answered" in atom.failure["exception_str"]
|
|
375
|
+
# And the same shape the persistence datasource reports.
|
|
376
|
+
assert set(atom.failure) >= {"exc_type_names", "exception_str"}
|
{taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/tests/unit/datasource/sqlalchemy/test_source.py
RENAMED
|
@@ -374,6 +374,26 @@ def test_upgrade_is_idempotent(tmp_path: Path) -> None:
|
|
|
374
374
|
upgrade(url)
|
|
375
375
|
|
|
376
376
|
|
|
377
|
+
def test_upgrade_can_be_told_where_to_keep_its_revision(
|
|
378
|
+
tmp_path: Path,
|
|
379
|
+
) -> None:
|
|
380
|
+
# Sharing a database with a host service that migrates its own schema
|
|
381
|
+
# means sharing ``alembic_version`` unless this is passed, and each
|
|
382
|
+
# tree then reads the other's revision as one it has never heard of.
|
|
383
|
+
url = f"sqlite:///{tmp_path / 'shared.db'}"
|
|
384
|
+
upgrade(url, version_table="taskflow_meter_version")
|
|
385
|
+
|
|
386
|
+
store = SQLADataSource(url)
|
|
387
|
+
with store.engine.begin() as conn:
|
|
388
|
+
tables = set(sa.inspect(conn).get_table_names())
|
|
389
|
+
|
|
390
|
+
assert "taskflow_meter_version" in tables
|
|
391
|
+
assert "alembic_version" not in tables
|
|
392
|
+
|
|
393
|
+
# And it stays idempotent against the table it was given.
|
|
394
|
+
upgrade(url, version_table="taskflow_meter_version")
|
|
395
|
+
|
|
396
|
+
|
|
377
397
|
def test_the_source_can_be_stopped_and_reused(
|
|
378
398
|
tmp_path: Path,
|
|
379
399
|
) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{taskflow_meter-1.1.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|