frequenz-dispatch 0.4.0__tar.gz → 0.5.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.
- {frequenz-dispatch-0.4.0/src/frequenz_dispatch.egg-info → frequenz_dispatch-0.5.0}/PKG-INFO +63 -26
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/README.md +24 -24
- frequenz_dispatch-0.5.0/RELEASE_NOTES.md +18 -0
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/pyproject.toml +17 -16
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz/dispatch/__init__.py +1 -2
- frequenz_dispatch-0.5.0/src/frequenz/dispatch/_dispatch.py +71 -0
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz/dispatch/_dispatcher.py +24 -28
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz/dispatch/_managing_actor.py +22 -22
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz/dispatch/actor.py +10 -12
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0/src/frequenz_dispatch.egg-info}/PKG-INFO +63 -26
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz_dispatch.egg-info/requires.txt +10 -12
- frequenz-dispatch-0.4.0/RELEASE_NOTES.md +0 -21
- frequenz-dispatch-0.4.0/src/frequenz/dispatch/_dispatch.py +0 -272
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/LICENSE +0 -0
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/MANIFEST.in +0 -0
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/setup.cfg +0 -0
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz/dispatch/_event.py +0 -0
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz/dispatch/conftest.py +0 -0
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz/dispatch/py.typed +0 -0
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz_dispatch.egg-info/SOURCES.txt +0 -0
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz_dispatch.egg-info/dependency_links.txt +0 -0
- {frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz_dispatch.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: frequenz-dispatch
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A highlevel interface for the dispatch API
|
|
5
5
|
Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
|
|
6
6
|
License: MIT
|
|
@@ -19,15 +19,52 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
20
|
Requires-Python: <4,>=3.11
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: typing-extensions<5.0.0,>=4.11.0
|
|
24
|
+
Requires-Dist: frequenz-sdk<1.0.0-rc1500,>=1.0.0-rc1302
|
|
25
|
+
Requires-Dist: frequenz-channels<2.0.0,>=1.3.0
|
|
26
|
+
Requires-Dist: frequenz-client-dispatch<0.9.0,>=0.8.2
|
|
22
27
|
Provides-Extra: dev-flake8
|
|
28
|
+
Requires-Dist: flake8==7.1.1; extra == "dev-flake8"
|
|
29
|
+
Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
|
|
30
|
+
Requires-Dist: flake8-pyproject==1.2.3; extra == "dev-flake8"
|
|
31
|
+
Requires-Dist: pydoclint==0.5.9; extra == "dev-flake8"
|
|
32
|
+
Requires-Dist: pydocstyle==6.3.0; extra == "dev-flake8"
|
|
23
33
|
Provides-Extra: dev-formatting
|
|
34
|
+
Requires-Dist: black==24.10.0; extra == "dev-formatting"
|
|
35
|
+
Requires-Dist: isort==5.13.2; extra == "dev-formatting"
|
|
24
36
|
Provides-Extra: dev-mkdocs
|
|
37
|
+
Requires-Dist: black==24.10.0; extra == "dev-mkdocs"
|
|
38
|
+
Requires-Dist: Markdown==3.7; extra == "dev-mkdocs"
|
|
39
|
+
Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
|
|
40
|
+
Requires-Dist: mkdocs-gen-files==0.5.0; extra == "dev-mkdocs"
|
|
41
|
+
Requires-Dist: mkdocs-literate-nav==0.6.1; extra == "dev-mkdocs"
|
|
42
|
+
Requires-Dist: mkdocs-macros-plugin==1.3.7; extra == "dev-mkdocs"
|
|
43
|
+
Requires-Dist: mkdocs-material==9.5.47; extra == "dev-mkdocs"
|
|
44
|
+
Requires-Dist: mkdocstrings[python]==0.27.0; extra == "dev-mkdocs"
|
|
45
|
+
Requires-Dist: mkdocstrings-python==1.12.2; extra == "dev-mkdocs"
|
|
46
|
+
Requires-Dist: frequenz-repo-config[lib]==0.11.0; extra == "dev-mkdocs"
|
|
25
47
|
Provides-Extra: dev-mypy
|
|
48
|
+
Requires-Dist: mypy==1.13.0; extra == "dev-mypy"
|
|
49
|
+
Requires-Dist: grpc-stubs==1.53.0.5; extra == "dev-mypy"
|
|
50
|
+
Requires-Dist: types-Markdown==3.7.0.20240822; extra == "dev-mypy"
|
|
51
|
+
Requires-Dist: frequenz-dispatch[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
|
|
26
52
|
Provides-Extra: dev-noxfile
|
|
53
|
+
Requires-Dist: uv==0.5.5; extra == "dev-noxfile"
|
|
54
|
+
Requires-Dist: nox==2024.10.9; extra == "dev-noxfile"
|
|
55
|
+
Requires-Dist: frequenz-repo-config[lib]==0.11.0; extra == "dev-noxfile"
|
|
27
56
|
Provides-Extra: dev-pylint
|
|
57
|
+
Requires-Dist: pylint==3.3.2; extra == "dev-pylint"
|
|
58
|
+
Requires-Dist: frequenz-dispatch[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
|
|
28
59
|
Provides-Extra: dev-pytest
|
|
60
|
+
Requires-Dist: pytest==8.3.4; extra == "dev-pytest"
|
|
61
|
+
Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.11.0; extra == "dev-pytest"
|
|
62
|
+
Requires-Dist: pytest-mock==3.14.0; extra == "dev-pytest"
|
|
63
|
+
Requires-Dist: pytest-asyncio==0.24.0; extra == "dev-pytest"
|
|
64
|
+
Requires-Dist: async-solipsism==0.7; extra == "dev-pytest"
|
|
65
|
+
Requires-Dist: time-machine==2.16.0; extra == "dev-pytest"
|
|
29
66
|
Provides-Extra: dev
|
|
30
|
-
|
|
67
|
+
Requires-Dist: frequenz-dispatch[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
|
|
31
68
|
|
|
32
69
|
# Dispatch Highlevel Interface
|
|
33
70
|
|
|
@@ -52,7 +89,7 @@ The [`Dispatcher` class](https://frequenz-floss.github.io/frequenz-dispatch-pyth
|
|
|
52
89
|
|
|
53
90
|
```python
|
|
54
91
|
import os
|
|
55
|
-
from frequenz.dispatch import Dispatcher
|
|
92
|
+
from frequenz.dispatch import Dispatcher
|
|
56
93
|
from unittest.mock import MagicMock
|
|
57
94
|
|
|
58
95
|
async def run():
|
|
@@ -73,29 +110,29 @@ async def run():
|
|
|
73
110
|
changed_running_status_rx = dispatcher.running_status_change.new_receiver()
|
|
74
111
|
|
|
75
112
|
async for dispatch in changed_running_status_rx:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
if dispatch.type != "MY_TYPE":
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
if dispatch.started:
|
|
117
|
+
print(f"Executing dispatch {dispatch.id}, due on {dispatch.start_time}")
|
|
118
|
+
if actor.is_running:
|
|
119
|
+
actor.reconfigure(
|
|
120
|
+
components=dispatch.target,
|
|
121
|
+
run_parameters=dispatch.payload, # custom actor parameters
|
|
122
|
+
dry_run=dispatch.dry_run,
|
|
123
|
+
until=dispatch.until,
|
|
124
|
+
) # this will reconfigure the actor
|
|
125
|
+
else:
|
|
126
|
+
# this will start a new actor with the given components
|
|
127
|
+
# and run it for the duration of the dispatch
|
|
128
|
+
actor.start(
|
|
129
|
+
components=dispatch.target,
|
|
130
|
+
run_parameters=dispatch.payload, # custom actor parameters
|
|
131
|
+
dry_run=dispatch.dry_run,
|
|
132
|
+
until=dispatch.until,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
actor.stop() # this will stop the actor
|
|
99
136
|
```
|
|
100
137
|
|
|
101
138
|
## Supported Platforms
|
|
@@ -21,7 +21,7 @@ The [`Dispatcher` class](https://frequenz-floss.github.io/frequenz-dispatch-pyth
|
|
|
21
21
|
|
|
22
22
|
```python
|
|
23
23
|
import os
|
|
24
|
-
from frequenz.dispatch import Dispatcher
|
|
24
|
+
from frequenz.dispatch import Dispatcher
|
|
25
25
|
from unittest.mock import MagicMock
|
|
26
26
|
|
|
27
27
|
async def run():
|
|
@@ -42,29 +42,29 @@ async def run():
|
|
|
42
42
|
changed_running_status_rx = dispatcher.running_status_change.new_receiver()
|
|
43
43
|
|
|
44
44
|
async for dispatch in changed_running_status_rx:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
if dispatch.type != "MY_TYPE":
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if dispatch.started:
|
|
49
|
+
print(f"Executing dispatch {dispatch.id}, due on {dispatch.start_time}")
|
|
50
|
+
if actor.is_running:
|
|
51
|
+
actor.reconfigure(
|
|
52
|
+
components=dispatch.target,
|
|
53
|
+
run_parameters=dispatch.payload, # custom actor parameters
|
|
54
|
+
dry_run=dispatch.dry_run,
|
|
55
|
+
until=dispatch.until,
|
|
56
|
+
) # this will reconfigure the actor
|
|
57
|
+
else:
|
|
58
|
+
# this will start a new actor with the given components
|
|
59
|
+
# and run it for the duration of the dispatch
|
|
60
|
+
actor.start(
|
|
61
|
+
components=dispatch.target,
|
|
62
|
+
run_parameters=dispatch.payload, # custom actor parameters
|
|
63
|
+
dry_run=dispatch.dry_run,
|
|
64
|
+
until=dispatch.until,
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
actor.stop() # this will stop the actor
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
## Supported Platforms
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Dispatch Highlevel Interface Release Notes
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
<!-- Here goes a general summary of what this release is about -->
|
|
6
|
+
|
|
7
|
+
## Upgrading
|
|
8
|
+
|
|
9
|
+
* The method `Dispatch.running(type: str)` was replaced with the property `Dispatch.started: bool`.
|
|
10
|
+
* The SDK dependency was widened to allow versions up to (excluding) v1.0.0-rc1500
|
|
11
|
+
|
|
12
|
+
## New Features
|
|
13
|
+
|
|
14
|
+
<!-- Here goes the main new features and examples or instructions on how to use them -->
|
|
15
|
+
|
|
16
|
+
## Bug Fixes
|
|
17
|
+
|
|
18
|
+
* Fixed a crash when reading a Dispatch with frequency YEARLY.
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
[build-system]
|
|
5
5
|
requires = [
|
|
6
|
-
"setuptools ==
|
|
7
|
-
"setuptools_scm[toml] ==
|
|
8
|
-
"frequenz-repo-config[lib] == 0.
|
|
6
|
+
"setuptools == 75.6.0",
|
|
7
|
+
"setuptools_scm[toml] == 8.1.0",
|
|
8
|
+
"frequenz-repo-config[lib] == 0.11.0",
|
|
9
9
|
]
|
|
10
10
|
build-backend = "setuptools.build_meta"
|
|
11
11
|
|
|
@@ -34,14 +34,13 @@ classifiers = [
|
|
|
34
34
|
]
|
|
35
35
|
requires-python = ">= 3.11, < 4"
|
|
36
36
|
dependencies = [
|
|
37
|
-
"python-dateutil >= 2.8.2, < 3.0",
|
|
38
37
|
"typing-extensions >= 4.11.0, < 5.0.0",
|
|
39
38
|
# Make sure to update the version for cross-referencing also in the
|
|
40
39
|
# mkdocs.yml file when changing the version here (look for the config key
|
|
41
40
|
# plugins.mkdocstrings.handlers.python.import)
|
|
42
|
-
"frequenz-sdk >= 1.0.0-
|
|
41
|
+
"frequenz-sdk >= 1.0.0-rc1302, < 1.0.0-rc1500",
|
|
43
42
|
"frequenz-channels >= 1.3.0, < 2.0.0",
|
|
44
|
-
"frequenz-client-dispatch >= 0.8.
|
|
43
|
+
"frequenz-client-dispatch >= 0.8.2, < 0.9.0",
|
|
45
44
|
]
|
|
46
45
|
dynamic = ["version"]
|
|
47
46
|
|
|
@@ -65,32 +64,32 @@ dev-mkdocs = [
|
|
|
65
64
|
"mkdocs-gen-files == 0.5.0",
|
|
66
65
|
"mkdocs-literate-nav == 0.6.1",
|
|
67
66
|
"mkdocs-macros-plugin == 1.3.7",
|
|
68
|
-
"mkdocs-material == 9.5.
|
|
69
|
-
"mkdocstrings[python] == 0.
|
|
67
|
+
"mkdocs-material == 9.5.47",
|
|
68
|
+
"mkdocstrings[python] == 0.27.0",
|
|
70
69
|
"mkdocstrings-python == 1.12.2",
|
|
71
|
-
"frequenz-repo-config[lib] == 0.
|
|
70
|
+
"frequenz-repo-config[lib] == 0.11.0",
|
|
72
71
|
]
|
|
73
72
|
dev-mypy = [
|
|
74
73
|
"mypy == 1.13.0",
|
|
75
|
-
|
|
74
|
+
# This dependency introduces breaking changes in patch releases
|
|
75
|
+
"grpc-stubs == 1.53.0.5",
|
|
76
76
|
"types-Markdown == 3.7.0.20240822",
|
|
77
|
-
"types-python-dateutil==2.9.0.20241003",
|
|
78
77
|
# For checking the noxfile, docs/ script, and tests
|
|
79
78
|
"frequenz-dispatch[dev-mkdocs,dev-noxfile,dev-pytest]",
|
|
80
79
|
]
|
|
81
80
|
dev-noxfile = [
|
|
82
|
-
"uv == 0.
|
|
81
|
+
"uv == 0.5.5",
|
|
83
82
|
"nox == 2024.10.9",
|
|
84
|
-
"frequenz-repo-config[lib] == 0.
|
|
83
|
+
"frequenz-repo-config[lib] == 0.11.0",
|
|
85
84
|
]
|
|
86
85
|
dev-pylint = [
|
|
87
|
-
"pylint == 3.3.
|
|
86
|
+
"pylint == 3.3.2",
|
|
88
87
|
# For checking the noxfile, docs/ script, and tests
|
|
89
88
|
"frequenz-dispatch[dev-mkdocs,dev-noxfile,dev-pytest]",
|
|
90
89
|
]
|
|
91
90
|
dev-pytest = [
|
|
92
|
-
"pytest == 8.3.
|
|
93
|
-
"frequenz-repo-config[extra-lint-examples] == 0.
|
|
91
|
+
"pytest == 8.3.4",
|
|
92
|
+
"frequenz-repo-config[extra-lint-examples] == 0.11.0",
|
|
94
93
|
"pytest-mock == 3.14.0",
|
|
95
94
|
"pytest-asyncio == 0.24.0",
|
|
96
95
|
"async-solipsism == 0.7",
|
|
@@ -152,6 +151,8 @@ disable = [
|
|
|
152
151
|
"unsubscriptable-object",
|
|
153
152
|
# Checked by mypy
|
|
154
153
|
"no-member",
|
|
154
|
+
"possibly-used-before-assignment",
|
|
155
|
+
"no-name-in-module",
|
|
155
156
|
# Checked by flake8
|
|
156
157
|
"f-string-without-interpolation",
|
|
157
158
|
"line-too-long",
|
|
@@ -15,7 +15,7 @@ A small overview of the most important classes in this module:
|
|
|
15
15
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
from ._dispatch import Dispatch
|
|
18
|
+
from ._dispatch import Dispatch
|
|
19
19
|
from ._dispatcher import Dispatcher, ReceiverFetcher
|
|
20
20
|
from ._event import Created, Deleted, DispatchEvent, Updated
|
|
21
21
|
from ._managing_actor import DispatchManagingActor, DispatchUpdate
|
|
@@ -28,7 +28,6 @@ __all__ = [
|
|
|
28
28
|
"ReceiverFetcher",
|
|
29
29
|
"Updated",
|
|
30
30
|
"Dispatch",
|
|
31
|
-
"RunningState",
|
|
32
31
|
"DispatchManagingActor",
|
|
33
32
|
"DispatchUpdate",
|
|
34
33
|
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# License: MIT
|
|
2
|
+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
|
|
3
|
+
|
|
4
|
+
"""Dispatch type with support for next_run calculation."""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Iterator
|
|
9
|
+
|
|
10
|
+
from frequenz.client.dispatch.types import Dispatch as BaseDispatch
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Dispatch(BaseDispatch):
|
|
15
|
+
"""Dispatch type with extra functionality."""
|
|
16
|
+
|
|
17
|
+
deleted: bool = False
|
|
18
|
+
"""Whether the dispatch is deleted."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
client_dispatch: BaseDispatch,
|
|
23
|
+
deleted: bool = False,
|
|
24
|
+
):
|
|
25
|
+
"""Initialize the dispatch.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
client_dispatch: The client dispatch.
|
|
29
|
+
deleted: Whether the dispatch is deleted.
|
|
30
|
+
"""
|
|
31
|
+
super().__init__(**client_dispatch.__dict__)
|
|
32
|
+
# Work around frozen to set deleted
|
|
33
|
+
object.__setattr__(self, "deleted", deleted)
|
|
34
|
+
|
|
35
|
+
def _set_deleted(self) -> None:
|
|
36
|
+
"""Mark the dispatch as deleted."""
|
|
37
|
+
object.__setattr__(self, "deleted", True)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def started(self) -> bool:
|
|
41
|
+
"""Check if the dispatch is started.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
True if the dispatch is started, False otherwise.
|
|
45
|
+
"""
|
|
46
|
+
if self.deleted:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
return super().started
|
|
50
|
+
|
|
51
|
+
# noqa is needed because of a bug in pydoclint that makes it think a `return` without a return
|
|
52
|
+
# value needs documenting
|
|
53
|
+
def missed_runs(self, since: datetime) -> Iterator[datetime]: # noqa: DOC405
|
|
54
|
+
"""Yield all missed runs of a dispatch.
|
|
55
|
+
|
|
56
|
+
Yields all missed runs of a dispatch.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
since: The time to start checking for missed runs.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A generator that yields all missed runs of a dispatch.
|
|
63
|
+
|
|
64
|
+
Yields:
|
|
65
|
+
datetime: The missed run.
|
|
66
|
+
"""
|
|
67
|
+
now = datetime.now(tz=timezone.utc)
|
|
68
|
+
|
|
69
|
+
while (next_run := self.next_run_after(since)) and next_run < now:
|
|
70
|
+
yield next_run
|
|
71
|
+
since = next_run
|
|
@@ -54,7 +54,7 @@ class Dispatcher:
|
|
|
54
54
|
Example: Processing running state change dispatches
|
|
55
55
|
```python
|
|
56
56
|
import os
|
|
57
|
-
from frequenz.dispatch import Dispatcher
|
|
57
|
+
from frequenz.dispatch import Dispatcher
|
|
58
58
|
from unittest.mock import MagicMock
|
|
59
59
|
|
|
60
60
|
async def run():
|
|
@@ -75,29 +75,29 @@ class Dispatcher:
|
|
|
75
75
|
changed_running_status = dispatcher.running_status_change.new_receiver()
|
|
76
76
|
|
|
77
77
|
async for dispatch in changed_running_status:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
78
|
+
if dispatch.type != "YOUR_DISPATCH_TYPE":
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
if dispatch.started:
|
|
82
|
+
print(f"Executing dispatch {dispatch.id}, due on {dispatch.start_time}")
|
|
83
|
+
if actor.is_running:
|
|
84
|
+
actor.reconfigure(
|
|
85
|
+
components=dispatch.target,
|
|
86
|
+
run_parameters=dispatch.payload, # custom actor parameters
|
|
87
|
+
dry_run=dispatch.dry_run,
|
|
88
|
+
until=dispatch.until,
|
|
89
|
+
) # this will reconfigure the actor
|
|
90
|
+
else:
|
|
91
|
+
# this will start a new actor with the given components
|
|
92
|
+
# and run it for the duration of the dispatch
|
|
93
|
+
actor.start(
|
|
94
|
+
components=dispatch.target,
|
|
95
|
+
run_parameters=dispatch.payload, # custom actor parameters
|
|
96
|
+
dry_run=dispatch.dry_run,
|
|
97
|
+
until=dispatch.until,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
actor.stop() # this will stop the actor
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
Example: Getting notification about dispatch lifecycle events
|
|
@@ -255,10 +255,6 @@ class Dispatcher:
|
|
|
255
255
|
- The payload changed
|
|
256
256
|
- The dispatch was deleted
|
|
257
257
|
|
|
258
|
-
Note: Reaching the end time (start_time + duration) will not
|
|
259
|
-
send a message, except when it was reached by modifying the duration.
|
|
260
|
-
|
|
261
|
-
|
|
262
258
|
Returns:
|
|
263
259
|
A new receiver for dispatches whose running status changed.
|
|
264
260
|
"""
|
{frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz/dispatch/_managing_actor.py
RENAMED
|
@@ -11,7 +11,7 @@ from frequenz.channels import Receiver, Sender
|
|
|
11
11
|
from frequenz.client.dispatch.types import TargetComponents
|
|
12
12
|
from frequenz.sdk.actor import Actor
|
|
13
13
|
|
|
14
|
-
from ._dispatch import Dispatch
|
|
14
|
+
from ._dispatch import Dispatch
|
|
15
15
|
|
|
16
16
|
_logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -121,7 +121,9 @@ class DispatchManagingActor(Actor):
|
|
|
121
121
|
"""
|
|
122
122
|
super().__init__()
|
|
123
123
|
self._dispatch_rx = running_status_receiver
|
|
124
|
-
self._actors
|
|
124
|
+
self._actors: frozenset[Actor] = frozenset(
|
|
125
|
+
[actor] if isinstance(actor, Actor) else actor
|
|
126
|
+
)
|
|
125
127
|
self._dispatch_type = dispatch_type
|
|
126
128
|
self._updates_sender = updates_sender
|
|
127
129
|
|
|
@@ -156,25 +158,23 @@ class DispatchManagingActor(Actor):
|
|
|
156
158
|
Args:
|
|
157
159
|
dispatch: The dispatch to handle.
|
|
158
160
|
"""
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
options=dispatch.payload,
|
|
172
|
-
)
|
|
161
|
+
if dispatch.type != self._dispatch_type:
|
|
162
|
+
_logger.debug("Ignoring dispatch %s", dispatch.id)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
if dispatch.started:
|
|
166
|
+
if self._updates_sender is not None:
|
|
167
|
+
_logger.info("Updated by dispatch %s", dispatch.id)
|
|
168
|
+
await self._updates_sender.send(
|
|
169
|
+
DispatchUpdate(
|
|
170
|
+
components=dispatch.target,
|
|
171
|
+
dry_run=dispatch.dry_run,
|
|
172
|
+
options=dispatch.payload,
|
|
173
173
|
)
|
|
174
|
-
|
|
175
|
-
_logger.info("Started by dispatch %s", dispatch.id)
|
|
176
|
-
self._start_actors()
|
|
177
|
-
case RunningState.DIFFERENT_TYPE:
|
|
178
|
-
_logger.debug(
|
|
179
|
-
"Unknown dispatch! Ignoring dispatch of type %s", dispatch.type
|
|
180
174
|
)
|
|
175
|
+
|
|
176
|
+
_logger.info("Started by dispatch %s", dispatch.id)
|
|
177
|
+
self._start_actors()
|
|
178
|
+
else:
|
|
179
|
+
_logger.info("Stopped by dispatch %s", dispatch.id)
|
|
180
|
+
await self._stop_actors("Dispatch stopped")
|
|
@@ -15,7 +15,7 @@ from frequenz.client.dispatch import Client
|
|
|
15
15
|
from frequenz.client.dispatch.types import Event
|
|
16
16
|
from frequenz.sdk.actor import Actor
|
|
17
17
|
|
|
18
|
-
from ._dispatch import Dispatch
|
|
18
|
+
from ._dispatch import Dispatch
|
|
19
19
|
from ._event import Created, Deleted, DispatchEvent, Updated
|
|
20
20
|
|
|
21
21
|
_logger = logging.getLogger(__name__)
|
|
@@ -126,7 +126,6 @@ class DispatchingActor(Actor):
|
|
|
126
126
|
self._dispatches.pop(dispatch.id)
|
|
127
127
|
await self._update_dispatch_schedule_and_notify(None, dispatch)
|
|
128
128
|
|
|
129
|
-
dispatch._set_deleted() # pylint: disable=protected-access
|
|
130
129
|
await self._lifecycle_updates_sender.send(
|
|
131
130
|
Deleted(dispatch=dispatch)
|
|
132
131
|
)
|
|
@@ -142,7 +141,7 @@ class DispatchingActor(Actor):
|
|
|
142
141
|
# The timer is always a tiny bit delayed, so we need to check if the
|
|
143
142
|
# actor is supposed to be running now (we're assuming it wasn't already
|
|
144
143
|
# running, as all checks are done before scheduling)
|
|
145
|
-
if dispatch.
|
|
144
|
+
if dispatch.started:
|
|
146
145
|
# If it should be running, schedule the stop event
|
|
147
146
|
self._schedule_stop(dispatch)
|
|
148
147
|
# If the actor is not running, we need to schedule the next start
|
|
@@ -193,7 +192,7 @@ class DispatchingActor(Actor):
|
|
|
193
192
|
await self._lifecycle_updates_sender.send(Deleted(dispatch=dispatch))
|
|
194
193
|
await self._update_dispatch_schedule_and_notify(None, dispatch)
|
|
195
194
|
|
|
196
|
-
# Set deleted only here as it influences the result of dispatch.
|
|
195
|
+
# Set deleted only here as it influences the result of dispatch.started
|
|
197
196
|
# which is used in above in _running_state_change
|
|
198
197
|
dispatch._set_deleted() # pylint: disable=protected-access
|
|
199
198
|
await self._lifecycle_updates_sender.send(Deleted(dispatch=dispatch))
|
|
@@ -221,8 +220,11 @@ class DispatchingActor(Actor):
|
|
|
221
220
|
if not dispatch and old_dispatch:
|
|
222
221
|
self._remove_scheduled(old_dispatch)
|
|
223
222
|
|
|
223
|
+
was_running = old_dispatch.started
|
|
224
|
+
old_dispatch._set_deleted() # pylint: disable=protected-access)
|
|
225
|
+
|
|
224
226
|
# If the dispatch was running, we need to notify
|
|
225
|
-
if
|
|
227
|
+
if was_running:
|
|
226
228
|
await self._send_running_state_change(old_dispatch)
|
|
227
229
|
|
|
228
230
|
# A new dispatch was created
|
|
@@ -230,9 +232,8 @@ class DispatchingActor(Actor):
|
|
|
230
232
|
assert not self._remove_scheduled(
|
|
231
233
|
dispatch
|
|
232
234
|
), "New dispatch already scheduled?!"
|
|
233
|
-
|
|
234
235
|
# If its currently running, send notification right away
|
|
235
|
-
if dispatch.
|
|
236
|
+
if dispatch.started:
|
|
236
237
|
await self._send_running_state_change(dispatch)
|
|
237
238
|
|
|
238
239
|
self._schedule_stop(dispatch)
|
|
@@ -249,7 +250,7 @@ class DispatchingActor(Actor):
|
|
|
249
250
|
if self._update_changed_running_state(dispatch, old_dispatch):
|
|
250
251
|
await self._send_running_state_change(dispatch)
|
|
251
252
|
|
|
252
|
-
if dispatch.
|
|
253
|
+
if dispatch.started:
|
|
253
254
|
self._schedule_stop(dispatch)
|
|
254
255
|
else:
|
|
255
256
|
self._schedule_start(dispatch)
|
|
@@ -336,7 +337,7 @@ class DispatchingActor(Actor):
|
|
|
336
337
|
"""
|
|
337
338
|
# If any of the runtime attributes changed, we need to send a message
|
|
338
339
|
runtime_state_attributes = [
|
|
339
|
-
"
|
|
340
|
+
"started",
|
|
340
341
|
"type",
|
|
341
342
|
"target",
|
|
342
343
|
"duration",
|
|
@@ -359,6 +360,3 @@ class DispatchingActor(Actor):
|
|
|
359
360
|
dispatch: The dispatch that changed.
|
|
360
361
|
"""
|
|
361
362
|
await self._running_state_change_sender.send(dispatch)
|
|
362
|
-
# Update the last sent notification time
|
|
363
|
-
# so we know if this change was already sent
|
|
364
|
-
dispatch._set_running_status_notified() # pylint: disable=protected-access
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: frequenz-dispatch
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A highlevel interface for the dispatch API
|
|
5
5
|
Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
|
|
6
6
|
License: MIT
|
|
@@ -19,15 +19,52 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
20
|
Requires-Python: <4,>=3.11
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: typing-extensions<5.0.0,>=4.11.0
|
|
24
|
+
Requires-Dist: frequenz-sdk<1.0.0-rc1500,>=1.0.0-rc1302
|
|
25
|
+
Requires-Dist: frequenz-channels<2.0.0,>=1.3.0
|
|
26
|
+
Requires-Dist: frequenz-client-dispatch<0.9.0,>=0.8.2
|
|
22
27
|
Provides-Extra: dev-flake8
|
|
28
|
+
Requires-Dist: flake8==7.1.1; extra == "dev-flake8"
|
|
29
|
+
Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
|
|
30
|
+
Requires-Dist: flake8-pyproject==1.2.3; extra == "dev-flake8"
|
|
31
|
+
Requires-Dist: pydoclint==0.5.9; extra == "dev-flake8"
|
|
32
|
+
Requires-Dist: pydocstyle==6.3.0; extra == "dev-flake8"
|
|
23
33
|
Provides-Extra: dev-formatting
|
|
34
|
+
Requires-Dist: black==24.10.0; extra == "dev-formatting"
|
|
35
|
+
Requires-Dist: isort==5.13.2; extra == "dev-formatting"
|
|
24
36
|
Provides-Extra: dev-mkdocs
|
|
37
|
+
Requires-Dist: black==24.10.0; extra == "dev-mkdocs"
|
|
38
|
+
Requires-Dist: Markdown==3.7; extra == "dev-mkdocs"
|
|
39
|
+
Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
|
|
40
|
+
Requires-Dist: mkdocs-gen-files==0.5.0; extra == "dev-mkdocs"
|
|
41
|
+
Requires-Dist: mkdocs-literate-nav==0.6.1; extra == "dev-mkdocs"
|
|
42
|
+
Requires-Dist: mkdocs-macros-plugin==1.3.7; extra == "dev-mkdocs"
|
|
43
|
+
Requires-Dist: mkdocs-material==9.5.47; extra == "dev-mkdocs"
|
|
44
|
+
Requires-Dist: mkdocstrings[python]==0.27.0; extra == "dev-mkdocs"
|
|
45
|
+
Requires-Dist: mkdocstrings-python==1.12.2; extra == "dev-mkdocs"
|
|
46
|
+
Requires-Dist: frequenz-repo-config[lib]==0.11.0; extra == "dev-mkdocs"
|
|
25
47
|
Provides-Extra: dev-mypy
|
|
48
|
+
Requires-Dist: mypy==1.13.0; extra == "dev-mypy"
|
|
49
|
+
Requires-Dist: grpc-stubs==1.53.0.5; extra == "dev-mypy"
|
|
50
|
+
Requires-Dist: types-Markdown==3.7.0.20240822; extra == "dev-mypy"
|
|
51
|
+
Requires-Dist: frequenz-dispatch[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
|
|
26
52
|
Provides-Extra: dev-noxfile
|
|
53
|
+
Requires-Dist: uv==0.5.5; extra == "dev-noxfile"
|
|
54
|
+
Requires-Dist: nox==2024.10.9; extra == "dev-noxfile"
|
|
55
|
+
Requires-Dist: frequenz-repo-config[lib]==0.11.0; extra == "dev-noxfile"
|
|
27
56
|
Provides-Extra: dev-pylint
|
|
57
|
+
Requires-Dist: pylint==3.3.2; extra == "dev-pylint"
|
|
58
|
+
Requires-Dist: frequenz-dispatch[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
|
|
28
59
|
Provides-Extra: dev-pytest
|
|
60
|
+
Requires-Dist: pytest==8.3.4; extra == "dev-pytest"
|
|
61
|
+
Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.11.0; extra == "dev-pytest"
|
|
62
|
+
Requires-Dist: pytest-mock==3.14.0; extra == "dev-pytest"
|
|
63
|
+
Requires-Dist: pytest-asyncio==0.24.0; extra == "dev-pytest"
|
|
64
|
+
Requires-Dist: async-solipsism==0.7; extra == "dev-pytest"
|
|
65
|
+
Requires-Dist: time-machine==2.16.0; extra == "dev-pytest"
|
|
29
66
|
Provides-Extra: dev
|
|
30
|
-
|
|
67
|
+
Requires-Dist: frequenz-dispatch[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
|
|
31
68
|
|
|
32
69
|
# Dispatch Highlevel Interface
|
|
33
70
|
|
|
@@ -52,7 +89,7 @@ The [`Dispatcher` class](https://frequenz-floss.github.io/frequenz-dispatch-pyth
|
|
|
52
89
|
|
|
53
90
|
```python
|
|
54
91
|
import os
|
|
55
|
-
from frequenz.dispatch import Dispatcher
|
|
92
|
+
from frequenz.dispatch import Dispatcher
|
|
56
93
|
from unittest.mock import MagicMock
|
|
57
94
|
|
|
58
95
|
async def run():
|
|
@@ -73,29 +110,29 @@ async def run():
|
|
|
73
110
|
changed_running_status_rx = dispatcher.running_status_change.new_receiver()
|
|
74
111
|
|
|
75
112
|
async for dispatch in changed_running_status_rx:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
if dispatch.type != "MY_TYPE":
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
if dispatch.started:
|
|
117
|
+
print(f"Executing dispatch {dispatch.id}, due on {dispatch.start_time}")
|
|
118
|
+
if actor.is_running:
|
|
119
|
+
actor.reconfigure(
|
|
120
|
+
components=dispatch.target,
|
|
121
|
+
run_parameters=dispatch.payload, # custom actor parameters
|
|
122
|
+
dry_run=dispatch.dry_run,
|
|
123
|
+
until=dispatch.until,
|
|
124
|
+
) # this will reconfigure the actor
|
|
125
|
+
else:
|
|
126
|
+
# this will start a new actor with the given components
|
|
127
|
+
# and run it for the duration of the dispatch
|
|
128
|
+
actor.start(
|
|
129
|
+
components=dispatch.target,
|
|
130
|
+
run_parameters=dispatch.payload, # custom actor parameters
|
|
131
|
+
dry_run=dispatch.dry_run,
|
|
132
|
+
until=dispatch.until,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
actor.stop() # this will stop the actor
|
|
99
136
|
```
|
|
100
137
|
|
|
101
138
|
## Supported Platforms
|
{frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz_dispatch.egg-info/requires.txt
RENAMED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
python-dateutil<3.0,>=2.8.2
|
|
2
1
|
typing-extensions<5.0.0,>=4.11.0
|
|
3
|
-
frequenz-sdk<1.0.0-
|
|
2
|
+
frequenz-sdk<1.0.0-rc1500,>=1.0.0-rc1302
|
|
4
3
|
frequenz-channels<2.0.0,>=1.3.0
|
|
5
|
-
frequenz-client-dispatch<0.9.0,>=0.8.
|
|
4
|
+
frequenz-client-dispatch<0.9.0,>=0.8.2
|
|
6
5
|
|
|
7
6
|
[dev]
|
|
8
7
|
frequenz-dispatch[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]
|
|
@@ -25,30 +24,29 @@ mike==2.1.3
|
|
|
25
24
|
mkdocs-gen-files==0.5.0
|
|
26
25
|
mkdocs-literate-nav==0.6.1
|
|
27
26
|
mkdocs-macros-plugin==1.3.7
|
|
28
|
-
mkdocs-material==9.5.
|
|
29
|
-
mkdocstrings[python]==0.
|
|
27
|
+
mkdocs-material==9.5.47
|
|
28
|
+
mkdocstrings[python]==0.27.0
|
|
30
29
|
mkdocstrings-python==1.12.2
|
|
31
|
-
frequenz-repo-config[lib]==0.
|
|
30
|
+
frequenz-repo-config[lib]==0.11.0
|
|
32
31
|
|
|
33
32
|
[dev-mypy]
|
|
34
33
|
mypy==1.13.0
|
|
35
34
|
grpc-stubs==1.53.0.5
|
|
36
35
|
types-Markdown==3.7.0.20240822
|
|
37
|
-
types-python-dateutil==2.9.0.20241003
|
|
38
36
|
frequenz-dispatch[dev-mkdocs,dev-noxfile,dev-pytest]
|
|
39
37
|
|
|
40
38
|
[dev-noxfile]
|
|
41
|
-
uv==0.
|
|
39
|
+
uv==0.5.5
|
|
42
40
|
nox==2024.10.9
|
|
43
|
-
frequenz-repo-config[lib]==0.
|
|
41
|
+
frequenz-repo-config[lib]==0.11.0
|
|
44
42
|
|
|
45
43
|
[dev-pylint]
|
|
46
|
-
pylint==3.3.
|
|
44
|
+
pylint==3.3.2
|
|
47
45
|
frequenz-dispatch[dev-mkdocs,dev-noxfile,dev-pytest]
|
|
48
46
|
|
|
49
47
|
[dev-pytest]
|
|
50
|
-
pytest==8.3.
|
|
51
|
-
frequenz-repo-config[extra-lint-examples]==0.
|
|
48
|
+
pytest==8.3.4
|
|
49
|
+
frequenz-repo-config[extra-lint-examples]==0.11.0
|
|
52
50
|
pytest-mock==3.14.0
|
|
53
51
|
pytest-asyncio==0.24.0
|
|
54
52
|
async-solipsism==0.7
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# Dispatch Highlevel Interface Release Notes
|
|
2
|
-
|
|
3
|
-
## Summary
|
|
4
|
-
|
|
5
|
-
* Updates lots of dependencies and through those gets a few new features:
|
|
6
|
-
* `start_immediately` when creating dispatches is now supported.
|
|
7
|
-
* `http2 keepalive` is now supported and enabled by default.
|
|
8
|
-
* Some bugfixes from the channels & sdk libraries. are now included.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## Upgrading
|
|
12
|
-
|
|
13
|
-
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
|
|
14
|
-
|
|
15
|
-
## New Features
|
|
16
|
-
|
|
17
|
-
<!-- Here goes the main new features and examples or instructions on how to use them -->
|
|
18
|
-
|
|
19
|
-
## Bug Fixes
|
|
20
|
-
|
|
21
|
-
* Fixed a crash in the `DispatchManagingActor` when dispatches shared an equal start time.
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
# License: MIT
|
|
2
|
-
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
|
|
3
|
-
|
|
4
|
-
"""Dispatch type with support for next_run calculation."""
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
from datetime import datetime, timezone
|
|
10
|
-
from enum import Enum
|
|
11
|
-
from typing import Iterator, cast
|
|
12
|
-
|
|
13
|
-
from dateutil import rrule
|
|
14
|
-
from frequenz.client.dispatch.recurrence import Frequency, Weekday
|
|
15
|
-
from frequenz.client.dispatch.types import Dispatch as BaseDispatch
|
|
16
|
-
|
|
17
|
-
_logger = logging.getLogger(__name__)
|
|
18
|
-
"""The logger for this module."""
|
|
19
|
-
|
|
20
|
-
_RRULE_FREQ_MAP = {
|
|
21
|
-
Frequency.MINUTELY: rrule.MINUTELY,
|
|
22
|
-
Frequency.HOURLY: rrule.HOURLY,
|
|
23
|
-
Frequency.DAILY: rrule.DAILY,
|
|
24
|
-
Frequency.WEEKLY: rrule.WEEKLY,
|
|
25
|
-
Frequency.MONTHLY: rrule.MONTHLY,
|
|
26
|
-
}
|
|
27
|
-
"""To map from our Frequency enum to the dateutil library enum."""
|
|
28
|
-
|
|
29
|
-
_RRULE_WEEKDAY_MAP = {
|
|
30
|
-
Weekday.MONDAY: rrule.MO,
|
|
31
|
-
Weekday.TUESDAY: rrule.TU,
|
|
32
|
-
Weekday.WEDNESDAY: rrule.WE,
|
|
33
|
-
Weekday.THURSDAY: rrule.TH,
|
|
34
|
-
Weekday.FRIDAY: rrule.FR,
|
|
35
|
-
Weekday.SATURDAY: rrule.SA,
|
|
36
|
-
Weekday.SUNDAY: rrule.SU,
|
|
37
|
-
}
|
|
38
|
-
"""To map from our Weekday enum to the dateutil library enum."""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class RunningState(Enum):
|
|
42
|
-
"""The running state of a dispatch."""
|
|
43
|
-
|
|
44
|
-
RUNNING = "RUNNING"
|
|
45
|
-
"""The dispatch is running."""
|
|
46
|
-
|
|
47
|
-
STOPPED = "STOPPED"
|
|
48
|
-
"""The dispatch is stopped."""
|
|
49
|
-
|
|
50
|
-
DIFFERENT_TYPE = "DIFFERENT_TYPE"
|
|
51
|
-
"""The dispatch is for a different type."""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
@dataclass(frozen=True)
|
|
55
|
-
class Dispatch(BaseDispatch):
|
|
56
|
-
"""Dispatch type with extra functionality."""
|
|
57
|
-
|
|
58
|
-
deleted: bool = False
|
|
59
|
-
"""Whether the dispatch is deleted."""
|
|
60
|
-
|
|
61
|
-
running_state_change_synced: datetime | None = None
|
|
62
|
-
"""The last time a message was sent about the running state change."""
|
|
63
|
-
|
|
64
|
-
def __init__(
|
|
65
|
-
self,
|
|
66
|
-
client_dispatch: BaseDispatch,
|
|
67
|
-
deleted: bool = False,
|
|
68
|
-
running_state_change_synced: datetime | None = None,
|
|
69
|
-
):
|
|
70
|
-
"""Initialize the dispatch.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
client_dispatch: The client dispatch.
|
|
74
|
-
deleted: Whether the dispatch is deleted.
|
|
75
|
-
running_state_change_synced: Timestamp of the last running state change message.
|
|
76
|
-
"""
|
|
77
|
-
super().__init__(**client_dispatch.__dict__)
|
|
78
|
-
# Work around frozen to set deleted
|
|
79
|
-
object.__setattr__(self, "deleted", deleted)
|
|
80
|
-
object.__setattr__(
|
|
81
|
-
self,
|
|
82
|
-
"running_state_change_synced",
|
|
83
|
-
running_state_change_synced,
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
def _set_deleted(self) -> None:
|
|
87
|
-
"""Mark the dispatch as deleted."""
|
|
88
|
-
object.__setattr__(self, "deleted", True)
|
|
89
|
-
|
|
90
|
-
@property
|
|
91
|
-
def _running_status_notified(self) -> bool:
|
|
92
|
-
"""Check that the latest running state change notification was sent.
|
|
93
|
-
|
|
94
|
-
Returns:
|
|
95
|
-
True if the latest running state change notification was sent, False otherwise.
|
|
96
|
-
"""
|
|
97
|
-
return self.running_state_change_synced == self.update_time
|
|
98
|
-
|
|
99
|
-
def _set_running_status_notified(self) -> None:
|
|
100
|
-
"""Mark the latest running state change notification as sent."""
|
|
101
|
-
object.__setattr__(self, "running_state_change_synced", self.update_time)
|
|
102
|
-
|
|
103
|
-
def running(self, type_: str) -> RunningState:
|
|
104
|
-
"""Check if the dispatch is currently supposed to be running.
|
|
105
|
-
|
|
106
|
-
Args:
|
|
107
|
-
type_: The type of the dispatch that should be running.
|
|
108
|
-
|
|
109
|
-
Returns:
|
|
110
|
-
RUNNING if the dispatch is running,
|
|
111
|
-
STOPPED if it is stopped,
|
|
112
|
-
DIFFERENT_TYPE if it is for a different type.
|
|
113
|
-
"""
|
|
114
|
-
if self.type != type_:
|
|
115
|
-
return RunningState.DIFFERENT_TYPE
|
|
116
|
-
|
|
117
|
-
if not self.active or self.deleted:
|
|
118
|
-
return RunningState.STOPPED
|
|
119
|
-
|
|
120
|
-
now = datetime.now(tz=timezone.utc)
|
|
121
|
-
|
|
122
|
-
if now < self.start_time:
|
|
123
|
-
return RunningState.STOPPED
|
|
124
|
-
# A dispatch without duration is always running once it started
|
|
125
|
-
if self.duration is None:
|
|
126
|
-
return RunningState.RUNNING
|
|
127
|
-
|
|
128
|
-
if until := self._until(now):
|
|
129
|
-
return RunningState.RUNNING if now < until else RunningState.STOPPED
|
|
130
|
-
|
|
131
|
-
return RunningState.STOPPED
|
|
132
|
-
|
|
133
|
-
@property
|
|
134
|
-
def until(self) -> datetime | None:
|
|
135
|
-
"""Time when the dispatch should end.
|
|
136
|
-
|
|
137
|
-
Returns the time that a running dispatch should end.
|
|
138
|
-
If the dispatch is not running, None is returned.
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
The time when the dispatch should end or None if the dispatch is not running.
|
|
142
|
-
"""
|
|
143
|
-
if not self.active or self.deleted:
|
|
144
|
-
return None
|
|
145
|
-
|
|
146
|
-
now = datetime.now(tz=timezone.utc)
|
|
147
|
-
return self._until(now)
|
|
148
|
-
|
|
149
|
-
@property
|
|
150
|
-
# noqa is needed because of a bug in pydoclint that makes it think a `return` without a return
|
|
151
|
-
# value needs documenting
|
|
152
|
-
def missed_runs(self) -> Iterator[datetime]: # noqa: DOC405
|
|
153
|
-
"""Yield all missed runs of a dispatch.
|
|
154
|
-
|
|
155
|
-
Yields all missed runs of a dispatch.
|
|
156
|
-
|
|
157
|
-
If a running state change notification was not sent in time
|
|
158
|
-
due to connection issues, this method will yield all missed runs
|
|
159
|
-
since the last sent notification.
|
|
160
|
-
|
|
161
|
-
Returns:
|
|
162
|
-
A generator that yields all missed runs of a dispatch.
|
|
163
|
-
"""
|
|
164
|
-
if self.update_time == self.running_state_change_synced:
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
from_time = self.update_time
|
|
168
|
-
now = datetime.now(tz=timezone.utc)
|
|
169
|
-
|
|
170
|
-
while (next_run := self.next_run_after(from_time)) and next_run < now:
|
|
171
|
-
yield next_run
|
|
172
|
-
from_time = next_run
|
|
173
|
-
|
|
174
|
-
@property
|
|
175
|
-
def next_run(self) -> datetime | None:
|
|
176
|
-
"""Calculate the next run of a dispatch.
|
|
177
|
-
|
|
178
|
-
Returns:
|
|
179
|
-
The next run of the dispatch or None if the dispatch is finished.
|
|
180
|
-
"""
|
|
181
|
-
return self.next_run_after(datetime.now(tz=timezone.utc))
|
|
182
|
-
|
|
183
|
-
def next_run_after(self, after: datetime) -> datetime | None:
|
|
184
|
-
"""Calculate the next run of a dispatch.
|
|
185
|
-
|
|
186
|
-
Args:
|
|
187
|
-
after: The time to calculate the next run from.
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
The next run of the dispatch or None if the dispatch is finished.
|
|
191
|
-
"""
|
|
192
|
-
if (
|
|
193
|
-
not self.recurrence.frequency
|
|
194
|
-
or self.recurrence.frequency == Frequency.UNSPECIFIED
|
|
195
|
-
or self.duration is None # Infinite duration
|
|
196
|
-
):
|
|
197
|
-
if after > self.start_time:
|
|
198
|
-
return None
|
|
199
|
-
return self.start_time
|
|
200
|
-
|
|
201
|
-
# Make sure no weekday is UNSPECIFIED
|
|
202
|
-
if Weekday.UNSPECIFIED in self.recurrence.byweekdays:
|
|
203
|
-
_logger.warning("Dispatch %s has UNSPECIFIED weekday, ignoring...", self.id)
|
|
204
|
-
return None
|
|
205
|
-
|
|
206
|
-
# No type information for rrule, so we need to cast
|
|
207
|
-
return cast(datetime | None, self._prepare_rrule().after(after, inc=True))
|
|
208
|
-
|
|
209
|
-
def _prepare_rrule(self) -> rrule.rrule:
|
|
210
|
-
"""Prepare the rrule object.
|
|
211
|
-
|
|
212
|
-
Returns:
|
|
213
|
-
The rrule object.
|
|
214
|
-
|
|
215
|
-
Raises:
|
|
216
|
-
ValueError: If the interval is invalid.
|
|
217
|
-
"""
|
|
218
|
-
count, until = (None, None)
|
|
219
|
-
if end := self.recurrence.end_criteria:
|
|
220
|
-
count = end.count
|
|
221
|
-
until = end.until
|
|
222
|
-
|
|
223
|
-
if self.recurrence.interval is None or self.recurrence.interval < 1:
|
|
224
|
-
raise ValueError("Interval must be at least 1")
|
|
225
|
-
|
|
226
|
-
rrule_obj = rrule.rrule(
|
|
227
|
-
freq=_RRULE_FREQ_MAP[self.recurrence.frequency],
|
|
228
|
-
dtstart=self.start_time,
|
|
229
|
-
count=count,
|
|
230
|
-
until=until,
|
|
231
|
-
byminute=self.recurrence.byminutes or None,
|
|
232
|
-
byhour=self.recurrence.byhours or None,
|
|
233
|
-
byweekday=[
|
|
234
|
-
_RRULE_WEEKDAY_MAP[weekday] for weekday in self.recurrence.byweekdays
|
|
235
|
-
]
|
|
236
|
-
or None,
|
|
237
|
-
bymonthday=self.recurrence.bymonthdays or None,
|
|
238
|
-
bymonth=self.recurrence.bymonths or None,
|
|
239
|
-
interval=self.recurrence.interval,
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
return rrule_obj
|
|
243
|
-
|
|
244
|
-
def _until(self, now: datetime) -> datetime | None:
|
|
245
|
-
"""Calculate the time when the dispatch should end.
|
|
246
|
-
|
|
247
|
-
If no previous run is found, None is returned.
|
|
248
|
-
|
|
249
|
-
Args:
|
|
250
|
-
now: The current time.
|
|
251
|
-
|
|
252
|
-
Returns:
|
|
253
|
-
The time when the dispatch should end or None if the dispatch is not running.
|
|
254
|
-
|
|
255
|
-
Raises:
|
|
256
|
-
ValueError: If the dispatch has no duration.
|
|
257
|
-
"""
|
|
258
|
-
if self.duration is None:
|
|
259
|
-
raise ValueError("_until: Dispatch has no duration")
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
not self.recurrence.frequency
|
|
263
|
-
or self.recurrence.frequency == Frequency.UNSPECIFIED
|
|
264
|
-
):
|
|
265
|
-
return self.start_time + self.duration
|
|
266
|
-
|
|
267
|
-
latest_past_start: datetime | None = self._prepare_rrule().before(now, inc=True)
|
|
268
|
-
|
|
269
|
-
if not latest_past_start:
|
|
270
|
-
return None
|
|
271
|
-
|
|
272
|
-
return latest_past_start + self.duration
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz_dispatch.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{frequenz-dispatch-0.4.0 → frequenz_dispatch-0.5.0}/src/frequenz_dispatch.egg-info/top_level.txt
RENAMED
|
File without changes
|