ocean-runner 0.2.19__py3-none-any.whl → 0.2.24__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/config.py +5 -5
- ocean_runner/py.typed +0 -0
- ocean_runner/runner.py +73 -62
- {ocean_runner-0.2.19.dist-info → ocean_runner-0.2.24.dist-info}/METADATA +21 -22
- ocean_runner-0.2.24.dist-info/RECORD +8 -0
- ocean_runner-0.2.19.dist-info/RECORD +0 -7
- {ocean_runner-0.2.19.dist-info → ocean_runner-0.2.24.dist-info}/WHEEL +0 -0
- {ocean_runner-0.2.19.dist-info → ocean_runner-0.2.24.dist-info}/licenses/LICENSE +0 -0
ocean_runner/config.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from enum import StrEnum, auto
|
|
2
2
|
from logging import Logger
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Generic, Sequence, TypeVar
|
|
4
|
+
from typing import Generic, Sequence, Type, TypeVar
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, ConfigDict, Field
|
|
7
7
|
from pydantic_settings import BaseSettings
|
|
8
8
|
|
|
9
|
-
InputT = TypeVar("InputT")
|
|
9
|
+
InputT = TypeVar("InputT", BaseModel, None)
|
|
10
10
|
|
|
11
11
|
DEFAULT = "DEFAULT"
|
|
12
12
|
|
|
@@ -21,13 +21,13 @@ class Keys(StrEnum):
|
|
|
21
21
|
class Environment(BaseSettings):
|
|
22
22
|
"""Environment configuration loaded from environment variables"""
|
|
23
23
|
|
|
24
|
-
base_dir: str | Path
|
|
24
|
+
base_dir: str | Path = Field(
|
|
25
25
|
default_factory=lambda: Path("/data"),
|
|
26
26
|
validation_alias=Keys.BASE_DIR.value,
|
|
27
27
|
description="Base data directory, defaults to '/data'",
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
-
dids: str |
|
|
30
|
+
dids: str | None = Field(
|
|
31
31
|
default=None,
|
|
32
32
|
validation_alias=Keys.DIDS.value,
|
|
33
33
|
description='Datasets DID\'s, format: ["XXXX"]',
|
|
@@ -51,7 +51,7 @@ class Config(BaseModel, Generic[InputT]):
|
|
|
51
51
|
|
|
52
52
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
53
53
|
|
|
54
|
-
custom_input: InputT | None = Field(
|
|
54
|
+
custom_input: Type[InputT] | None = Field(
|
|
55
55
|
default=None,
|
|
56
56
|
description="Algorithm's custom input types, must be a dataclass_json",
|
|
57
57
|
)
|
ocean_runner/py.typed
ADDED
|
File without changes
|
ocean_runner/runner.py
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
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, Dict, Generic, TypeAlias, TypeVar
|
|
7
9
|
|
|
8
|
-
from oceanprotocol_job_details import JobDetails
|
|
10
|
+
from oceanprotocol_job_details import JobDetails, load_job_details
|
|
11
|
+
from pydantic import BaseModel, JsonValue
|
|
9
12
|
|
|
10
13
|
from ocean_runner.config import Config
|
|
11
14
|
|
|
12
|
-
InputT = TypeVar("InputT")
|
|
15
|
+
InputT = TypeVar("InputT", BaseModel, None)
|
|
13
16
|
ResultT = TypeVar("ResultT")
|
|
17
|
+
T = TypeVar("T")
|
|
14
18
|
|
|
15
|
-
ValidateFuncT = Callable[["Algorithm"], None]
|
|
16
|
-
RunFuncT = Callable[["Algorithm"], ResultT] | None
|
|
17
|
-
SaveFuncT = Callable[["Algorithm", ResultT, Path], None]
|
|
18
|
-
ErrorFuncT = Callable[["Algorithm", Exception], None]
|
|
19
19
|
|
|
20
|
+
Algo: TypeAlias = "Algorithm[InputT, ResultT]"
|
|
21
|
+
ValidateFuncT: TypeAlias = Callable[[Algo], None | Awaitable[None] | None]
|
|
22
|
+
RunFuncT: TypeAlias = Callable[[Algo], ResultT | Awaitable[ResultT]]
|
|
23
|
+
SaveFuncT: TypeAlias = Callable[[Algo, ResultT, Path], Awaitable[None] | None]
|
|
24
|
+
ErrorFuncT: TypeAlias = Callable[[Algo, Exception], Awaitable[None] | None]
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
|
|
27
|
+
def default_error_callback(algorithm: Algorithm, error: Exception) -> None:
|
|
22
28
|
algorithm.logger.exception("Error during algorithm execution")
|
|
23
|
-
raise
|
|
29
|
+
raise error
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
def default_validation(algorithm: Algorithm) -> None:
|
|
@@ -29,10 +35,29 @@ def default_validation(algorithm: Algorithm) -> None:
|
|
|
29
35
|
assert algorithm.job_details.files, "Files missing"
|
|
30
36
|
|
|
31
37
|
|
|
32
|
-
def default_save(algorithm: Algorithm, result: ResultT, base: Path) -> None:
|
|
38
|
+
async def default_save(algorithm: Algorithm, result: ResultT, base: Path) -> None:
|
|
39
|
+
import aiofiles
|
|
40
|
+
|
|
33
41
|
algorithm.logger.info("Saving results using default save")
|
|
34
|
-
with open(base / "result.txt", "w+") as f:
|
|
35
|
-
f.write(str(result))
|
|
42
|
+
async with aiofiles.open(base / "result.txt", "w+") as f:
|
|
43
|
+
await f.write(str(result))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def execute(function: Callable[..., T | Awaitable[T]], *args, **kwargs) -> T:
|
|
47
|
+
result = function(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
if inspect.isawaitable(result):
|
|
50
|
+
return await result
|
|
51
|
+
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(slots=True)
|
|
56
|
+
class Functions(Generic[InputT, ResultT]):
|
|
57
|
+
validate: ValidateFuncT = field(default=default_validation, init=False)
|
|
58
|
+
run: RunFuncT | None = field(default=None, init=False)
|
|
59
|
+
save: SaveFuncT = field(default=default_save, init=False)
|
|
60
|
+
error: ErrorFuncT = field(default=default_error_callback, init=False)
|
|
36
61
|
|
|
37
62
|
|
|
38
63
|
@dataclass
|
|
@@ -44,33 +69,13 @@ class Algorithm(Generic[InputT, ResultT]):
|
|
|
44
69
|
"""
|
|
45
70
|
|
|
46
71
|
config: InitVar[Config[InputT] | None] = field(default=None)
|
|
47
|
-
logger: Logger = field(init=False)
|
|
48
|
-
_job_details: JobDetails[InputT] = field(init=False)
|
|
49
|
-
_result: ResultT | None = field(default=None, init=False)
|
|
50
|
-
|
|
51
|
-
# Decorator-registered callbacks
|
|
52
|
-
_validate_fn: ValidateFuncT = field(
|
|
53
|
-
default=default_validation,
|
|
54
|
-
init=False,
|
|
55
|
-
repr=False,
|
|
56
|
-
)
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
default=None,
|
|
60
|
-
init=False,
|
|
61
|
-
repr=False,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
_save_fn: SaveFuncT = field(
|
|
65
|
-
default=default_save,
|
|
66
|
-
init=False,
|
|
67
|
-
repr=False,
|
|
68
|
-
)
|
|
73
|
+
logger: Logger = field(init=False, repr=False)
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
repr=False
|
|
75
|
+
_job_details: JobDetails[InputT] = field(init=False)
|
|
76
|
+
_result: ResultT | None = field(default=None, init=False)
|
|
77
|
+
_functions: Functions[InputT, ResultT] = field(
|
|
78
|
+
default_factory=Functions, init=False, repr=False
|
|
74
79
|
)
|
|
75
80
|
|
|
76
81
|
def __post_init__(self, config: Config[InputT] | None) -> None:
|
|
@@ -106,7 +111,7 @@ class Algorithm(Generic[InputT, ResultT]):
|
|
|
106
111
|
f"Added [{len(configuration.source_paths)}] entries to PATH"
|
|
107
112
|
)
|
|
108
113
|
|
|
109
|
-
self.configuration = configuration
|
|
114
|
+
self.configuration: Config[InputT] = configuration
|
|
110
115
|
|
|
111
116
|
class Error(RuntimeError): ...
|
|
112
117
|
|
|
@@ -127,55 +132,61 @@ class Algorithm(Generic[InputT, ResultT]):
|
|
|
127
132
|
# ---------------------------
|
|
128
133
|
|
|
129
134
|
def validate(self, fn: ValidateFuncT) -> ValidateFuncT:
|
|
130
|
-
self.
|
|
135
|
+
self._functions.validate = fn
|
|
131
136
|
return fn
|
|
132
137
|
|
|
133
138
|
def run(self, fn: RunFuncT) -> RunFuncT:
|
|
134
|
-
self.
|
|
139
|
+
self._functions.run = fn
|
|
135
140
|
return fn
|
|
136
141
|
|
|
137
142
|
def save_results(self, fn: SaveFuncT) -> SaveFuncT:
|
|
138
|
-
self.
|
|
143
|
+
self._functions.save = fn
|
|
139
144
|
return fn
|
|
140
145
|
|
|
141
146
|
def on_error(self, fn: ErrorFuncT) -> ErrorFuncT:
|
|
142
|
-
self.
|
|
147
|
+
self._functions.error = fn
|
|
143
148
|
return fn
|
|
144
149
|
|
|
145
150
|
# ---------------------------
|
|
146
151
|
# Execution Pipeline
|
|
147
152
|
# ---------------------------
|
|
148
153
|
|
|
149
|
-
def
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
154
|
+
async def execute(self) -> ResultT | None:
|
|
155
|
+
env = self.configuration.environment
|
|
156
|
+
config: Dict[str, JsonValue] = {
|
|
157
|
+
"base_dir": str(env.base_dir),
|
|
158
|
+
"dids": env.dids,
|
|
159
|
+
"secret": env.secret,
|
|
160
|
+
"transformation_did": env.transformation_did,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
self._job_details = load_job_details(config, self.configuration.custom_input)
|
|
159
164
|
|
|
160
165
|
self.logger.info("Loaded JobDetails")
|
|
161
|
-
self.logger.debug(
|
|
166
|
+
self.logger.debug(self.job_details.model_dump())
|
|
162
167
|
|
|
163
168
|
try:
|
|
164
|
-
|
|
165
|
-
self._validate_fn(self)
|
|
169
|
+
await execute(self._functions.validate, self)
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
if self._run_fn:
|
|
171
|
+
if self._functions.run:
|
|
169
172
|
self.logger.info("Running algorithm...")
|
|
170
|
-
self._result = self.
|
|
173
|
+
self._result = await execute(self._functions.run, self)
|
|
171
174
|
else:
|
|
172
175
|
self.logger.error("No run() function defined. Skipping execution.")
|
|
173
176
|
self._result = None
|
|
174
177
|
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
await execute(
|
|
179
|
+
self._functions.save,
|
|
180
|
+
algorithm=self,
|
|
181
|
+
result=self._result,
|
|
182
|
+
base=self.job_details.paths.outputs,
|
|
183
|
+
)
|
|
177
184
|
|
|
178
185
|
except Exception as e:
|
|
179
|
-
self.
|
|
186
|
+
await execute(self._functions.error, self, e)
|
|
180
187
|
|
|
181
188
|
return self._result
|
|
189
|
+
|
|
190
|
+
def __call__(self) -> ResultT | None:
|
|
191
|
+
"""Executes the algorithm pipeline: validate → run → save_results."""
|
|
192
|
+
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.24
|
|
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,8 @@ 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.9
|
|
21
22
|
Requires-Dist: pydantic-settings>=2.12.0
|
|
22
23
|
Requires-Dist: pydantic>=2.12.5
|
|
23
24
|
Requires-Dist: pytest>=8.4.2
|
|
@@ -27,7 +28,6 @@ Description-Content-Type: text/markdown
|
|
|
27
28
|
|
|
28
29
|
Ocean Runner is a package that eases algorithm creation in the scope of OceanProtocol.
|
|
29
30
|
|
|
30
|
-
|
|
31
31
|
## Installation
|
|
32
32
|
|
|
33
33
|
```bash
|
|
@@ -48,7 +48,7 @@ algorithm = Algorithm()
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
@algorithm.run
|
|
51
|
-
def run():
|
|
51
|
+
def run(_: Algorithm):
|
|
52
52
|
return random.randint()
|
|
53
53
|
|
|
54
54
|
|
|
@@ -75,14 +75,14 @@ algorithm = Algorithm(
|
|
|
75
75
|
Config(
|
|
76
76
|
custom_input: ... # dataclass
|
|
77
77
|
# Custom algorithm parameters dataclass.
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
logger: ... # type: logging.Logger
|
|
80
80
|
# Custom logger to use.
|
|
81
81
|
|
|
82
82
|
source_paths: ... # type: Iterable[Path]
|
|
83
83
|
# Source paths to include in the PATH
|
|
84
|
-
|
|
85
|
-
environment: ...
|
|
84
|
+
|
|
85
|
+
environment: ...
|
|
86
86
|
# type: ocean_runner.Environment. Mock of environment variables.
|
|
87
87
|
)
|
|
88
88
|
)
|
|
@@ -91,12 +91,12 @@ algorithm = Algorithm(
|
|
|
91
91
|
```python
|
|
92
92
|
import logging
|
|
93
93
|
|
|
94
|
+
from pydantic import BaseModel
|
|
94
95
|
from ocean_runner import Algorithm, Config
|
|
95
96
|
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
foobar: string
|
|
98
|
+
class CustomInput(BaseModel):
|
|
99
|
+
foobar: string
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
logger = logging.getLogger(__name__)
|
|
@@ -106,7 +106,7 @@ algorithm = Algorithm(
|
|
|
106
106
|
Config(
|
|
107
107
|
custom_input: CustomInput,
|
|
108
108
|
"""
|
|
109
|
-
Load the Algorithm's Custom Input into a CustomInput
|
|
109
|
+
Load the Algorithm's Custom Input into a CustomInput instance.
|
|
110
110
|
"""
|
|
111
111
|
|
|
112
112
|
source_paths: [Path("/algorithm/src")],
|
|
@@ -162,34 +162,32 @@ algorithm = Algorithm()
|
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
@algorithm.on_error
|
|
165
|
-
def error_callback(ex: Exception):
|
|
165
|
+
def error_callback(algorithm: Algorithm, ex: Exception):
|
|
166
166
|
algorithm.logger.exception(ex)
|
|
167
167
|
raise algorithm.Error() from ex
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
@algorithm.validate
|
|
171
|
-
def val():
|
|
171
|
+
def val(algorithm: Algorithm):
|
|
172
172
|
assert algorithm.job_details.files, "Empty input dir"
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
@algorithm.run
|
|
176
|
-
def run() -> pd.DataFrame:
|
|
177
|
-
_, filename = next(algorithm.job_details.
|
|
176
|
+
def run(algorithm: Algorithm) -> pd.DataFrame:
|
|
177
|
+
_, filename = next(algorithm.job_details.inputs())
|
|
178
178
|
return pd.read_csv(filename).describe(include="all")
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
@algorithm.save_results
|
|
182
|
-
def save(
|
|
183
|
-
algorithm.logger.info(f"Descriptive statistics: {
|
|
184
|
-
|
|
182
|
+
def save(algorithm: Algorithm, result: pd.DataFrame, base: Path):
|
|
183
|
+
algorithm.logger.info(f"Descriptive statistics: {result}")
|
|
184
|
+
result.to_csv(base / "result.csv")
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
if __name__ == "__main__":
|
|
188
188
|
algorithm()
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
-
|
|
192
|
-
|
|
193
191
|
### Default implementations
|
|
194
192
|
|
|
195
193
|
As seen in the minimal example, all methods implemented in `Algorithm` have a default implementation which will be commented here.
|
|
@@ -205,7 +203,7 @@ As seen in the minimal example, all methods implemented in `Algorithm` have a de
|
|
|
205
203
|
|
|
206
204
|
.run()
|
|
207
205
|
|
|
208
|
-
"""
|
|
206
|
+
"""
|
|
209
207
|
Has NO default implementation, must pass a callback that returns a result of any type.
|
|
210
208
|
"""
|
|
211
209
|
|
|
@@ -221,7 +219,8 @@ As seen in the minimal example, all methods implemented in `Algorithm` have a de
|
|
|
221
219
|
To load the OceanProtocol JobDetails instance, the program will read some environment variables, they can be mocked passing an instance of `Environment` through the configuration of the algorithm.
|
|
222
220
|
|
|
223
221
|
Environment variables:
|
|
222
|
+
|
|
224
223
|
- `DIDS` (optional) Input dataset(s) DID's, must have format: `["abc..90"]`. Defaults to reading them automatically from the `DDO` data directory.
|
|
225
224
|
- `TRANSFORMATION_DID` (optional, default="DEFAULT"): Algorithm DID, must have format: `abc..90`.
|
|
226
|
-
- `SECRET` (optional, default="DEFAULT"): Algorithm secret.
|
|
225
|
+
- `SECRET` (optional, default="DEFAULT"): Algorithm secret.
|
|
227
226
|
- `BASE_DIR` (optional, default="/data"): Base path to the OceanProtocol data directories.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ocean_runner/__init__.py,sha256=hRyMrE7K0DEbNRa-iNwA2xFOM85JJvjeKIdiWvcOm4Y,154
|
|
2
|
+
ocean_runner/config.py,sha256=VfDNyYjO6PpM2tmtiCdCiY4oGT1JMB_zl5hZjLeT0Vg,1928
|
|
3
|
+
ocean_runner/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
ocean_runner/runner.py,sha256=bmWzQqZci3NSUclCwGuzfYfWOuXgKCUT0u3D3HljBMo,6235
|
|
5
|
+
ocean_runner-0.2.24.dist-info/METADATA,sha256=axJvnOryvmL_Y6UuN1aEOpGSJ0pR_DOOMGRfcScPRec,6755
|
|
6
|
+
ocean_runner-0.2.24.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
ocean_runner-0.2.24.dist-info/licenses/LICENSE,sha256=_B25KqK4amoADWkMN150tnZFm_Fy7VvZpvIC8ZydWdI,1053
|
|
8
|
+
ocean_runner-0.2.24.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
ocean_runner/__init__.py,sha256=hRyMrE7K0DEbNRa-iNwA2xFOM85JJvjeKIdiWvcOm4Y,154
|
|
2
|
-
ocean_runner/config.py,sha256=paN2I3ISIxRO1isCIuUgNsL9dsRhEc_n6KjTH6yNZU4,1919
|
|
3
|
-
ocean_runner/runner.py,sha256=VaayC8UXn2Iyl1RuUcTap9vyTyDYkvm16p3ucaRAOhQ,5527
|
|
4
|
-
ocean_runner-0.2.19.dist-info/METADATA,sha256=4n10dG0cfNAPraB9qJ4WcINyzJNFAxwDQtfZufbs0sU,6635
|
|
5
|
-
ocean_runner-0.2.19.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
-
ocean_runner-0.2.19.dist-info/licenses/LICENSE,sha256=_B25KqK4amoADWkMN150tnZFm_Fy7VvZpvIC8ZydWdI,1053
|
|
7
|
-
ocean_runner-0.2.19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|