ocean-runner 0.2.13__tar.gz → 0.2.14__tar.gz

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.

Potentially problematic release.


This version of ocean-runner might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ocean-runner
3
- Version: 0.2.13
3
+ Version: 0.2.14
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
@@ -2,7 +2,7 @@ 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, TypeVar
5
+ from typing import Iterable, TypeVar
6
6
 
7
7
  T = TypeVar("T")
8
8
 
@@ -43,9 +43,6 @@ class Config:
43
43
  custom_input: T | None = None
44
44
  """Algorithm's custom input types, must be a dataclass_json"""
45
45
 
46
- error_callback: Callable[[Exception], None] = None
47
- """Callback to execute upon exceptions"""
48
-
49
46
  logger: Logger | None = None
50
47
  """Logger to use in the algorithm"""
51
48
 
@@ -54,7 +51,5 @@ class Config:
54
51
  )
55
52
  """Paths that should be included so the code executes correctly"""
56
53
 
57
- environment: Environment = field(
58
- default_factory=lambda: Environment(),
59
- )
54
+ environment: Environment = field(default_factory=lambda: Environment())
60
55
  """Mock of environment data"""
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import InitVar, asdict, dataclass, field
4
+ from logging import Logger
5
+ from pathlib import Path
6
+ from typing import Callable, Generic, Self, TypeVar
7
+
8
+ from oceanprotocol_job_details import JobDetails
9
+
10
+ from ocean_runner.config import Config
11
+
12
+ JobDetailsT = TypeVar("JobDetailsT")
13
+ ResultT = TypeVar("ResultT")
14
+
15
+
16
+ def default_error_callback(_, e: Exception) -> None:
17
+ raise e
18
+
19
+
20
+ def default_validation(algorithm: Algorithm) -> None:
21
+ algorithm.logger.info("Validating input using default validation")
22
+ assert algorithm.job_details.ddos, "DDOs missing"
23
+ assert algorithm.job_details.files, "Files missing"
24
+
25
+
26
+ def default_save(*, result: ResultT, base: Path, algorithm: Algorithm) -> None:
27
+ algorithm.logger.info("Saving results using default save")
28
+ with open(base / "result.txt", "w+") as f:
29
+ f.write(str(result))
30
+
31
+
32
+ @dataclass
33
+ class Algorithm(Generic[JobDetailsT, ResultT]):
34
+ """
35
+ A configurable algorithm runner that behaves like a FastAPI app:
36
+ - You register `validate`, `run`, and `save_results` via decorators.
37
+ - You execute the full pipeline by calling `app()`.
38
+ """
39
+
40
+ config: InitVar[Config | None] = None
41
+ logger: Logger = field(init=False)
42
+ _job_details: JobDetails[JobDetailsT] = field(init=False)
43
+ _result: ResultT | None = field(default=None, init=False)
44
+
45
+ error_callback: Callable[[Algorithm, Exception], None] = default_error_callback
46
+
47
+ # Decorator-registered callbacks
48
+ _validate_fn: Callable[[Algorithm], None] | None = field(default=None, init=False)
49
+ _run_fn: Callable[[Algorithm], ResultT] | None = field(default=None, init=False)
50
+ _save_fn: Callable[[ResultT, Path, Algorithm], None] | None = field(
51
+ default=None, init=False
52
+ )
53
+
54
+ def __post_init__(self, config: Config | None) -> None:
55
+ config: Config = config or Config()
56
+
57
+ # Configure logger
58
+ if config.logger:
59
+ self.logger = config.logger
60
+ else:
61
+ import logging
62
+
63
+ logging.basicConfig(
64
+ level=logging.DEBUG,
65
+ format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
66
+ datefmt="%Y-%m-%d %H:%M:%S",
67
+ )
68
+ self.logger = logging.getLogger("ocean_runner")
69
+
70
+ # Normalize base_dir
71
+ if isinstance(config.environment.base_dir, str):
72
+ config.environment.base_dir = Path(config.environment.base_dir)
73
+
74
+ # Extend sys.path for custom imports
75
+ if config.source_paths:
76
+ import sys
77
+
78
+ sys.path.extend([str(path.absolute()) for path in config.source_paths])
79
+ self.logger.debug(f"Added [{len(config.source_paths)}] entries to PATH")
80
+
81
+ # Load job details
82
+ self._job_details = JobDetails.load(
83
+ _type=config.custom_input,
84
+ base_dir=config.environment.base_dir,
85
+ dids=config.environment.dids,
86
+ transformation_did=config.environment.transformation_did,
87
+ secret=config.environment.secret,
88
+ )
89
+
90
+ self.logger.info("Loaded JobDetails")
91
+ self.logger.debug(asdict(self.job_details))
92
+
93
+ self.config = config
94
+
95
+ class Error(RuntimeError): ...
96
+
97
+ @property
98
+ def job_details(self) -> JobDetails:
99
+ if not self._job_details:
100
+ raise Algorithm.Error("JobDetails not initialized or missing")
101
+ return self._job_details
102
+
103
+ @property
104
+ def result(self) -> ResultT:
105
+ if self._result is None:
106
+ raise Algorithm.Error("Result missing, run the algorithm first")
107
+ return self._result
108
+
109
+ # ---------------------------
110
+ # Decorators (FastAPI-style)
111
+ # ---------------------------
112
+
113
+ def validate(self, fn: Callable[[Self], None]) -> Callable[[Self], None]:
114
+ self._validate_fn = fn
115
+ return fn
116
+
117
+ def run(self, fn: Callable[[Self], ResultT]) -> Callable[[Self], ResultT]:
118
+ self._run_fn = fn
119
+ return fn
120
+
121
+ def save_results(self, fn: Callable[[ResultT, Path, Algorithm], None]) -> Callable:
122
+ self._save_fn = fn
123
+ return fn
124
+
125
+ def on_error(self, fn: Callable[[Algorithm, Exception], None]) -> Callable:
126
+ self.error_callback = fn
127
+ return fn
128
+
129
+ # ---------------------------
130
+ # Execution Pipeline
131
+ # ---------------------------
132
+
133
+ def __call__(self) -> ResultT | None:
134
+ """Executes the algorithm pipeline: validate → run → save_results."""
135
+ try:
136
+ # Validation step
137
+ if self._validate_fn:
138
+ self.logger.info("Running custom validation...")
139
+ self._validate_fn(self)
140
+ else:
141
+ self.logger.info("Running default validation...")
142
+ default_validation(self)
143
+
144
+ # Run step
145
+ if self._run_fn:
146
+ self.logger.info("Running algorithm...")
147
+ self._result = self._run_fn(self)
148
+ else:
149
+ self.logger.warning("No run() function defined. Skipping execution.")
150
+ self._result = None
151
+
152
+ # Save step
153
+ if self._save_fn:
154
+ self.logger.info("Saving results...")
155
+ self._save_fn(
156
+ self._result,
157
+ self.job_details.paths.outputs,
158
+ self,
159
+ )
160
+ else:
161
+ self.logger.info("No save_results() defined. Using default.")
162
+ default_save(
163
+ result=self._result,
164
+ base=self.job_details.paths.outputs,
165
+ algorithm=self,
166
+ )
167
+
168
+ except Exception as e:
169
+ self.logger.exception("Error during algorithm execution")
170
+ self.error_callback(self, e)
171
+
172
+ return self._result
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ocean-runner"
3
- version = "0.2.13"
3
+ version = "0.2.14"
4
4
  description = "A fluent API for OceanProtocol algorithms"
