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 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 | None = Field(
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 | list[Path] | None = Field(
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
- from dataclasses import InitVar, asdict, dataclass, field
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 # type: ignore
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
- def default_error_callback(algorithm: Algorithm, e: Exception) -> None:
26
+
27
+ def default_error_callback(algorithm: Algorithm, error: Exception) -> None:
22
28
  algorithm.logger.exception("Error during algorithm execution")
23
- raise e
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
- _run_fn: RunFuncT = field(
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
- _error_callback: ErrorFuncT = field(
71
- default=default_error_callback,
72
- init=False,
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._validate_fn = fn
135
+ self._functions.validate = fn
131
136
  return fn
132
137
 
133
138
  def run(self, fn: RunFuncT) -> RunFuncT:
134
- self._run_fn = fn
139
+ self._functions.run = fn
135
140
  return fn
136
141
 
137
142
  def save_results(self, fn: SaveFuncT) -> SaveFuncT:
138
- self._save_fn = fn
143
+ self._functions.save = fn
139
144
  return fn
140
145
 
141
146
  def on_error(self, fn: ErrorFuncT) -> ErrorFuncT:
142
- self._error_callback = fn
147
+ self._functions.error = fn
143
148
  return fn
144
149
 
145
150
  # ---------------------------
146
151
  # Execution Pipeline
147
152
  # ---------------------------
148
153
 
149
- def __call__(self) -> ResultT | None:
150
- """Executes the algorithm pipeline: validate → run → save_results."""
151
- # Load job details
152
- self._job_details = JobDetails.load(
153
- _type=self.configuration.custom_input,
154
- base_dir=self.configuration.environment.base_dir,
155
- dids=self.configuration.environment.dids,
156
- transformation_did=self.configuration.environment.transformation_did,
157
- secret=self.configuration.environment.secret,
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(asdict(self.job_details))
166
+ self.logger.debug(self.job_details.model_dump())
162
167
 
163
168
  try:
164
- # Validation step
165
- self._validate_fn(self)
169
+ await execute(self._functions.validate, self)
166
170
 
167
- # Run step
168
- if self._run_fn:
171
+ if self._functions.run:
169
172
  self.logger.info("Running algorithm...")
170
- self._result = self._run_fn(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
- # Save step
176
- self._save_fn(self, self._result, self.job_details.paths.outputs)
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._error_callback(self, e)
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.19
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: oceanprotocol-job-details>=0.2.8
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
- @dataclass
98
- class CustomInput:
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 dataclass instance.
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.next_path())
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(results: pd.DataFrame, path: Path):
183
- algorithm.logger.info(f"Descriptive statistics: {results}")
184
- results.to_csv(path / "results.csv")
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,,