climate-ref-celery 0.6.3__tar.gz → 0.6.5__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.
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/PKG-INFO +1 -1
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/pyproject.toml +1 -1
- climate_ref_celery-0.6.5/src/climate_ref_celery/cli.py +172 -0
- climate_ref_celery-0.6.5/tests/unit/test_cli.py +196 -0
- climate_ref_celery-0.6.3/src/climate_ref_celery/cli.py +0 -110
- climate_ref_celery-0.6.3/tests/unit/test_cli.py +0 -124
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/.gitignore +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/LICENCE +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/NOTICE +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/README.md +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/__init__.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/app.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/celeryconf/__init__.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/celeryconf/base.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/celeryconf/dev.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/celeryconf/prod.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/executor.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/py.typed +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/tasks.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/worker_tasks.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/tests/conftest.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/tests/unit/test_app.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/tests/unit/test_executor.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/tests/unit/test_tasks.py +0 -0
- {climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/tests/unit/test_worker_tasks.py +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Managing remote celery workers
|
|
3
|
+
|
|
4
|
+
This module is used to manage remote execution workers for the Climate REF project.
|
|
5
|
+
It is added to the `ref` command line interface if the `climate-ref-celery` package is installed.
|
|
6
|
+
|
|
7
|
+
A celery worker should be run for each diagnostic provider.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import importlib.metadata
|
|
11
|
+
from warnings import warn
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
from loguru import logger
|
|
15
|
+
|
|
16
|
+
from climate_ref_celery.app import create_celery_app
|
|
17
|
+
from climate_ref_celery.tasks import register_celery_tasks
|
|
18
|
+
from climate_ref_core.providers import DiagnosticProvider
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(help=__doc__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def import_provider(provider_name: str) -> DiagnosticProvider:
|
|
24
|
+
"""
|
|
25
|
+
Import the provider using the name of a registered provider.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
provider_name:
|
|
30
|
+
The name of a registered provider.
|
|
31
|
+
|
|
32
|
+
Packages can register a provider by defining an
|
|
33
|
+
[entry point](https://packaging.python.org/en/latest/specifications/entry-points/)
|
|
34
|
+
in its `pyproject.toml` file under the group `"climate-ref.providers"`.
|
|
35
|
+
|
|
36
|
+
Example: 'climate_ref_esmvaltool:provider' would require a section in the `pyproject.toml` for the
|
|
37
|
+
`climate_ref_esmvaltool` package like this:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
[project.entry-points."climate-ref.providers"]
|
|
41
|
+
esmvaltool = "climate_ref_esmvaltool:provider"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`"esmvaltool"` or ("climate_ref_esmvaltool:provider")
|
|
45
|
+
can then be used as the `provider_name` argument.
|
|
46
|
+
|
|
47
|
+
If the entry point is not found, an error will be raised
|
|
48
|
+
and the list of available providers will be shown.
|
|
49
|
+
|
|
50
|
+
Raises
|
|
51
|
+
------
|
|
52
|
+
typer.Abort
|
|
53
|
+
If the provider_package does not define a 'provider' variable
|
|
54
|
+
|
|
55
|
+
If the provider_package is not found
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
:
|
|
60
|
+
The provider instance
|
|
61
|
+
"""
|
|
62
|
+
provider_entry_points = importlib.metadata.entry_points(group="climate-ref.providers")
|
|
63
|
+
for entry_point in provider_entry_points:
|
|
64
|
+
logger.debug(f"found entry point: {entry_point}")
|
|
65
|
+
|
|
66
|
+
# Also support the case where the entrypoint definition ('name:provider') is supplied
|
|
67
|
+
if entry_point.name == provider_name or entry_point.value == provider_name: # noqa: PLR1714
|
|
68
|
+
break
|
|
69
|
+
else:
|
|
70
|
+
found_entry_points = ", ".join(f"{ep.name} ({ep.value})" for ep in provider_entry_points)
|
|
71
|
+
if len(found_entry_points) == 0:
|
|
72
|
+
found_entry_points = "[]"
|
|
73
|
+
|
|
74
|
+
typer.echo(
|
|
75
|
+
f"No entry point named {provider_name!r} was found. Found entry points: {found_entry_points}."
|
|
76
|
+
)
|
|
77
|
+
raise typer.Abort()
|
|
78
|
+
|
|
79
|
+
# Get the provider from the provider_package
|
|
80
|
+
try:
|
|
81
|
+
provider = entry_point.load()
|
|
82
|
+
except ModuleNotFoundError:
|
|
83
|
+
_split = entry_point.value.split(":", 1)
|
|
84
|
+
typer.echo(f"Invalid entrypoint {entry_point}: Package {_split[0]!r} not found.")
|
|
85
|
+
raise typer.Abort()
|
|
86
|
+
except AttributeError:
|
|
87
|
+
_split = entry_point.value.split(":", 1)
|
|
88
|
+
typer.echo(
|
|
89
|
+
f"Invalid entrypoint {entry_point}: {_split[0]!r} does not define a {_split[1]!r} attribute."
|
|
90
|
+
)
|
|
91
|
+
raise typer.Abort()
|
|
92
|
+
|
|
93
|
+
if not isinstance(provider, DiagnosticProvider):
|
|
94
|
+
typer.echo(f"Expected DiagnosticProvider, got {type(provider)}")
|
|
95
|
+
raise typer.Abort()
|
|
96
|
+
return provider
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command()
|
|
100
|
+
def start_worker(
|
|
101
|
+
ctx: typer.Context,
|
|
102
|
+
loglevel: str = typer.Option("info", help="Log level for the worker"),
|
|
103
|
+
provider: list[str] | None = typer.Option(
|
|
104
|
+
help="Name of the provider to start a worker for. This argument may be supplied multiple times. "
|
|
105
|
+
"If no provider is given, the worker will consume the default queue.",
|
|
106
|
+
default=None,
|
|
107
|
+
),
|
|
108
|
+
package: str | None = typer.Option(help="Deprecated. Use provider instead", default=None),
|
|
109
|
+
extra_args: list[str] = typer.Argument(None, help="Additional arguments for the worker"),
|
|
110
|
+
) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Start a Celery worker for the given provider.
|
|
113
|
+
|
|
114
|
+
A celery worker enables the execution of tasks in the background on multiple different nodes.
|
|
115
|
+
This worker will register a celery task for each diagnostic in the provider.
|
|
116
|
+
The worker tasks can be executed by sending a celery task with the name
|
|
117
|
+
'{package_slug}_{diagnostic_slug}'.
|
|
118
|
+
|
|
119
|
+
Providers must be registered as entry points in the `pyproject.toml` file of the package.
|
|
120
|
+
The entry point should be defined under the group `climate-ref.providers`
|
|
121
|
+
(See `import_provider` for details).
|
|
122
|
+
"""
|
|
123
|
+
# Create a new celery app
|
|
124
|
+
celery_app = create_celery_app("climate_ref_celery")
|
|
125
|
+
|
|
126
|
+
if package:
|
|
127
|
+
msg = "The '--package' argument is deprecated. Use '--provider' instead."
|
|
128
|
+
# Deprecation warning for package argument
|
|
129
|
+
warn(
|
|
130
|
+
msg,
|
|
131
|
+
DeprecationWarning,
|
|
132
|
+
stacklevel=2,
|
|
133
|
+
)
|
|
134
|
+
typer.echo(msg)
|
|
135
|
+
# Assume the package is the provider
|
|
136
|
+
provider = [package + ":provider"]
|
|
137
|
+
|
|
138
|
+
queues = []
|
|
139
|
+
if provider:
|
|
140
|
+
for p in provider:
|
|
141
|
+
# Attempt to import the provider
|
|
142
|
+
provider_instance = import_provider(p)
|
|
143
|
+
|
|
144
|
+
if hasattr(ctx.obj, "config"):
|
|
145
|
+
# Configure the provider so that it knows where the conda environments are
|
|
146
|
+
provider_instance.configure(ctx.obj.config)
|
|
147
|
+
|
|
148
|
+
# Wrap each diagnostics in the provider with a celery tasks
|
|
149
|
+
register_celery_tasks(celery_app, provider_instance)
|
|
150
|
+
queues.append(provider_instance.slug)
|
|
151
|
+
else:
|
|
152
|
+
# This might need some tweaking in later PRs to pull in the appropriate tasks
|
|
153
|
+
import climate_ref_celery.worker_tasks # noqa: F401
|
|
154
|
+
|
|
155
|
+
queues.append("celery")
|
|
156
|
+
|
|
157
|
+
argv = ["worker", "-E", f"--loglevel={loglevel}", f"--queues={','.join(queues)}", *(extra_args or [])]
|
|
158
|
+
celery_app.worker_main(argv=argv)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.command()
|
|
162
|
+
def list_config() -> None:
|
|
163
|
+
"""
|
|
164
|
+
List the celery configuration
|
|
165
|
+
"""
|
|
166
|
+
celery_app = create_celery_app("climate_ref_celery")
|
|
167
|
+
|
|
168
|
+
print(celery_app.conf.humanize())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__": # pragma: no cover
|
|
172
|
+
app()
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from climate_ref_celery.cli import app
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from climate_ref_core.providers import DiagnosticProvider
|
|
8
|
+
|
|
9
|
+
runner = CliRunner()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_cli_help():
|
|
13
|
+
result = runner.invoke(app, ["--help"])
|
|
14
|
+
assert result.exit_code == 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_create_celery_app(mocker):
|
|
19
|
+
return mocker.patch("climate_ref_celery.cli.create_celery_app")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def mock_register_celery_tasks(mocker):
|
|
24
|
+
return mocker.patch("climate_ref_celery.cli.register_celery_tasks")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.parametrize("provider", ["test_package", "test_package:provider"])
|
|
28
|
+
def test_start_worker_success(mocker, mock_create_celery_app, mock_register_celery_tasks, provider):
|
|
29
|
+
mock_celery_app = mock_create_celery_app.return_value
|
|
30
|
+
mock_provider = mocker.MagicMock(spec=DiagnosticProvider)
|
|
31
|
+
mock_provider.slug = "example"
|
|
32
|
+
|
|
33
|
+
mock_entry_point = mocker.Mock(spec=importlib.metadata.EntryPoint)
|
|
34
|
+
mock_entry_point.name = "test_package"
|
|
35
|
+
mock_entry_point.value = "test_package:provider"
|
|
36
|
+
mock_entry_point.load.return_value = mock_provider
|
|
37
|
+
mock_entry_points = mocker.patch("importlib.metadata.entry_points", return_value=[mock_entry_point])
|
|
38
|
+
|
|
39
|
+
result = runner.invoke(app, ["start-worker", "--provider", provider])
|
|
40
|
+
|
|
41
|
+
assert result.exit_code == 0
|
|
42
|
+
mock_entry_points.assert_called_once_with(group="climate-ref.providers")
|
|
43
|
+
mock_register_celery_tasks.assert_called_once_with(mock_create_celery_app.return_value, mock_provider)
|
|
44
|
+
mock_celery_app.worker_main.assert_called_once_with(
|
|
45
|
+
argv=["worker", "-E", "--loglevel=info", "--queues=example"]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_start_worker_multiple(mocker, mock_create_celery_app, mock_register_celery_tasks):
|
|
50
|
+
mock_celery_app = mock_create_celery_app.return_value
|
|
51
|
+
|
|
52
|
+
mock_provider_a = mocker.MagicMock(spec=DiagnosticProvider)
|
|
53
|
+
mock_provider_a.slug = "example"
|
|
54
|
+
mock_entry_point_a = mocker.Mock(spec=importlib.metadata.EntryPoint)
|
|
55
|
+
mock_entry_point_a.name = "test_package"
|
|
56
|
+
mock_entry_point_a.value = "test_package:provider"
|
|
57
|
+
mock_entry_point_a.load.return_value = mock_provider_a
|
|
58
|
+
|
|
59
|
+
mock_provider_b = mocker.MagicMock(spec=DiagnosticProvider)
|
|
60
|
+
mock_provider_b.slug = "other"
|
|
61
|
+
mock_entry_point_b = mocker.Mock(spec=importlib.metadata.EntryPoint)
|
|
62
|
+
mock_entry_point_b.name = "other_package"
|
|
63
|
+
mock_entry_point_b.value = "other_package:provider"
|
|
64
|
+
mock_entry_point_b.load.return_value = mock_provider_b
|
|
65
|
+
|
|
66
|
+
mock_entry_points = mocker.patch(
|
|
67
|
+
"importlib.metadata.entry_points", return_value=[mock_entry_point_a, mock_entry_point_b]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
result = runner.invoke(app, ["start-worker", "--provider", "test_package", "--provider", "other_package"])
|
|
71
|
+
|
|
72
|
+
assert result.exit_code == 0
|
|
73
|
+
mock_entry_points.assert_called_with(group="climate-ref.providers")
|
|
74
|
+
mock_register_celery_tasks.assert_any_call(mock_create_celery_app.return_value, mock_provider_a)
|
|
75
|
+
mock_register_celery_tasks.assert_any_call(mock_create_celery_app.return_value, mock_provider_b)
|
|
76
|
+
mock_celery_app.worker_main.assert_called_once_with(
|
|
77
|
+
argv=["worker", "-E", "--loglevel=info", "--queues=example,other"]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_start_core_worker_success(mock_create_celery_app, mock_register_celery_tasks):
|
|
82
|
+
mock_celery_app = mock_create_celery_app.return_value
|
|
83
|
+
|
|
84
|
+
result = runner.invoke(app, ["start-worker"])
|
|
85
|
+
|
|
86
|
+
assert result.exit_code == 0
|
|
87
|
+
mock_celery_app.worker_main.assert_called_once_with(
|
|
88
|
+
argv=["worker", "-E", "--loglevel=info", "--queues=celery"]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_start_worker_success_extra_args(mocker, mock_create_celery_app, mock_register_celery_tasks):
|
|
93
|
+
mock_worker_main = mock_create_celery_app.return_value
|
|
94
|
+
mock_provider = mocker.MagicMock(spec=DiagnosticProvider)
|
|
95
|
+
mock_provider.slug = "example"
|
|
96
|
+
|
|
97
|
+
mock_entry_point = mocker.Mock(spec=importlib.metadata.EntryPoint)
|
|
98
|
+
mock_entry_point.name = "test_package"
|
|
99
|
+
mock_entry_point.load.return_value = mock_provider
|
|
100
|
+
mocker.patch("importlib.metadata.entry_points", return_value=[mock_entry_point])
|
|
101
|
+
|
|
102
|
+
result = runner.invoke(
|
|
103
|
+
app,
|
|
104
|
+
[
|
|
105
|
+
"start-worker",
|
|
106
|
+
"--loglevel",
|
|
107
|
+
"error",
|
|
108
|
+
"--provider",
|
|
109
|
+
"test_package",
|
|
110
|
+
"--",
|
|
111
|
+
"--extra-args",
|
|
112
|
+
"--concurrency=2",
|
|
113
|
+
],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert result.exit_code == 0, result.output
|
|
117
|
+
mock_worker_main.worker_main.assert_called_once_with(
|
|
118
|
+
argv=["worker", "-E", "--loglevel=error", "--queues=example", "--extra-args", "--concurrency=2"]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_start_worker_package_not_registered(mocker, mock_create_celery_app):
|
|
123
|
+
mocker.patch("importlib.metadata.entry_points", return_value=[])
|
|
124
|
+
|
|
125
|
+
result = runner.invoke(app, ["start-worker", "--provider", "unregistered_package"])
|
|
126
|
+
|
|
127
|
+
assert result.exit_code == 1
|
|
128
|
+
assert "No entry point named 'unregistered_package' was found" in result.output
|
|
129
|
+
assert "Found entry points: []" in result.output
|
|
130
|
+
mock_create_celery_app.assert_called_once_with("climate_ref_celery")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_start_worker_package_not_found(mocker, mock_create_celery_app):
|
|
134
|
+
mock_entry_point = mocker.Mock(spec=importlib.metadata.EntryPoint)
|
|
135
|
+
mock_entry_point.name = "missing_package"
|
|
136
|
+
mock_entry_point.value = "missing_package:provider"
|
|
137
|
+
mock_entry_point.load.side_effect = ModuleNotFoundError
|
|
138
|
+
mock_entry_points = mocker.patch("importlib.metadata.entry_points", return_value=[mock_entry_point])
|
|
139
|
+
|
|
140
|
+
result = runner.invoke(app, ["start-worker", "--provider", "missing_package"])
|
|
141
|
+
|
|
142
|
+
assert result.exit_code == 1
|
|
143
|
+
assert "Package 'missing_package' not found" in result.output
|
|
144
|
+
mock_create_celery_app.assert_called_once_with("climate_ref_celery")
|
|
145
|
+
mock_entry_points.assert_called_once_with(group="climate-ref.providers")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_start_worker_missing_provider(mocker, mock_create_celery_app):
|
|
149
|
+
mock_entry_point = mocker.Mock(spec=importlib.metadata.EntryPoint)
|
|
150
|
+
mock_entry_point.name = "test_package"
|
|
151
|
+
mock_entry_point.value = "test_package:provider"
|
|
152
|
+
mock_entry_point.load.side_effect = AttributeError
|
|
153
|
+
mocker.patch("importlib.metadata.entry_points", return_value=[mock_entry_point])
|
|
154
|
+
|
|
155
|
+
result = runner.invoke(app, ["start-worker", "--provider", "test_package"])
|
|
156
|
+
|
|
157
|
+
assert result.exit_code == 1, result.output
|
|
158
|
+
assert "'test_package' does not define a 'provider' attribute" in result.output
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_start_worker_incorrect_provider(mocker, mock_create_celery_app):
|
|
162
|
+
# Not a DiagnosticProvider
|
|
163
|
+
mock_provider = mocker.Mock()
|
|
164
|
+
|
|
165
|
+
mock_entry_point = mocker.Mock(spec=importlib.metadata.EntryPoint)
|
|
166
|
+
mock_entry_point.name = "test_package"
|
|
167
|
+
mock_entry_point.load.return_value = mock_provider
|
|
168
|
+
mocker.patch("importlib.metadata.entry_points", return_value=[mock_entry_point])
|
|
169
|
+
|
|
170
|
+
result = runner.invoke(app, ["start-worker", "--provider", "test_package"])
|
|
171
|
+
|
|
172
|
+
assert result.exit_code == 1, result.output
|
|
173
|
+
assert "Expected DiagnosticProvider, got <class 'unittest.mock.Mock'>" in result.output
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_start_worker_deprecated_package(mocker, mock_create_celery_app):
|
|
177
|
+
mock_provider = mocker.MagicMock(spec=DiagnosticProvider)
|
|
178
|
+
mock_provider.slug = "example"
|
|
179
|
+
|
|
180
|
+
mock_entry_point = mocker.Mock(spec=importlib.metadata.EntryPoint)
|
|
181
|
+
mock_entry_point.name = "test_package"
|
|
182
|
+
mock_entry_point.value = "test_package:provider"
|
|
183
|
+
mock_entry_point.load.return_value = mock_provider
|
|
184
|
+
mocker.patch("importlib.metadata.entry_points", return_value=[mock_entry_point])
|
|
185
|
+
|
|
186
|
+
result = runner.invoke(app, ["start-worker", "--package", "test_package"])
|
|
187
|
+
|
|
188
|
+
assert result.exit_code == 0, result.output
|
|
189
|
+
assert "The '--package' argument is deprecated. Use '--provider' instead." in result.output
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_list_config():
|
|
193
|
+
result = runner.invoke(app, ["list-config"])
|
|
194
|
+
|
|
195
|
+
assert result.exit_code == 0, result.output
|
|
196
|
+
assert "broker_url: 'redis://localhost:6379/1'" in result.stdout
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Managing remote celery workers
|
|
3
|
-
|
|
4
|
-
This module is used to manage remote execution workers for the Climate REF project.
|
|
5
|
-
It is added to the `ref` command line interface if the `climate-ref-celery` package is installed.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import importlib
|
|
9
|
-
|
|
10
|
-
import typer
|
|
11
|
-
|
|
12
|
-
from climate_ref_celery.app import create_celery_app
|
|
13
|
-
from climate_ref_celery.tasks import register_celery_tasks
|
|
14
|
-
from climate_ref_core.providers import DiagnosticProvider
|
|
15
|
-
|
|
16
|
-
app = typer.Typer(help=__doc__)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def import_provider(provider_package: str) -> DiagnosticProvider:
|
|
20
|
-
"""
|
|
21
|
-
Import the provider from a given package.
|
|
22
|
-
|
|
23
|
-
Parameters
|
|
24
|
-
----------
|
|
25
|
-
provider_package:
|
|
26
|
-
The package to import the provider from
|
|
27
|
-
|
|
28
|
-
Raises
|
|
29
|
-
------
|
|
30
|
-
typer.Abort
|
|
31
|
-
If the provider_package does not define a 'provider' variable
|
|
32
|
-
|
|
33
|
-
If the provider_package is not found
|
|
34
|
-
|
|
35
|
-
Returns
|
|
36
|
-
-------
|
|
37
|
-
:
|
|
38
|
-
The provider instance
|
|
39
|
-
"""
|
|
40
|
-
try:
|
|
41
|
-
imp = importlib.import_module(provider_package.replace("-", "_"))
|
|
42
|
-
except ModuleNotFoundError:
|
|
43
|
-
typer.echo(f"Package '{provider_package}' not found")
|
|
44
|
-
raise typer.Abort()
|
|
45
|
-
|
|
46
|
-
# Get the provider from the provider_package
|
|
47
|
-
try:
|
|
48
|
-
provider = imp.provider
|
|
49
|
-
except AttributeError:
|
|
50
|
-
typer.echo("The package must define a 'provider' attribute")
|
|
51
|
-
raise typer.Abort()
|
|
52
|
-
if not isinstance(provider, DiagnosticProvider):
|
|
53
|
-
typer.echo(f"Expected DiagnosticProvider, got {type(provider)}")
|
|
54
|
-
raise typer.Abort()
|
|
55
|
-
return provider
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@app.command()
|
|
59
|
-
def start_worker(
|
|
60
|
-
ctx: typer.Context,
|
|
61
|
-
loglevel: str = typer.Option("info", help="Log level for the worker"),
|
|
62
|
-
package: str | None = typer.Option(help="Package to import tasks from", default=None),
|
|
63
|
-
extra_args: list[str] = typer.Argument(None, help="Additional arguments for the worker"),
|
|
64
|
-
) -> None:
|
|
65
|
-
"""
|
|
66
|
-
Start a Celery worker for the given package.
|
|
67
|
-
|
|
68
|
-
A celery worker enables the execution of tasks in the background on multiple different nodes.
|
|
69
|
-
This worker will register a celery task for each diagnostic in the provider.
|
|
70
|
-
The worker tasks can be executed by sending a celery task with the name
|
|
71
|
-
'{package_slug}_{diagnostic_slug}'.
|
|
72
|
-
|
|
73
|
-
The package must define a 'provider' variable that is an instance of 'ref_core.DiagnosticProvider'.
|
|
74
|
-
"""
|
|
75
|
-
# Create a new celery app
|
|
76
|
-
celery_app = create_celery_app("climate_ref_celery")
|
|
77
|
-
|
|
78
|
-
if package:
|
|
79
|
-
# Attempt to import the provider
|
|
80
|
-
provider = import_provider(package)
|
|
81
|
-
|
|
82
|
-
if hasattr(ctx.obj, "config"):
|
|
83
|
-
# Configure the provider so that it knows where the conda environments are
|
|
84
|
-
provider.configure(ctx.obj.config)
|
|
85
|
-
|
|
86
|
-
# Wrap each diagnostics in the provider with a celery tasks
|
|
87
|
-
register_celery_tasks(celery_app, provider)
|
|
88
|
-
queue = provider.slug
|
|
89
|
-
else:
|
|
90
|
-
# This might need some tweaking in later PRs to pull in the appropriate tasks
|
|
91
|
-
import climate_ref_celery.worker_tasks # noqa: F401
|
|
92
|
-
|
|
93
|
-
queue = "celery"
|
|
94
|
-
|
|
95
|
-
argv = ["worker", "-E", f"--loglevel={loglevel}", f"--queues={queue}", *(extra_args or [])]
|
|
96
|
-
celery_app.worker_main(argv=argv)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@app.command()
|
|
100
|
-
def list_config() -> None:
|
|
101
|
-
"""
|
|
102
|
-
List the celery configuration
|
|
103
|
-
"""
|
|
104
|
-
celery_app = create_celery_app("climate_ref_celery")
|
|
105
|
-
|
|
106
|
-
print(celery_app.conf.humanize())
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if __name__ == "__main__": # pragma: no cover
|
|
110
|
-
app()
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from climate_ref_celery.cli import app
|
|
3
|
-
from typer.testing import CliRunner
|
|
4
|
-
|
|
5
|
-
from climate_ref_core.providers import DiagnosticProvider
|
|
6
|
-
|
|
7
|
-
runner = CliRunner()
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_cli_help():
|
|
11
|
-
result = runner.invoke(app, ["--help"])
|
|
12
|
-
assert result.exit_code == 0
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@pytest.fixture
|
|
16
|
-
def mock_create_celery_app(mocker):
|
|
17
|
-
return mocker.patch("climate_ref_celery.cli.create_celery_app")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@pytest.fixture
|
|
21
|
-
def mock_register_celery_tasks(mocker):
|
|
22
|
-
return mocker.patch("climate_ref_celery.cli.register_celery_tasks")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_start_worker_success(mocker, mock_create_celery_app, mock_register_celery_tasks):
|
|
26
|
-
mock_celery_app = mock_create_celery_app.return_value
|
|
27
|
-
mock_provider = mocker.MagicMock(spec=DiagnosticProvider)
|
|
28
|
-
mock_provider.slug = "example"
|
|
29
|
-
|
|
30
|
-
mock_import_module = mocker.patch(
|
|
31
|
-
"importlib.import_module", return_value=mocker.Mock(provider=mock_provider)
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
result = runner.invoke(app, ["start-worker", "--package", "test_package"])
|
|
35
|
-
|
|
36
|
-
assert result.exit_code == 0
|
|
37
|
-
mock_import_module.assert_called_once_with("test_package")
|
|
38
|
-
mock_register_celery_tasks.assert_called_once_with(mock_create_celery_app.return_value, mock_provider)
|
|
39
|
-
mock_celery_app.worker_main.assert_called_once_with(
|
|
40
|
-
argv=["worker", "-E", "--loglevel=info", "--queues=example"]
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def test_start_core_worker_success(mock_create_celery_app, mock_register_celery_tasks):
|
|
45
|
-
mock_celery_app = mock_create_celery_app.return_value
|
|
46
|
-
|
|
47
|
-
result = runner.invoke(app, ["start-worker"])
|
|
48
|
-
|
|
49
|
-
assert result.exit_code == 0
|
|
50
|
-
mock_celery_app.worker_main.assert_called_once_with(
|
|
51
|
-
argv=["worker", "-E", "--loglevel=info", "--queues=celery"]
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_start_worker_success_extra_args(mocker, mock_create_celery_app, mock_register_celery_tasks):
|
|
56
|
-
mock_worker_main = mock_create_celery_app.return_value
|
|
57
|
-
mock_provider = mocker.MagicMock(spec=DiagnosticProvider)
|
|
58
|
-
mock_provider.slug = "example"
|
|
59
|
-
|
|
60
|
-
mocker.patch("importlib.import_module", return_value=mocker.Mock(provider=mock_provider))
|
|
61
|
-
|
|
62
|
-
result = runner.invoke(
|
|
63
|
-
app,
|
|
64
|
-
[
|
|
65
|
-
"start-worker",
|
|
66
|
-
"--loglevel",
|
|
67
|
-
"error",
|
|
68
|
-
"--package",
|
|
69
|
-
"test_package",
|
|
70
|
-
"--",
|
|
71
|
-
"--extra-args",
|
|
72
|
-
"--concurrency=2",
|
|
73
|
-
],
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
assert result.exit_code == 0, result.output
|
|
77
|
-
mock_worker_main.worker_main.assert_called_once_with(
|
|
78
|
-
argv=["worker", "-E", "--loglevel=error", "--queues=example", "--extra-args", "--concurrency=2"]
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def test_start_worker_package_not_found(mocker, mock_create_celery_app):
|
|
83
|
-
mock_import_module = mocker.patch("importlib.import_module", side_effect=ModuleNotFoundError)
|
|
84
|
-
|
|
85
|
-
result = runner.invoke(app, ["start-worker", "--package", "missing_package"])
|
|
86
|
-
|
|
87
|
-
assert result.exit_code == 1
|
|
88
|
-
assert "Package 'missing_package' not found" in result.output
|
|
89
|
-
mock_create_celery_app.assert_called_once_with("climate_ref_celery")
|
|
90
|
-
mock_import_module.assert_called_once_with("missing_package")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def test_start_worker_missing_provider(mocker, mock_create_celery_app):
|
|
94
|
-
mock_module = mocker.Mock()
|
|
95
|
-
del mock_module.provider
|
|
96
|
-
mock_import_module = mocker.patch("importlib.import_module", return_value=mock_module)
|
|
97
|
-
|
|
98
|
-
result = runner.invoke(app, ["start-worker", "--package", "test_package"])
|
|
99
|
-
|
|
100
|
-
assert result.exit_code == 1, result.output
|
|
101
|
-
assert "The package must define a 'provider' attribute" in result.output
|
|
102
|
-
mock_import_module.assert_called_once_with("test_package")
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def test_start_worker_incorrect_provider(mocker, mock_create_celery_app):
|
|
106
|
-
# Not a DiagnosticProvider
|
|
107
|
-
mock_provider = mocker.Mock()
|
|
108
|
-
|
|
109
|
-
mock_import_module = mocker.patch(
|
|
110
|
-
"importlib.import_module", return_value=mocker.Mock(provider=mock_provider)
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
result = runner.invoke(app, ["start-worker", "--package", "test_package"])
|
|
114
|
-
|
|
115
|
-
assert result.exit_code == 1, result.output
|
|
116
|
-
assert "Expected DiagnosticProvider, got <class 'unittest.mock.Mock'>" in result.output
|
|
117
|
-
mock_import_module.assert_called_once_with("test_package")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def test_list_config():
|
|
121
|
-
result = runner.invoke(app, ["list-config"])
|
|
122
|
-
|
|
123
|
-
assert result.exit_code == 0, result.output
|
|
124
|
-
assert "broker_url: 'redis://localhost:6379/1'" in result.stdout
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/celeryconf/__init__.py
RENAMED
|
File without changes
|
{climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/celeryconf/base.py
RENAMED
|
File without changes
|
{climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/celeryconf/dev.py
RENAMED
|
File without changes
|
{climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/celeryconf/prod.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{climate_ref_celery-0.6.3 → climate_ref_celery-0.6.5}/src/climate_ref_celery/worker_tasks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|