ocean-runner 0.2.7__py3-none-any.whl → 0.2.18__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
@@ -2,10 +2,12 @@ import os
2
2
  from dataclasses import asdict, dataclass, field
3
3
  from logging import Logger
4
4
  from pathlib import Path
5
- from typing import Callable, Iterable, Literal, TypeVar
5
+ from typing import Iterable, TypeVar
6
6
 
7
7
  T = TypeVar("T")
8
8
 
9
+ DEFAULT = "DEFAULT"
10
+
9
11
 
10
12
  @dataclass
11
13
  class Environment:
@@ -17,25 +19,20 @@ class Environment:
17
19
  """Base data directory, defaults to '/data'"""
18
20
 
19
21
  dids: str = field(
20
- default_factory=lambda: os.environ.get("DIDS"),
22
+ default_factory=lambda: os.environ.get("DIDS", None),
21
23
  )
22
24
  """Datasets DID's, format: '["XXXX"]'"""
23
25
 
24
26
  transformation_did: str = field(
25
- default_factory=lambda: os.environ.get("TRANSFORMATION_DID"),
27
+ default_factory=lambda: os.environ.get("TRANSFORMATION_DID", DEFAULT),
26
28
  )
27
29
  """Transformation (algorithm) DID"""
28
30
 
29
31
  secret: str = field(
30
- default_factory=lambda: os.environ.get("SECRET"),
32
+ default_factory=lambda: os.environ.get("SECRET", DEFAULT),
31
33
  )
32
34
  """Super secret secret"""
33
35
 
34
- runtime: Literal["dev", "test"] = field(
35
- default_factory=lambda: os.environ.get("RUNTIME", "dev").lower()
36
- )
37
- """Select runtime mode"""
38
-
39
36
  dict = asdict
40
37
 
41
38
 
@@ -46,9 +43,6 @@ class Config:
46
43
  custom_input: T | None = None
47
44
  """Algorithm's custom input types, must be a dataclass_json"""
48
45
 
49
- error_callback: Callable[[Exception], None] = None
50
- """Callback to execute upon exceptions"""
51
-
52
46
  logger: Logger | None = None
53
47
  """Logger to use in the algorithm"""
54
48
 
@@ -57,7 +51,5 @@ class Config:
57
51
  )
58
52
  """Paths that should be included so the code executes correctly"""
59
53
 
60
- environment: Environment = field(
61
- default_factory=lambda: Environment(),
62
- )
54
+ environment: Environment = field(default_factory=lambda: Environment())
63
55
  """Mock of environment data"""
ocean_runner/runner.py CHANGED
@@ -3,70 +3,75 @@ from __future__ import annotations
3
3
  from dataclasses import InitVar, asdict, dataclass, field
4
4
  from logging import Logger
5
5
  from pathlib import Path
6
- from typing import Callable, Generic, Self, TypeVar
6
+ from typing import Callable, Generic, TypeVar
7
7
 
8
8
  from oceanprotocol_job_details import JobDetails
9
9
 
10
10
  from ocean_runner.config import Config
11
- from ocean_runner.runtime_mode import RuntimeMode
12
11
 
13
- JobDetailsT = TypeVar(
14
- "JobDetailsT",
15
- )
12
+ JobDetailsT = TypeVar("JobDetailsT")
16
13
  ResultT = TypeVar("ResultT")
17
14
 
18
15
 
19
- def default_error_callback(_: Algorithm, e: Exception) -> None:
16
+ def default_error_callback(algorithm: Algorithm, e: Exception) -> None:
17
+ algorithm.logger.exception("Error during algorithm execution")
20
18
  raise e
21
19
 
22
20
 
23
21
  def default_validation(algorithm: Algorithm) -> None:
24
22
  algorithm.logger.info("Validating input using default validation")
25
-
26
23
  assert algorithm.job_details.ddos, "DDOs missing"
27
24
  assert algorithm.job_details.files, "Files missing"
28
25
 
29
26
 
30
27
  def default_save(*, result: ResultT, base: Path, algorithm: Algorithm) -> None:
31
28
  algorithm.logger.info("Saving results using default save")
