Skip to content

Commit 91eddbb

Browse files
committed
fix: http service extension
1 parent e44c7be commit 91eddbb

9 files changed

Lines changed: 87 additions & 6 deletions

File tree

.github/workflows/tests-worker.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
- name: Run tests
3737
run: |
3838
cd icij-worker
39+
uv pip install tests/test-plugin
3940
uv run --dev --all-extras --frozen pytest -vvv --cache-clear --show-capture=all -r A tests
4041
services:
4142
neo4j:
Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,50 @@
11
import importlib
2+
import warnings
3+
24
from typing import Any
35

46

5-
class VariableNotFound(ImportError):
6-
pass
7+
class VariableNotFound(ImportError): ... # pylint: disable=multiple-statements
78

89

910
def import_variable(name: str) -> Any:
11+
if ":" in name:
12+
return _import_variable(name)
13+
return _legacy_import_variable(name)
14+
15+
16+
def _import_variable(name: str) -> Any:
17+
module, variable_name = name.split(":")
18+
if not module:
19+
raise VariableNotFound(f"{name} not found in available module")
20+
try:
21+
module = importlib.import_module(module)
22+
except ModuleNotFoundError as e:
23+
raise VariableNotFound(e.msg) from e
24+
try:
25+
variable = getattr(module, variable_name)
26+
except AttributeError as e:
27+
raise VariableNotFound(e) from e
28+
return variable
29+
30+
31+
def _legacy_import_variable(name: str) -> Any:
32+
msg = (
33+
"importing using only dot will be soon deprecated,"
34+
" use the new path.to.module:variable syntax"
35+
)
36+
warnings.warn(msg, DeprecationWarning)
1037
parts = name.split(".")
1138
submodule = ".".join(parts[:-1])
39+
if not submodule:
40+
raise VariableNotFound(f"{name} not found in available module")
1241
variable_name = parts[-1]
1342
try:
1443
module = importlib.import_module(submodule)
1544
except ModuleNotFoundError as e:
1645
raise VariableNotFound(e.msg) from e
1746
try:
18-
subclass = getattr(module, variable_name)
47+
variable = getattr(module, variable_name)
1948
except AttributeError as e:
2049
raise VariableNotFound(e) from e
21-
return subclass
50+
return variable

icij-worker/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ services:
5555
PORT: 8000
5656
# Uncomment this and set it to your app path
5757
#TASK_MANAGER__APP_PATH: path.to.app_module.app_variable
58+
# Uncomment this and allow the service to reach the app code
5859

5960
healthcheck:
6061
test: curl -f http://localhost:8000/health

icij-worker/icij_worker/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,6 @@
4343

4444
from .backend import WorkerBackend
4545
from .event_publisher import EventPublisher
46+
47+
# APP hook mean to be overridden with plugins
48+
APP_HOOK = AsyncApp(name="app_hook")

icij-worker/icij_worker/app.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
import logging
33
from contextlib import asynccontextmanager
44
from copy import deepcopy
5+
from importlib.metadata import entry_points
56
from inspect import iscoroutinefunction, signature
67
from typing import Callable, final
78

89
from pydantic import BaseModel, field_validator, ConfigDict
910
from typing_extensions import Self
1011

11-
from icij_common.import_utils import import_variable
12+
from icij_common.import_utils import VariableNotFound, import_variable
1213
from icij_common.pydantic_utils import ICIJSettings, icij_config
1314
from icij_worker.routing_strategy import RoutingStrategy
1415
from icij_worker.typing_ import Dependency
@@ -194,7 +195,21 @@ def _validate_group(self, task: RegisteredTask):
194195

195196
@classmethod
196197
def load(cls, app_path: str, config: AsyncAppConfig | None = None) -> Self:
197-
app = deepcopy(import_variable(app_path))
198+
try:
199+
app = import_variable(app_path)
200+
except VariableNotFound as e:
201+
app_plugins = entry_points(group="icij_worker.APP_HOOK")
202+
for entry_point in app_plugins:
203+
if entry_point.name == app_path:
204+
app = entry_point.load()
205+
break
206+
else:
207+
msg = (
208+
f"invalid app path {app_path}, not found in available modules"
209+
f" nor in icij_worker plugins"
210+
)
211+
raise ValueError(msg) from e
212+
app = deepcopy(app)
198213
if config is not None:
199214
app.with_config(config)
200215
return app

icij-worker/tests/test-plugin/README.md

Whitespace-only changes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[project]
2+
name = "test-plugin"
3+
version = "0.1.0"
4+
description = "Test plugin"
5+
authors = [
6+
{ name = "Clément Doumouro", email = "cdoumouro@icij.org" },
7+
{ name = "ICIJ", email = "engineering@icij.org" },
8+
]
9+
readme = "README.md"
10+
requires-python = "~=3.10"
11+
dependencies = []
12+
13+
[project.entry-points."icij_worker.APP_HOOK"]
14+
plugged_app = "test_plugin:app"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from icij_worker import AsyncApp
2+
3+
app = AsyncApp(name="plugged")
4+
5+
6+
@app.task()
7+
def plugged_hello_world() -> str:
8+
return "Hello Plugged World!"

icij-worker/tests/test_app.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ def i_m_b():
3131
return app
3232

3333

34+
def test_load_from_plugin():
35+
# Given
36+
# This is the name in the plugged app registry, see tests/test-plugin/pyproject.toml
37+
path = "plugged_app"
38+
# When
39+
app = AsyncApp.load(path)
40+
assert isinstance(app, AsyncApp)
41+
assert app.name == "plugged"
42+
43+
3444
@pytest.mark.parametrize(
3545
"group,expected_keys",
3646
[("", ["i_m_a", "i_m_b"]), ("a", ["i_m_a"]), ("b", ["i_m_b"])],

0 commit comments

Comments
 (0)