ocean-runner 0.2.18__py3-none-any.whl → 0.2.23__py3-none-any.whl
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.
- ocean_runner/__init__.py +1 -1
- ocean_runner/config.py +48 -32
- ocean_runner/py.typed +0 -0
- ocean_runner/runner.py +95 -94
- {ocean_runner-0.2.18.dist-info → ocean_runner-0.2.23.dist-info}/METADATA +5 -2
- ocean_runner-0.2.23.dist-info/RECORD +8 -0
- ocean_runner-0.2.18.dist-info/RECORD +0 -7
- {ocean_runner-0.2.18.dist-info → ocean_runner-0.2.23.dist-info}/WHEEL +0 -0
- {ocean_runner-0.2.18.dist-info → ocean_runner-0.2.23.dist-info}/licenses/LICENSE +0 -0
ocean_runner/__init__.py
CHANGED
ocean_runner/config.py
CHANGED
|
@@ -1,55 +1,71 @@
|
|
|
1
|
-
import
|
|
2
|
-
from dataclasses import asdict, dataclass, field
|
|
1
|
+
from enum import StrEnum, auto
|
|
3
2
|
from logging import Logger
|
|
4
3
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
4
|
+
from typing import Generic, Sequence, TypeVar
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
from pydantic_settings import BaseSettings
|
|
8
|
+
|
|
9
|
+
InputT = TypeVar("InputT")
|
|
8
10
|
|
|
9
11
|
DEFAULT = "DEFAULT"
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
class Keys(StrEnum):
|
|
15
|
+
SECRET = auto()
|
|
16
|
+
BASE_DIR = auto()
|
|
17
|
+
TRANSFORMATION_DID = auto()
|
|
18
|
+
DIDS = auto()
|
|
15
19
|
|
|
16
|
-
base_dir: str | None = field(
|
|
17
|
-
default_factory=lambda: os.environ.get("BASE_DIR", None),
|
|
18
|
-
)
|
|
19
|
-
"""Base data directory, defaults to '/data'"""
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
class Environment(BaseSettings):
|
|
22
|
+
"""Environment configuration loaded from environment variables"""
|
|
23
|
+
|
|
24
|
+
base_dir: str | Path | None = Field(
|
|
25
|
+
default_factory=lambda: Path("/data"),
|
|
26
|
+
validation_alias=Keys.BASE_DIR.value,
|
|
27
|
+
description="Base data directory, defaults to '/data'",
|
|
23
28
|
)
|
|
24
|
-
"""Datasets DID's, format: '["XXXX"]'"""
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
default_factory=
|
|
30
|
+
dids: list[Path] | str | None = Field(
|
|
31
|
+
default_factory=list,
|
|
32
|
+
validation_alias=Keys.DIDS.value,
|
|
33
|
+
description='Datasets DID\'s, format: ["XXXX"]',
|
|
28
34
|
)
|
|
29
|
-
"""Transformation (algorithm) DID"""
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
transformation_did: str = Field(
|
|
37
|
+
default=DEFAULT,
|
|
38
|
+
validation_alias=Keys.TRANSFORMATION_DID.value,
|
|
39
|
+
description="Transformation (algorithm) DID",
|
|
33
40
|
)
|
|
34
|
-
"""Super secret secret"""
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
secret: str = Field(
|
|
43
|
+
default=DEFAULT,
|
|
44
|
+
validation_alias=Keys.SECRET.value,
|
|
45
|
+
description="Super secret secret",
|
|
46
|
+
)
|
|
37
47
|
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
class Config:
|
|
49
|
+
class Config(BaseModel, Generic[InputT]):
|
|
41
50
|
"""Algorithm overall configuration"""
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
"""Algorithm's custom input types, must be a dataclass_json"""
|
|
52
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
custom_input: InputT | None = Field(
|
|
55
|
+
default=None,
|
|
56
|
+
description="Algorithm's custom input types, must be a dataclass_json",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
logger: Logger | None = Field(
|
|
60
|
+
default=None,
|
|
61
|
+
description="Logger to use in the algorithm",
|
|
62
|
+
)
|
|
48
63
|
|
|
49
|
-
source_paths:
|
|
50
|
-
default_factory=lambda: [Path("/algorithm/src")]
|
|
64
|
+
source_paths: Sequence[Path] = Field(
|
|
65
|
+
default_factory=lambda: [Path("/algorithm/src")],
|
|
66
|
+
description="Paths that should be included so the code executes correctly",
|
|
51
67
|
)
|
|
52
|
-
"""Paths that should be included so the code executes correctly"""
|
|
53
68
|
|
|
54
|
-
environment: Environment =
|
|
55
|
-
|
|
69
|
+
environment: Environment = Field(
|
|
70
|
+
default_factory=Environment, description="Environment configuration"
|
|
71
|
+
)
|
ocean_runner/py.typed
ADDED
|
File without changes
|
ocean_runner/runner.py
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from dataclasses import InitVar, dataclass, field
|
|
4
6
|
from logging import Logger
|
|
5
7
|
from pathlib import Path
|
|
6
|
-
from typing import Callable, Generic, TypeVar
|
|
8
|
+
from typing import Awaitable, Callable, Generic, TypeAlias, TypeVar
|
|
7
9
|
|
|
8
|
-
from oceanprotocol_job_details import JobDetails
|
|
10
|
+
from oceanprotocol_job_details import load_job_details, JobDetails # type: ignore
|
|
9
11
|
|
|
10
12
|
from ocean_runner.config import Config
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
InputT = TypeVar("InputT")
|
|
13
15
|
ResultT = TypeVar("ResultT")
|
|
16
|
+
T = TypeVar("T")
|
|
14
17
|
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
Algo: TypeAlias = "Algorithm[InputT, ResultT]"
|
|
20
|
+
ValidateFuncT: TypeAlias = Callable[[Algo], None | Awaitable[None] | None]
|
|
21
|
+
RunFuncT: TypeAlias = Callable[[Algo], ResultT | Awaitable[ResultT]]
|
|
22
|
+
SaveFuncT: TypeAlias = Callable[[Algo, ResultT, Path], Awaitable[None] | None]
|
|
23
|
+
ErrorFuncT: TypeAlias = Callable[[Algo, Exception], Awaitable[None] | None]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def default_error_callback(algorithm: Algorithm, error: Exception) -> None:
|
|
17
27
|
algorithm.logger.exception("Error during algorithm execution")
|
|
18
|
-
raise
|
|
28
|
+
raise error
|
|
19
29
|
|
|
20
30
|
|
|
21
31
|
def default_validation(algorithm: Algorithm) -> None:
|
|
@@ -24,56 +34,55 @@ def default_validation(algorithm: Algorithm) -> None:
|
|
|
24
34
|
assert algorithm.job_details.files, "Files missing"
|
|
25
35
|
|
|
26
36
|
|
|
27
|
-
def default_save(
|
|
37
|
+
async def default_save(algorithm: Algorithm, result: ResultT, base: Path) -> None:
|
|
38
|
+
import aiofiles
|
|
39
|
+
|
|
28
40
|
algorithm.logger.info("Saving results using default save")
|
|
29
|
-
with open(base / "result.txt", "w+") as f:
|
|
30
|
-
f.write(str(result))
|
|
41
|
+
async with aiofiles.open(base / "result.txt", "w+") as f:
|
|
42
|
+
await f.write(str(result))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def execute(function: Callable[..., T | Awaitable[T]], *args, **kwargs) -> T:
|
|
46
|
+
result = function(*args, **kwargs)
|
|
47
|
+
|
|
48
|
+
if inspect.isawaitable(result):
|
|
49
|
+
return await result
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(slots=True)
|
|
55
|
+
class Functions(Generic[InputT, ResultT]):
|
|
56
|
+
validate: ValidateFuncT = field(default=default_validation, init=False)
|
|
57
|
+
run: RunFuncT | None = field(default=None, init=False)
|
|
58
|
+
save: SaveFuncT = field(default=default_save, init=False)
|
|
59
|
+
error: ErrorFuncT = field(default=default_error_callback, init=False)
|
|
31
60
|
|
|
32
61
|
|
|
33
62
|
@dataclass
|
|
34
|
-
class Algorithm(Generic[
|
|
63
|
+
class Algorithm(Generic[InputT, ResultT]):
|
|
35
64
|
"""
|
|
36
65
|
A configurable algorithm runner that behaves like a FastAPI app:
|
|
37
66
|
- You register `validate`, `run`, and `save_results` via decorators.
|
|
38
67
|
- You execute the full pipeline by calling `app()`.
|
|
39
68
|
"""
|
|
40
69
|
|
|
41
|
-
config: InitVar[Config | None] = None
|
|
42
|
-
logger: Logger = field(init=False)
|
|
43
|
-
_job_details: JobDetails[JobDetailsT] = field(init=False)
|
|
44
|
-
_result: ResultT | None = field(default=None, init=False)
|
|
45
|
-
|
|
46
|
-
# Decorator-registered callbacks
|
|
47
|
-
_validate_fn: Callable[[Algorithm], None] | None = field(
|
|
48
|
-
default=None,
|
|
49
|
-
init=False,
|
|
50
|
-
repr=False,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
_run_fn: Callable[[Algorithm], ResultT] | None = field(
|
|
54
|
-
default=None,
|
|
55
|
-
init=False,
|
|
56
|
-
repr=False,
|
|
57
|
-
)
|
|
70
|
+
config: InitVar[Config[InputT] | None] = field(default=None)
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
default=None,
|
|
61
|
-
init=False,
|
|
62
|
-
repr=False,
|
|
63
|
-
)
|
|
72
|
+
logger: Logger = field(init=False, repr=False)
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
repr=False
|
|
74
|
+
_job_details: JobDetails[InputT] = field(init=False)
|
|
75
|
+
_result: ResultT | None = field(default=None, init=False)
|
|
76
|
+
_functions: Functions[InputT, ResultT] = field(
|
|
77
|
+
default_factory=Functions, init=False, repr=False
|
|
69
78
|
)
|
|
70
79
|
|
|
71
|
-
def __post_init__(self, config: Config | None) -> None:
|
|
72
|
-
|
|
80
|
+
def __post_init__(self, config: Config[InputT] | None) -> None:
|
|
81
|
+
configuration = config or Config()
|
|
73
82
|
|
|
74
83
|
# Configure logger
|
|
75
|
-
if
|
|
76
|
-
self.logger =
|
|
84
|
+
if configuration.logger:
|
|
85
|
+
self.logger = configuration.logger
|
|
77
86
|
else:
|
|
78
87
|
import logging
|
|
79
88
|
|
|
@@ -82,20 +91,26 @@ class Algorithm(Generic[JobDetailsT, ResultT]):
|
|
|
82
91
|
format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
|
|
83
92
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
84
93
|
)
|
|
85
|
-
self.logger = logging.getLogger(
|
|
94
|
+
self.logger = logging.getLogger(__name__)
|
|
86
95
|
|
|
87
96
|
# Normalize base_dir
|
|
88
|
-
if isinstance(
|
|
89
|
-
|
|
97
|
+
if isinstance(configuration.environment.base_dir, str):
|
|
98
|
+
configuration.environment.base_dir = Path(
|
|
99
|
+
configuration.environment.base_dir
|
|
100
|
+
)
|
|
90
101
|
|
|
91
102
|
# Extend sys.path for custom imports
|
|
92
|
-
if
|
|
103
|
+
if configuration.source_paths:
|
|
93
104
|
import sys
|
|
94
105
|
|
|
95
|
-
sys.path.extend(
|
|
96
|
-
|
|
106
|
+
sys.path.extend(
|
|
107
|
+
[str(path.absolute()) for path in configuration.source_paths]
|
|
108
|
+
)
|
|
109
|
+
self.logger.debug(
|
|
110
|
+
f"Added [{len(configuration.source_paths)}] entries to PATH"
|
|
111
|
+
)
|
|
97
112
|
|
|
98
|
-
self.
|
|
113
|
+
self.configuration = configuration
|
|
99
114
|
|
|
100
115
|
class Error(RuntimeError): ...
|
|
101
116
|
|
|
@@ -115,76 +130,62 @@ class Algorithm(Generic[JobDetailsT, ResultT]):
|
|
|
115
130
|
# Decorators (FastAPI-style)
|
|
116
131
|
# ---------------------------
|
|
117
132
|
|
|
118
|
-
def validate(self, fn:
|
|
119
|
-
self.
|
|
133
|
+
def validate(self, fn: ValidateFuncT) -> ValidateFuncT:
|
|
134
|
+
self._functions.validate = fn
|
|
120
135
|
return fn
|
|
121
136
|
|
|
122
|
-
def run(self, fn:
|
|
123
|
-
self.
|
|
137
|
+
def run(self, fn: RunFuncT) -> RunFuncT:
|
|
138
|
+
self._functions.run = fn
|
|
124
139
|
return fn
|
|
125
140
|
|
|
126
|
-
def save_results(self, fn:
|
|
127
|
-
self.
|
|
141
|
+
def save_results(self, fn: SaveFuncT) -> SaveFuncT:
|
|
142
|
+
self._functions.save = fn
|
|
128
143
|
return fn
|
|
129
144
|
|
|
130
|
-
def on_error(self, fn:
|
|
131
|
-
self.
|
|
145
|
+
def on_error(self, fn: ErrorFuncT) -> ErrorFuncT:
|
|
146
|
+
self._functions.error = fn
|
|
132
147
|
return fn
|
|
133
148
|
|
|
134
149
|
# ---------------------------
|
|
135
150
|
# Execution Pipeline
|
|
136
151
|
# ---------------------------
|
|
137
152
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
153
|
+
async def execute(self) -> ResultT | None:
|
|
154
|
+
self._job_details = load_job_details(
|
|
155
|
+
{
|
|
156
|
+
"base_dir": self.configuration.environment.base_dir,
|
|
157
|
+
"dids": self.configuration.environment.dids or [],
|
|
158
|
+
"secret": self.configuration.environment.secret,
|
|
159
|
+
"transformation_did": self.configuration.environment.transformation_did,
|
|
160
|
+
},
|
|
161
|
+
self.configuration.custom_input,
|
|
147
162
|
)
|
|
148
163
|
|
|
149
164
|
self.logger.info("Loaded JobDetails")
|
|
150
|
-
self.logger.debug(
|
|
165
|
+
self.logger.debug(self.job_details.model_dump())
|
|
151
166
|
|
|
152
167
|
try:
|
|
153
|
-
|
|
154
|
-
if self._validate_fn:
|
|
155
|
-
self.logger.info("Running custom validation...")
|
|
156
|
-
self._validate_fn()
|
|
157
|
-
else:
|
|
158
|
-
self.logger.info("Running default validation...")
|
|
159
|
-
default_validation(self)
|
|
168
|
+
await execute(self._functions.validate, self)
|
|
160
169
|
|
|
161
|
-
|
|
162
|
-
if self._run_fn:
|
|
170
|
+
if self._functions.run:
|
|
163
171
|
self.logger.info("Running algorithm...")
|
|
164
|
-
self._result = self.
|
|
172
|
+
self._result = await execute(self._functions.run, self)
|
|
165
173
|
else:
|
|
166
|
-
self.logger.
|
|
174
|
+
self.logger.error("No run() function defined. Skipping execution.")
|
|
167
175
|
self._result = None
|
|
168
176
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
self
|
|
172
|
-
self.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
else:
|
|
177
|
-
self.logger.info("No save_results() defined. Using default.")
|
|
178
|
-
default_save(
|
|
179
|
-
result=self._result,
|
|
180
|
-
base=self.job_details.paths.outputs,
|
|
181
|
-
algorithm=self,
|
|
182
|
-
)
|
|
177
|
+
await execute(
|
|
178
|
+
self._functions.save,
|
|
179
|
+
algorithm=self,
|
|
180
|
+
result=self._result,
|
|
181
|
+
base=self.job_details.paths.outputs,
|
|
182
|
+
)
|
|
183
183
|
|
|
184
184
|
except Exception as e:
|
|
185
|
-
|
|
186
|
-
self._error_callback(e)
|
|
187
|
-
else:
|
|
188
|
-
default_error_callback(self, e)
|
|
185
|
+
await execute(self._functions.error, self, e)
|
|
189
186
|
|
|
190
187
|
return self._result
|
|
188
|
+
|
|
189
|
+
def __call__(self) -> ResultT | None:
|
|
190
|
+
"""Executes the algorithm pipeline: validate → run → save_results."""
|
|
191
|
+
return asyncio.run(self.execute())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ocean-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.23
|
|
4
4
|
Summary: A fluent API for OceanProtocol algorithms
|
|
5
5
|
Project-URL: Homepage, https://github.com/AgrospAI/ocean-runner
|
|
6
6
|
Project-URL: Issues, https://github.com/AgrospAI/ocean-runner/issues
|
|
@@ -17,7 +17,10 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
17
17
|
Classifier: Operating System :: OS Independent
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
-
Requires-Dist:
|
|
20
|
+
Requires-Dist: aiofiles>=25.1.0
|
|
21
|
+
Requires-Dist: oceanprotocol-job-details>=0.3.4
|
|
22
|
+
Requires-Dist: pydantic-settings>=2.12.0
|
|
23
|
+
Requires-Dist: pydantic>=2.12.5
|
|
21
24
|
Requires-Dist: pytest>=8.4.2
|
|
22
25
|
Description-Content-Type: text/markdown
|
|
23
26
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ocean_runner/__init__.py,sha256=hRyMrE7K0DEbNRa-iNwA2xFOM85JJvjeKIdiWvcOm4Y,154
|
|
2
|
+
ocean_runner/config.py,sha256=_P8MbKGzL36p8LO_m6nj3cPB0h6L2YL8satxUNXXnUI,1927
|
|
3
|
+
ocean_runner/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
ocean_runner/runner.py,sha256=0-McElMM_dzubLUbRdeDaE0WY_dNj0IN_5cGen4-4bM,6242
|
|
5
|
+
ocean_runner-0.2.23.dist-info/METADATA,sha256=HkaOEgMvm98GQT9VsGMSN6FwTNzzMnocGpZyKu4HgSs,6667
|
|
6
|
+
ocean_runner-0.2.23.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
ocean_runner-0.2.23.dist-info/licenses/LICENSE,sha256=_B25KqK4amoADWkMN150tnZFm_Fy7VvZpvIC8ZydWdI,1053
|
|
8
|
+
ocean_runner-0.2.23.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
ocean_runner/__init__.py,sha256=awAmE6kZhuwcrD3gT7qFZArdhiuzW-EFTA6tGKhw06k,138
|
|
2
|
-
ocean_runner/config.py,sha256=gyyUotPJ7n8wPPdsJZIBUT4zBlkoNbhV876JDTdPNsY,1398
|
|
3
|
-
ocean_runner/runner.py,sha256=2j0XNk06gIlPoer_kVRtSf-noYZzhORi_7-UnPQBJxA,6005
|
|
4
|
-
ocean_runner-0.2.18.dist-info/METADATA,sha256=NsZ4U_FM2scbLrsgP9BrxczPoBzItV1xs2VlttEGTAI,6562
|
|
5
|
-
ocean_runner-0.2.18.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
-
ocean_runner-0.2.18.dist-info/licenses/LICENSE,sha256=_B25KqK4amoADWkMN150tnZFm_Fy7VvZpvIC8ZydWdI,1053
|
|
7
|
-
ocean_runner-0.2.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|