32
-
33
29
  with open(base / "result.txt", "w+") as f:
34
30
  f.write(str(result))
35
31
 
36
32
 
37
- def default_test_run(algorithm: Algorithm) -> int:
38
- import pytest
39
-
40
- result = pytest.main()
41
-
42
- if result == 0:
43
- algorithm.logger.info("Passed all tests")
44
- else:
45
- algorithm.logger.error("Some tests failed")
46
-
47
- return result
48
-
49
-
50
33
  @dataclass
51
34
  class Algorithm(Generic[JobDetailsT, ResultT]):
35
+ """
36
+ A configurable algorithm runner that behaves like a FastAPI app:
37
+ - You register `validate`, `run`, and `save_results` via decorators.
38
+ - You execute the full pipeline by calling `app()`.
39
+ """
52
40
 
53
41
  config: InitVar[Config | None] = None
54
-
55
- # Load from config
56
42
  logger: Logger = field(init=False)
57
-
58
43
  _job_details: JobDetails[JobDetailsT] = field(init=False)
59
44
  _result: ResultT | None = field(default=None, init=False)
60
- _runtime: RuntimeMode = field(default=RuntimeMode.DEV, init=False)
61
45
 
62
- error_callback = default_error_callback
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
+ )
58
+
59
+ _save_fn: Callable[[ResultT, Path, Algorithm], None] | None = field(
60
+ default=None,
61
+ init=False,
62
+ repr=False,
63
+ )
64
+
65
+ _error_callback: Callable[[Algorithm, Exception], None] | None = field(
66
+ default=None,
67
+ init=False,
68
+ repr=False,
69
+ )
63
70
 
64
71
  def __post_init__(self, config: Config | None) -> None:
65
72
  config: Config = config or Config()
66
73
 
67
- if config.error_callback:
68
- self.error_callback = config.error_callback
69
-
74
+ # Configure logger
70
75
  if config.logger:
71
76
  self.logger = config.logger
72
77
  else:
@@ -77,30 +82,20 @@ class Algorithm(Generic[JobDetailsT, ResultT]):
77
82
  format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
78
83
  datefmt="%Y-%m-%d %H:%M:%S",
79
84
  )
80
-
81
85
  self.logger = logging.getLogger("ocean_runner")
82
86
 
87
+ # Normalize base_dir
83
88
  if isinstance(config.environment.base_dir, str):
84
89
  config.environment.base_dir = Path(config.environment.base_dir)
85
90
 
91
+ # Extend sys.path for custom imports
86
92
  if config.source_paths:
87
93
  import sys
88
94
 
89
95
  sys.path.extend([str(path.absolute()) for path in config.source_paths])
90
96
  self.logger.debug(f"Added [{len(config.source_paths)}] entries to PATH")
91
97
 
92
- self._runtime = RuntimeMode(config.environment.runtime) or self._runtime
93
-
94
- self._job_details = JobDetails.load(
95
- _type=config.custom_input,
96
- base_dir=config.environment.base_dir,
97
- dids=config.environment.dids,
98
- transformation_did=config.environment.transformation_did,
99
- secret=config.environment.secret,
100
- )
101
-
102
- self.logger.info("Loaded JobDetails")
103
- self.logger.debug(asdict(self.job_details))
98
+ self.config = config
104
99
 
105
100
  class Error(RuntimeError): ...
106
101
 
@@ -112,46 +107,84 @@ class Algorithm(Generic[JobDetailsT, ResultT]):
112
107
 
113
108
  @property
114
109
  def result(self) -> ResultT:
115
- if not self._result:
110
+ if self._result is None:
116
111
  raise Algorithm.Error("Result missing, run the algorithm first")
117
112
  return self._result
118
113
 
119
- def validate(self, callback: Callable[[Self], None] = default_validation) -> Self:
120
- self.logger.info("Validating instance...")
121
- try:
122
- callback(self)
123
- except Exception as e:
124
- self.error_callback(e)
114
+ # ---------------------------
115
+ # Decorators (FastAPI-style)
116
+ # ---------------------------
125
117
 
