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 CHANGED
@@ -1,4 +1,4 @@
1
1
  from ocean_runner.config import Config, Environment
2
2
  from ocean_runner.runner import Algorithm
3
3
 
4
- __all__ = [Config, Algorithm, Environment]
4
+ __all__ = [Config, Algorithm, Environment] # type: ignore
ocean_runner/config.py CHANGED
@@ -1,55 +1,71 @@
1
- import os
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 Iterable, TypeVar
4
+ from typing import Generic, Sequence, TypeVar
6
5
 
7
- T = TypeVar("T")
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
- @dataclass
13
- class Environment:
14
- """Environment variables mock"""
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
- dids: str = field(
22
- default_factory=lambda: os.environ.get("DIDS", None),
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
- transformation_did: str = field(
27
- default_factory=lambda: os.environ.get("TRANSFORMATION_DID", DEFAULT),
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
- secret: str = field(
32
- default_factory=lambda: os.environ.get("SECRET", DEFAULT),
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
- dict = asdict
42
+ secret: str = Field(
43
+ default=DEFAULT,
44
+ validation_alias=Keys.SECRET.value,
45
+ description="Super secret secret",
46
+ )
37
47
 
38
48
 
39
- @dataclass
40
- class Config:
49
+ class Config(BaseModel, Generic[InputT]):
41
50
  """Algorithm overall configuration"""
42
51
 
43
- custom_input: T | None = None
44
- """Algorithm's custom input types, must be a dataclass_json"""
52
+ model_config = ConfigDict(arbitrary_types_allowed=True)
45
53
 
46
- logger: Logger | None = None
47
- """Logger to use in the algorithm"""
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: Iterable[Path] = field(
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 = field(default_factory=lambda: Environment())
55
- """Mock of environment data"""
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
- 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, 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
- JobDetailsT = TypeVar("JobDetailsT")
14
+ InputT = TypeVar("InputT")
13
15
  ResultT = TypeVar("ResultT")
16
+ T = TypeVar("T")
14
17
 
15
18
 
16
- def default_error_callback(algorithm: Algorithm, e: Exception) -> None:
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 e
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(*, result: ResultT, base: Path, algorithm: Algorithm) -> None:
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[JobDetailsT, ResultT]):
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
- _save_fn: Callable[[ResultT, Path, Algorithm], None] | None = field(
60
- default=None,
61
- init=False,
62
- repr=False,
63
- )
72
+ logger: Logger = field(init=False, repr=False)
64
73
 
65
- _error_callback: Callable[[Algorithm, Exception], None] | None = field(
66
- default=None,
67
- init=False,
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
- config: Config = config or Config()
80
+ def __post_init__(self, config: Config[InputT] | None) -> None:
81
+ configuration = config or Config()
73
82
 
74
83
  # Configure logger
75
- if config.logger:
76
- self.logger = config.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("ocean_runner")
94
+ self.logger = logging.getLogger(__name__)
86
95
 
87
96
  # Normalize base_dir
88
- if isinstance(config.environment.base_dir, str):
89
- config.environment.base_dir = Path(config.environment.base_dir)
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 config.source_paths:
103
+ if configuration.source_paths:
93
104
  import sys
94
105
 
95
- sys.path.extend([str(path.absolute()) for path in config.source_paths])
96
- self.logger.debug(f"Added [{len(config.source_paths)}] entries to PATH")
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.config = config
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: Callable[[], None]) -> Callable[[], None]:
119
- self._validate_fn = fn
133
+ def validate(self, fn: ValidateFuncT) -> ValidateFuncT:
134
+ self._functions.validate = fn
120
135
  return fn
121
136
 
122
- def run(self, fn: Callable[[], ResultT]) -> Callable[[], ResultT]:
123
- self._run_fn = fn
137
+ def run(self, fn: RunFuncT) -> RunFuncT:
138
+ self._functions.run = fn
124
139
  return fn
125
140
 
126
- def save_results(self, fn: Callable[[ResultT, Path], None]) -> Callable:
127
- self._save_fn = fn
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: Callable[[Exception], None]) -> Callable:
131
- self._error_callback = fn
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 __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,
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(asdict(self.job_details))
165
+ self.logger.debug(self.job_details.model_dump())
151
166
 
152
167
  try:
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)
168
+ await execute(self._functions.validate, self)
160
169
 
161
- # Run step
162
- if self._run_fn:
170
+ if self._functions.run:
163
171
  self.logger.info("Running algorithm...")
164
- self._result = self._run_fn()
172
+ self._result = await execute(self._functions.run, self)
165
173
  else:
166
- self.logger.warning("No run() function defined. Skipping execution.")
174
+ self.logger.error("No run() function defined. Skipping execution.")
167
175
  self._result = None
168
176
 
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
- )
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
- if self._error_callback:
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.18
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: oceanprotocol-job-details>=0.2.8
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,,