5
5
  authors = [
6
6
  { name = "AgrospAI", email = "agrospai@udl.cat" },
@@ -1,178 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import inspect
4
- from dataclasses import InitVar, asdict, dataclass, field
5
- from logging import Logger
6
- from pathlib import Path
7
- from typing import Callable, Generic, Self, TypeVar
8
-
9
- from oceanprotocol_job_details import JobDetails
10
-
11
- from ocean_runner.config import Config
12
-
13
- JobDetailsT = TypeVar("JobDetailsT")
14
- ResultT = TypeVar("ResultT")
15
-
16
-
17
- def default_error_callback(_: Algorithm, e: Exception) -> None:
18
- raise e
19
-
20
-
21
- def default_validation(algorithm: Algorithm) -> None:
22
- algorithm.logger.info("Validating input using default validation")
23
- assert algorithm.job_details.ddos, "DDOs missing"
24
- assert algorithm.job_details.files, "Files missing"
25
-
26
-
27
- def default_save(*, result: ResultT, base: Path, algorithm: Algorithm) -> None:
28
- algorithm.logger.info("Saving results using default save")
29
- with open(base / "result.txt", "w+") as f:
30
- f.write(str(result))
31
-
32
-
33
- @dataclass
34
- class Algorithm(Generic[JobDetailsT, ResultT]):
35
-
36
- config: InitVar[Config | None] = None
37
- logger: Logger = field(init=False)
38
- _job_details: JobDetails[JobDetailsT] = field(init=False)
39
- _result: ResultT | None = field(default=None, init=False)
40
- error_callback = default_error_callback
41
-
42
- def __post_init__(self, config: Config | None) -> None:
43
- config: Config = config or Config()
44
- if config.error_callback:
45
- self.error_callback = config.error_callback
46
-
47
- self._setup_logger(config)
48
- self._load_job_details(config)
49
- self._load_user_defined_functions()
50
- self._maybe_autorun(config)
51
-
52
- # ---------------------
53
- # Setup helpers
54
- # ---------------------
55
-
56
- def _setup_logger(self, config: Config) -> None:
57
- if config.logger:
58
- self.logger = config.logger
59
- else:
60
- import logging
61
-
62
- logging.basicConfig(
63
- level=logging.DEBUG,
64
- format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
65
- datefmt="%Y-%m-%d %H:%M:%S",
66
- )
67
- self.logger = logging.getLogger("ocean_runner")
68
-
69
- def _load_job_details(self, config: Config) -> None:
70
- if isinstance(config.environment.base_dir, str):
71
- config.environment.base_dir = Path(config.environment.base_dir)
72
- if config.source_paths:
73
- import sys
74
-
75
- sys.path.extend([str(path.absolute()) for path in config.source_paths])
76
- self.logger.debug(f"Added [{len(config.source_paths)}] entries to PATH")
77
-
78
- self._job_details = JobDetails.load(
79
- _type=config.custom_input,
80
- base_dir=config.environment.base_dir,
81
- dids=config.environment.dids,
82
- transformation_did=config.environment.transformation_did,
83
- secret=config.environment.secret,
84
- )
85
- self.logger.info("Loaded JobDetails")
86
- self.logger.debug(asdict(self.job_details))
87
-
88
- # ---------------------
89
- # Auto-detect functions
90
- # ---------------------
91
-
92
- def _load_user_defined_functions(self) -> None:
93
- caller = inspect.getmodule(inspect.stack()[2][0])
94
- if not caller:
95
- return
96
-
97
- for name, default in {
98
- "validation": default_validation,
99
- "run": None,
100
- "save": default_save,
101
- }.items():
102
- fn = getattr(caller, name, None)
103
- if callable(fn):
104
- self.logger.debug(f"Found user-defined '{name}' function")
105
- setattr(self, f"_user_{name}", fn)
106
- elif default:
107
- setattr(self, f"_user_{name}", default)
108
-
109
- self._caller_module = caller
110
-
111
- # ---------------------
112
- # Auto-run logic
113
- # ---------------------
114
-
115
- def _maybe_autorun(self, config) -> None:
116
- """Automatically runs if caller has __ocean_runner_autorun__ = True"""
117
- if (
118
- getattr(self, "_caller_module", None)
119
- and getattr(self._caller_module, "__ocean_runner_autorun__", False)
120
- and not getattr(config, "_from_test", False)
121
- ):
122
- self.logger.info("Auto-running algorithm...")
123
- self.validate().run().save_results()
124
-
125
- # ---------------------
126
- # Main API
127
- # ---------------------
128
-
129
- @property
130
- def job_details(self) -> JobDetails:
131
- if not self._job_details:
132
- raise Algorithm.Error("JobDetails not initialized or missing")
133
- return self._job_details
134
-
135
- @property
136
- def result(self) -> ResultT:
137
- if self._result is None:
138
- raise Algorithm.Error("Result missing, run the algorithm first")
139
- return self._result
140
-
141
- def validate(self, callback: Callable[[Self], None] | None = None) -> Self:
142
- callback = callback or getattr(self, "_user_validation", default_validation)
143
- self.logger.info("Validating instance...")
144
- try:
145
- callback(self)
146
- except Exception as e:
147
- self.error_callback(e)
148
- return self
149
-
150
- def run(self, callback: Callable[[Self], ResultT] | None = None) -> Self:
151
- callback = callback or getattr(self, "_user_run", None)
152
- if callback is None:
153
- raise Algorithm.Error("No 'run' function found")
154
- self.logger.info("Running algorithm...")
155
- try:
156
- self._result = callback(self)
157
- except Exception as e:
158
- self.error_callback(e)
159
- return self
160
-
161
- def save_results(
162
- self,
163
- callback: Callable[[ResultT, Path, Algorithm], None] | None = None,
164
- *,
165
- override_path: Path | None = None,
166
- ) -> None:
167
- callback = callback or getattr(self, "_user_save", default_save)
168
- self.logger.info("Saving results...")
169
- try:
170
- callback(
171
- result=self.result,
172
- base=override_path or self.job_details.paths.outputs,
173
- algorithm=self,
174
- )
175
- except Exception as e:
176
- self.error_callback(e)
177
-
178
- class Error(RuntimeError): ...
File without changes
File without changes
File without changes