126
- return self
118
+ def validate(self, fn: Callable[[], None]) -> Callable[[], None]:
119
+ self._validate_fn = fn
120
+ return fn
127
121
 
128
- def run(self, callable: Callable[[Self], ResultT]) -> Self:
129
- self.logger.info("Running algorithm...")
130
- try:
131
- if self._runtime == RuntimeMode.TEST:
132
- callable = default_test_run
122
+ def run(self, fn: Callable[[], ResultT]) -> Callable[[], ResultT]:
123
+ self._run_fn = fn
124
+ return fn
133
125
 
134
- self._result = callable(self)
135
- except Exception as e:
136
- self.error_callback(e)
126
+ def save_results(self, fn: Callable[[ResultT, Path], None]) -> Callable:
127
+ self._save_fn = fn
128
+ return fn
129
+
130
+ def on_error(self, fn: Callable[[Exception], None]) -> Callable:
131
+ self._error_callback = fn
132
+ return fn
133
+
134
+ # ---------------------------
135
+ # Execution Pipeline
136
+ # ---------------------------
137
+
138
+ def __call__(self) -> ResultT | None:
139
+ """Executes the algorithm pipeline: validate → run → save_results."""
140
+ # Load job details
141
+ self._job_details = JobDetails.load(
142
+ _type=self.config.custom_input,
143
+ base_dir=self.config.environment.base_dir,
144
+ dids=self.config.environment.dids,
145
+ transformation_did=self.config.environment.transformation_did,
146
+ secret=self.config.environment.secret,
147
+ )
137
148
 
138
- return self
149
+ self.logger.info("Loaded JobDetails")
150
+ self.logger.debug(asdict(self.job_details))
139
151
 
140
- def save_results(
141
- self,
142
- callable: Callable[[ResultT, Path, Algorithm], None] = default_save,
143
- *,
144
- override_path: Path | None = None,
145
- ) -> None:
146
- self.logger.info("Saving results...")
147
152
  try:
148
- callable(
149
- results=self.result,
150
- base_path=override_path or self.job_details.paths.outputs,
151
- algorithm=self,
152
- )
153
- except Exception as e:
154
- self.error_callback(e)
153
+ # Validation step
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)
160
+
161
+ # Run step
162
+ if self._run_fn:
163
+ self.logger.info("Running algorithm...")
164
+ self._result = self._run_fn()
165
+ else:
166
+ self.logger.warning("No run() function defined. Skipping execution.")
167
+ self._result = None
168
+
169
+ # Save step
170
+ if self._save_fn:
171
+ self.logger.info("Saving results...")
172
+ self._save_fn(
173
+ self._result,
174
+ self.job_details.paths.outputs,
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
+ )
155
183
 
184
+ except Exception as e:
185
+ if self._error_callback:
186
+ self._error_callback(e)
187
+ else:
188
+ default_error_callback(self, e)
156
189
 
157
- __all__ = [Algorithm]
190
+ return self._result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ocean-runner
3
- Version: 0.2.7
3
+ Version: 0.2.18
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,13 +17,13 @@ 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.6
20
+ Requires-Dist: oceanprotocol-job-details>=0.2.8
21
21
  Requires-Dist: pytest>=8.4.2
22
22
  Description-Content-Type: text/markdown
23
23
 
24
24
  # ocean-runner
25
25
 
26
- Ocean Runner is a package that brings a fluent API for APP creation and running in the scope of OceanProtocol.
26
+ Ocean Runner is a package that eases algorithm creation in the scope of OceanProtocol.
27
27
 
28
28
 
29
29
  ## Installation
@@ -40,33 +40,40 @@ uv add ocean-runner
40
40
 
41
41
  ```python
42
42
  import random
43
- from ocean_runner import Algorithm, Config
43
+ from ocean_runner import Algorithm
44
+
45
+ algorithm = Algorithm()
46
+
44
47
 
48
+ @algorithm.run
49
+ def run():
50
+ return random.randint()
45
51
 
46
- Algorithm().run(lambda _: random.randint()).save_results()
52
+
53
+ if __name__ == "__main__":
54
+ algorithm()
47
55
  ```
48
56
 
49
- To use minimally the API, you can just provide a callback to the run method, defaulting for the rest of behaviours. This code snippet will:
57
+ This code snippet will:
50
58
 
51
- - Read the OceanProtocol JobDetails from the environment variables and use default file paths.
52
- - Generate a random integer.
53
- - Store the result in a "result.txt" file within the default outputs path.
59
+ - Read the OceanProtocol JobDetails from the environment variables and use default configuration file paths.
60
+ - Execute the run function.
61
+ - Execute the default saving function, storing the result in a "result.txt" file within the default outputs path.
54
62
 
55
63
  ### Tuning
56
64
 
57
65
  #### Application Config
58
66
 
59
- The application configuration can be tweaked by passing a Config instance to its' constructor.
67
+ The application configuration can be tweaked by passing a Config instance to its constructor.
60
68
 
61
69
  ```python
62
- Algorithm(
70
+ from ocean_runner import Algorithm, Config
71
+
72
+ algorithm = Algorithm(
63
73
  Config(
64
74
  custom_input: ... # dataclass
65
75
  # Custom algorithm parameters dataclass.
66
76
 
67
- error_callback: ... # Callable[[Exception], None]
68
- # Callback to run on exceptions.
69
-
70
77
  logger: ... # type: logging.Logger
71
78
  # Custom logger to use.
72
79
 
@@ -82,6 +89,8 @@ Algorithm(
82
89
  ```python
83
90
  import logging
84
91
 
92
+ from ocean_runner import Algorithm, Config
93
+
85
94
 
86
95
  @dataclass
87
96
  class CustomInput:
@@ -91,19 +100,13 @@ class CustomInput:
91
100
  logger = logging.getLogger(__name__)
92
101
 
93
102
 
94
- Algorithm(
103
+ algorithm = Algorithm(
95
104
  Config(
96
105
  custom_input: CustomInput,
97
106
  """
98
107
  Load the Algorithm's Custom Input into a CustomInput dataclass instance.
99
108
  """
100
109
 
101
- error_callback: lambda ex: logger.exception(ex),
102
- """
103
- Run this callback when an exception is caught
104
- NOTE: it's not recommended to catch exceptions this way. Should re-raise and halt the execution.
105
- """
106
-
107
110
  source_paths: [Path("/algorithm/src")],
108
111
  """
109
112
  Source paths to include in the PATH. '/algorithm/src' is the default since our templates place the algorithm source files there.
@@ -134,11 +137,6 @@ Algorithm(
134
137
  """
135
138
  Random secret to use while testing.
136
139
  """
137
-
138
- runtime: "dev",
139
- """
140
- Runtime mode. "dev" to run normally, "test" to run pytest
141
- """
142
140
  )
143
141
  """
144
142
  Should not be needed in production algorithms, used to mock environment variables, defaults to using env.
@@ -148,44 +146,72 @@ Algorithm(
148
146
 
149
147
  ```
150
148
 
151
- ## Default behaviours
149
+ #### Behaviour Config
152
150
 
153
- ### Default implementations
154
-
155
- As seen in the minimal example, all methods implemented in `Algorithm` have a default implementation which will be commented here.
151
+ To fully configure the behaviour of the algorithm as in the [Minimal Example](#minimal-example), you can do it decorating your defined function as in the following example, which features all the possible algorithm customization.
156
152
 
157
153
  ```python
154
+ from pathlib import Path
158
155
 
159
- (
160
- Algorithm()
161
-
162
- """
163
- Default constructor, will use default values of Config.
164
- """
165
-
166
- .validate()
167
-
168
- """
169
- Will validate the algorithm's job detail instance, checking for the existence of:
170
- - `job_details.ddos`
171
- - `job_details.files`
172
- """
156
+ import pandas as pd
157
+ from ocean_runner import Algorithm
173
158
 
174
- .run()
159
+ algorithm = Algorithm()
175
160
 
176
- """
177
- Has NO default implementation, must pass a callback that returns a result of any type.
178
- """
179
161
 
180
- .save_results()
162
+ @algorithm.on_error
163
+ def error_callback(ex: Exception):
164
+ algorithm.logger.exception(ex)
165
+ raise algorithm.Error() from ex
181
166
 
182
- """
183
- Stores the result of running the algorithm in "outputs/results.txt"
184
- """
185
167
 
186
- )
168
+ @algorithm.validate
169
+ def val():
170
+ assert algorithm.job_details.files, "Empty input dir"
171
+
172
+
173
+ @algorithm.run
174
+ def run() -> pd.DataFrame:
175
+ _, filename = next(algorithm.job_details.next_path())
176
+ return pd.read_csv(filename).describe(include="all")
177
+
178
+
179
+ @algorithm.save_results
180
+ def save(results: pd.DataFrame, path: Path):
181
+ algorithm.logger.info(f"Descriptive statistics: {results}")
182
+ results.to_csv(path / "results.csv")
183
+
184
+
185
+ if __name__ == "__main__":
186
+ algorithm()
187
+ ```
188
+
189
+
190
+
191
+ ### Default implementations
192
+
193
+ As seen in the minimal example, all methods implemented in `Algorithm` have a default implementation which will be commented here.
194
+
195
+ ```python
196
+ .validate()
197
+
198
+ """
199
+ Will validate the algorithm's job detail instance, checking for the existence of:
200
+ - `job_details.ddos`
201
+ - `job_details.files`
202
+ """
203
+
204
+ .run()
205
+
206
+ """
207
+ Has NO default implementation, must pass a callback that returns a result of any type.
208
+ """
187
209
 
210
+ .save_results()
188
211
 
212
+ """
213
+ Stores the result of running the algorithm in "outputs/results.txt"
214
+ """
189
215
  ```
190
216
 
191
217
  ### Job Details
@@ -193,8 +219,7 @@ As seen in the minimal example, all methods implemented in `Algorithm` have a de
193
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.
194
220
 
195
221
  Environment variables:
196
- - `DIDS` Input dataset(s) DID's, must have format: `["abc..90"]`
197
- - `TRANSFORMATION_DID` Algorithm DID, must have format: `abc..90`
198
- - `SECRET` Algorithm secret.
222
+ - `DIDS` (optional) Input dataset(s) DID's, must have format: `["abc..90"]`. Defaults to reading them automatically from the `DDO` data directory.
223
+ - `TRANSFORMATION_DID` (optional, default="DEFAULT"): Algorithm DID, must have format: `abc..90`.
224
+ - `SECRET` (optional, default="DEFAULT"): Algorithm secret.
199
225
  - `BASE_DIR` (optional, default="/data"): Base path to the OceanProtocol data directories.
200
- - `RUNTIME` (optional, default="dev"): Runtime mode
@@ -0,0 +1,7 @@
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,6 +0,0 @@
1
- from enum import Enum
2
-
3
-
4
- class RuntimeMode(Enum):
5
- DEV = "dev"
6
- TEST = "test"
@@ -1,8 +0,0 @@
1
- ocean_runner/__init__.py,sha256=awAmE6kZhuwcrD3gT7qFZArdhiuzW-EFTA6tGKhw06k,138
2
- ocean_runner/config.py,sha256=zzpGxotGhGnNdHKFJqQ7fuXK5zW2IGOpzftHilcySD4,1644
3
- ocean_runner/runner.py,sha256=j0BOuxO9PgI2wV7812VN9FghgwZz4cLjV0bu_mHP3YA,4574
4
- ocean_runner/runtime_mode.py,sha256=WbGTaoL3hxBWbxM8luwyOwwtQonqyIhbus0_Jd-F-3k,83
5
- ocean_runner-0.2.7.dist-info/METADATA,sha256=d0KyDgAwzqadDcnelkgENnmCUWU_fXFh7CMKndatRYE,6101
6
- ocean_runner-0.2.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- ocean_runner-0.2.7.dist-info/licenses/LICENSE,sha256=_B25KqK4amoADWkMN150tnZFm_Fy7VvZpvIC8ZydWdI,1053
8
- ocean_runner-0.2.7.dist-info/RECORD,,