oceanprotocol-job-details 0.0.8__tar.gz → 0.0.10__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.
- oceanprotocol_job_details-0.0.10/.gitignore +6 -0
- oceanprotocol_job_details-0.0.10/PKG-INFO +83 -0
- {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/README.md +21 -1
- {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/dataclasses/constants.py +2 -2
- {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/dataclasses/job_details.py +3 -9
- oceanprotocol_job_details-0.0.10/oceanprotocol_job_details/dataclasses/ocean.py +67 -0
- {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/job_details.py +27 -9
- oceanprotocol_job_details-0.0.10/oceanprotocol_job_details/loaders/impl/map.py +120 -0
- oceanprotocol_job_details-0.0.10/oceanprotocol_job_details/loaders/impl/utils.py +57 -0
- {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/pyproject.toml +11 -12
- oceanprotocol_job_details-0.0.8/PKG-INFO +0 -62
- oceanprotocol_job_details-0.0.8/oceanprotocol_job_details/loaders/impl/map.py +0 -121
- {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/LICENSE +0 -0
- {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/__init__.py +0 -0
- {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/dataclasses/__init__.py +0 -0
- {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/loaders/__init__.py +0 -0
- {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/loaders/impl/__init__.py +0 -0
- {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/loaders/loader.py +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oceanprotocol-job-details
|
|
3
|
+
Version: 0.0.10
|
|
4
|
+
Summary: A Python package to get details from OceanProtocol jobs
|
|
5
|
+
Project-URL: Homepage, https://github.com/AgrospAI/oceanprotocol-job-details
|
|
6
|
+
Project-URL: Issues, https://github.com/AgrospAI/oceanprotocol-job-details/issues
|
|
7
|
+
Author-email: Christian López García <christian.lopez@udl.cat>
|
|
8
|
+
License: Copyright 2025 Agrospai
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: orjson>=3.10.15
|
|
21
|
+
Requires-Dist: pydantic>=2.10.6
|
|
22
|
+
Requires-Dist: pytest<9,>=8.3.4
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
A Python package to get details from OceanProtocol jobs
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
pip install oceanprotocol-job-details
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
As a simple library, we only need to import the main object and use it once:
|
|
38
|
+
|
|
39
|
+
```Python
|
|
40
|
+
from oceanprotocol_job_details.job_details import OceanProtocolJobDetails
|
|
41
|
+
|
|
42
|
+
# Using default parameters
|
|
43
|
+
job_details = OceanProtocolJobDetails().load()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Assumes the following directory structure:
|
|
47
|
+
```
|
|
48
|
+
<ROOT_FOLDER>
|
|
49
|
+
└───data
|
|
50
|
+
├───ddos
|
|
51
|
+
├───inputs
|
|
52
|
+
└───logs
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Core functionalities
|
|
56
|
+
|
|
57
|
+
Given the Ocean Protocol job details structure as in [https://github.com/GX4FM-Base-X/pontus-x-ontology](Pontus-X Ontology), parses the passed algorithm parameters into an object to use in your algorithms.
|
|
58
|
+
|
|
59
|
+
1. Parsing JSON
|
|
60
|
+
1. Validation
|
|
61
|
+
1. Metadata and service extraction
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
### Advanced Usage (not recommended)
|
|
65
|
+
|
|
66
|
+
If instead of the environment variables, we want to use another kind of mapping, can pass it as a parameter and it will work as long as it has the same key values (Can be implemented in a more generic way, but there is no need right now).
|
|
67
|
+
|
|
68
|
+
```Python
|
|
69
|
+
from oceanprotocol_job_details.job_details import OceanProtocolJobDetails
|
|
70
|
+
from oceanprotocol_job_details.loaders.impl.environment import Keys
|
|
71
|
+
|
|
72
|
+
# Fill in with values that will be used instead of env
|
|
73
|
+
custom_mapper = {
|
|
74
|
+
Keys.ROOT_FOLDER: " ... ", # Use when you don't want the algorithm to take '/' as base Path
|
|
75
|
+
Keys.ALGORITHM: " ... ",
|
|
76
|
+
Keys.DIDS: " ... ",
|
|
77
|
+
Keys.SECRET: " ... ",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
job_details = OceanProtocolJobDetails(mapper=custom_mapper).load()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
|
|
@@ -19,6 +19,24 @@ from oceanprotocol_job_details.job_details import OceanProtocolJobDetails
|
|
|
19
19
|
job_details = OceanProtocolJobDetails().load()
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
Assumes the following directory structure:
|
|
23
|
+
```
|
|
24
|
+
<ROOT_FOLDER>
|
|
25
|
+
└───data
|
|
26
|
+
├───ddos
|
|
27
|
+
├───inputs
|
|
28
|
+
└───logs
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Core functionalities
|
|
32
|
+
|
|
33
|
+
Given the Ocean Protocol job details structure as in [https://github.com/GX4FM-Base-X/pontus-x-ontology](Pontus-X Ontology), parses the passed algorithm parameters into an object to use in your algorithms.
|
|
34
|
+
|
|
35
|
+
1. Parsing JSON
|
|
36
|
+
1. Validation
|
|
37
|
+
1. Metadata and service extraction
|
|
38
|
+
|
|
39
|
+
|
|
22
40
|
### Advanced Usage (not recommended)
|
|
23
41
|
|
|
24
42
|
If instead of the environment variables, we want to use another kind of mapping, can pass it as a parameter and it will work as long as it has the same key values (Can be implemented in a more generic way, but there is no need right now).
|
|
@@ -29,11 +47,13 @@ from oceanprotocol_job_details.loaders.impl.environment import Keys
|
|
|
29
47
|
|
|
30
48
|
# Fill in with values that will be used instead of env
|
|
31
49
|
custom_mapper = {
|
|
50
|
+
Keys.ROOT_FOLDER: " ... ", # Use when you don't want the algorithm to take '/' as base Path
|
|
32
51
|
Keys.ALGORITHM: " ... ",
|
|
33
52
|
Keys.DIDS: " ... ",
|
|
34
|
-
Keys.ROOT: " ... ",
|
|
35
53
|
Keys.SECRET: " ... ",
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
job_details = OceanProtocolJobDetails(mapper=custom_mapper).load()
|
|
39
57
|
```
|
|
58
|
+
|
|
59
|
+
|
|
@@ -20,11 +20,11 @@ class _ServiceType:
|
|
|
20
20
|
METADATA: str = "metadata"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
@dataclass(
|
|
23
|
+
@dataclass()
|
|
24
24
|
class _Paths:
|
|
25
25
|
"""Common paths used in the Ocean Protocol directories"""
|
|
26
26
|
|
|
27
|
-
DATA: Path = Path("data")
|
|
27
|
+
DATA: Path = Path("/data")
|
|
28
28
|
|
|
29
29
|
INPUTS: Path = DATA / "inputs"
|
|
30
30
|
DDOS: Path = DATA / "ddos"
|
|
@@ -35,9 +35,6 @@ class Algorithm:
|
|
|
35
35
|
class JobDetails:
|
|
36
36
|
"""Details of the current job, such as the used inputs and algorithm"""
|
|
37
37
|
|
|
38
|
-
root: Path
|
|
39
|
-
"""The root folder of the Ocean Protocol directories"""
|
|
40
|
-
|
|
41
38
|
dids: Sequence[Path]
|
|
42
39
|
"""Identifiers for the inputs"""
|
|
43
40
|
|
|
@@ -54,13 +51,10 @@ class JobDetails:
|
|
|
54
51
|
_parameters: InitVar[Optional[_MetadataType]] = None
|
|
55
52
|
|
|
56
53
|
def __post_init__(self, _):
|
|
57
|
-
os.makedirs(
|
|
54
|
+
os.makedirs(Paths.LOGS, exist_ok=True)
|
|
58
55
|
|
|
59
56
|
logging.getLogger().addHandler(
|
|
60
|
-
logging.FileHandler(
|
|
61
|
-
self.root / Paths.LOGS / "job_details.log",
|
|
62
|
-
mode="w",
|
|
63
|
-
)
|
|
57
|
+
logging.FileHandler(Paths.LOGS / "job_details.log", mode="w")
|
|
64
58
|
)
|
|
65
59
|
|
|
66
60
|
@property
|
|
@@ -68,7 +62,7 @@ class JobDetails:
|
|
|
68
62
|
"""Parameters for algorithm job, read from default path"""
|
|
69
63
|
|
|
70
64
|
if parameters is None:
|
|
71
|
-
parameters =
|
|
65
|
+
parameters = Paths.ALGORITHM_CUSTOM_PARAMETERS
|
|
72
66
|
|
|
73
67
|
if self._parameters is None:
|
|
74
68
|
if not parameters.exists():
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from dataclasses import Field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Annotated, Any, List, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, HttpUrl
|
|
6
|
+
|
|
7
|
+
"""Base classes for the Ocean Protocol algorithm structure"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Credential:
|
|
11
|
+
type: Annotated[str, Field(frozen=True)]
|
|
12
|
+
values: Annotated[List[str], Field(frozen=True)]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Credentials:
|
|
16
|
+
allow: Optional[Annotated[List[Credential], Field(frozen=True)]] = []
|
|
17
|
+
deny: Optional[Annotated[List[Credential], Field(frozen=True)]] = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Metadata(BaseModel):
|
|
21
|
+
"""Base class for the Metadata structure"""
|
|
22
|
+
|
|
23
|
+
description: Annotated[str, Field(frozen=True)]
|
|
24
|
+
name: Annotated[str, Field(frozen=True)]
|
|
25
|
+
type: Annotated[str, Field(frozen=True)]
|
|
26
|
+
author: Annotated[str, Field(frozen=True)]
|
|
27
|
+
license: Annotated[str, Field(frozen=True)]
|
|
28
|
+
|
|
29
|
+
algorithm: Any
|
|
30
|
+
tags: Optional[Annotated[List[str], Field(frozen=True)]] = None
|
|
31
|
+
created: Optional[Annotated[datetime, Field(frozen=True)]] = None
|
|
32
|
+
updated: Optional[Annotated[datetime, Field(frozen=True)]] = None
|
|
33
|
+
copyrightHolder: Optional[Annotated[str, Field(frozen=True)]] = None
|
|
34
|
+
links: Optional[Annotated[List[HttpUrl], Field(frozen=True)]] = None
|
|
35
|
+
contentLanguage: Optional[Annotated[str, Field(frozen=True)]] = None
|
|
36
|
+
categories: Optional[Annotated[List[str], Field(frozen=True)]] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Service(BaseModel):
|
|
40
|
+
"""Base class for the Service structure"""
|
|
41
|
+
|
|
42
|
+
id: Annotated[str, Field(frozen=True)]
|
|
43
|
+
type: Annotated[str, Field(frozen=True)]
|
|
44
|
+
timeout: Annotated[int, Field(frozen=True)]
|
|
45
|
+
files: Annotated[str, Field(frozen=True)]
|
|
46
|
+
datatokenAddress: Annotated[str, Field(frozen=True)]
|
|
47
|
+
serviceEndpoint: Annotated[HttpUrl, Field(frozen=True)]
|
|
48
|
+
|
|
49
|
+
compute: Any
|
|
50
|
+
consumerParameters: Any
|
|
51
|
+
additionalInformation: Any
|
|
52
|
+
name: Optional[Annotated[str, Field(frozen=True)]] = None
|
|
53
|
+
description: Optional[Annotated[str, Field(frozen=True)]] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DDO(BaseModel):
|
|
57
|
+
"""DDO structure in Ocean Protocol"""
|
|
58
|
+
|
|
59
|
+
id: Annotated[str, Field(frozen=True)]
|
|
60
|
+
context: Annotated[List[str], Field(frozen=True)]
|
|
61
|
+
version: Annotated[str, Field(frozen=True)]
|
|
62
|
+
chainId: Annotated[int, Field(frozen=True)]
|
|
63
|
+
nftAddress: Annotated[str, Field(frozen=True)]
|
|
64
|
+
metadata: Annotated[Metadata, Field(frozen=True)]
|
|
65
|
+
services: Annotated[List[Service], Field(frozen=True)]
|
|
66
|
+
|
|
67
|
+
credentials: Annotated[Optional[str], Field(frozen=True)] = None
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from ctypes import ArgumentError
|
|
1
2
|
import logging
|
|
2
3
|
import os
|
|
3
4
|
from typing import Any, Literal, Mapping, Optional
|
|
@@ -10,12 +11,10 @@ from oceanprotocol_job_details.loaders.loader import Loader
|
|
|
10
11
|
logging.basicConfig(
|
|
11
12
|
level=logging.INFO,
|
|
12
13
|
format="%(asctime)s [%(threadName)s] [%(levelname)s] %(message)s",
|
|
13
|
-
handlers=[
|
|
14
|
-
logging.StreamHandler(),
|
|
15
|
-
],
|
|
14
|
+
handlers=[logging.StreamHandler()],
|
|
16
15
|
)
|
|
17
16
|
|
|
18
|
-
_Implementations = Literal["
|
|
17
|
+
_Implementations = Literal["map"]
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
class OceanProtocolJobDetails(Loader[JobDetails]):
|
|
@@ -29,14 +28,33 @@ class OceanProtocolJobDetails(Loader[JobDetails]):
|
|
|
29
28
|
*args,
|
|
30
29
|
**kwargs,
|
|
31
30
|
):
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
match implementation.lower():
|
|
32
|
+
case "map":
|
|
33
|
+
self._loader = lambda: Map(mapper=mapper, keys=keys, *args, **kwargs)
|
|
34
|
+
case _:
|
|
35
|
+
raise ArgumentError(f"Implementation {implementation} not valid")
|
|
37
36
|
|
|
38
37
|
def load(self) -> JobDetails:
|
|
39
38
|
return self._loader().load()
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
del _Implementations
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _main():
|
|
45
|
+
"""Main function to test functionalities"""
|
|
46
|
+
|
|
47
|
+
# Re-define logging configuration
|
|
48
|
+
logging.basicConfig(
|
|
49
|
+
level=logging.DEBUG,
|
|
50
|
+
format="%(asctime)s [%(threadName)s] [%(levelname)s] %(message)s",
|
|
51
|
+
handlers=[logging.StreamHandler()],
|
|
52
|
+
force=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
job_details = OceanProtocolJobDetails().load()
|
|
56
|
+
logging.info(f"Loaded job details: {job_details}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
_main()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Loads the current Job Details from the environment variables, could be abstracted to a more general 'mapper loader' but won't, since right now it fits our needs"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Mapping, Optional, Sequence, final
|
|
7
|
+
|
|
8
|
+
from orjson import JSONDecodeError, loads
|
|
9
|
+
|
|
10
|
+
from oceanprotocol_job_details.dataclasses.constants import DidKeys, Paths, ServiceType
|
|
11
|
+
from oceanprotocol_job_details.dataclasses.job_details import Algorithm, JobDetails
|
|
12
|
+
from oceanprotocol_job_details.loaders.impl.utils import do, execute_predicate
|
|
13
|
+
from oceanprotocol_job_details.loaders.loader import Loader
|
|
14
|
+
|
|
15
|
+
logger = getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Keys:
|
|
20
|
+
"""Environment keys passed to the algorithm"""
|
|
21
|
+
|
|
22
|
+
ROOT_FOLDER = "ROOT_FOLDER"
|
|
23
|
+
SECRET: str = "secret"
|
|
24
|
+
ALGORITHM: str = "TRANSFORMATION_DID"
|
|
25
|
+
DIDS: str = "DIDS"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _update_paths_from_root(root: Path):
|
|
29
|
+
"""Update the default from a root folder
|
|
30
|
+
|
|
31
|
+
:param root: root folder to update the paths
|
|
32
|
+
:type root: Path
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
Paths.DATA = root / "data"
|
|
36
|
+
Paths.INPUTS = Paths.DATA / "inputs"
|
|
37
|
+
Paths.DDOS = Paths.DATA / "ddos"
|
|
38
|
+
Paths.OUTPUTS = Paths.DATA / "outputs"
|
|
39
|
+
Paths.LOGS = Paths.DATA / "logs"
|
|
40
|
+
Paths.ALGORITHM_CUSTOM_PARAMETERS = Paths.INPUTS / "algoCustomData.json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _files_from_service(service):
|
|
44
|
+
return service[DidKeys.ATTRIBUTES][DidKeys.MAIN][DidKeys.FILES]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@final
|
|
48
|
+
class Map(Loader[JobDetails]):
|
|
49
|
+
"""Loads the current Job Details from the environment variables"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, mapper: Mapping[str, str], keys: Keys, *args, **kwargs) -> None:
|
|
52
|
+
super().__init__(*args, **kwargs)
|
|
53
|
+
|
|
54
|
+
self._mapper = mapper
|
|
55
|
+
self._keys = keys
|
|
56
|
+
|
|
57
|
+
execute_predicate(
|
|
58
|
+
lambda: _update_paths_from_root(Path(self._mapper[Keys.ROOT_FOLDER])),
|
|
59
|
+
lambda: Keys.ROOT_FOLDER in self._mapper,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def load(self, *args, **kwargs) -> JobDetails:
|
|
63
|
+
return self._from_dids(self._dids())
|
|
64
|
+
|
|
65
|
+
def _from_dids(self, dids: Sequence[Path]) -> JobDetails:
|
|
66
|
+
return JobDetails(
|
|
67
|
+
dids=dids,
|
|
68
|
+
files=self._files(dids),
|
|
69
|
+
algorithm=self._algorithm(),
|
|
70
|
+
secret=self._secret(),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def _dids(self) -> Sequence[Path]:
|
|
74
|
+
return loads(self._mapper.get(self._keys.DIDS, []))
|
|
75
|
+
|
|
76
|
+
def _files(self, dids: Optional[Sequence[Path]]) -> Mapping[str, Sequence[Path]]:
|
|
77
|
+
"""Iterate through the given DIDs and retrieve their respective filepaths
|
|
78
|
+
|
|
79
|
+
:param dids: dids to read the files from
|
|
80
|
+
:type dids: Optional[Sequence[Path]]
|
|
81
|
+
:raises FileNotFoundError: if the DDO file does not exist
|
|
82
|
+
:return: _description_
|
|
83
|
+
:rtype: Mapping[str, Sequence[Path]]
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
files: Mapping[str, Sequence[Path]] = {}
|
|
87
|
+
for did in dids:
|
|
88
|
+
# For each given DID, check if the DDO file exists and read its metadata
|
|
89
|
+
|
|
90
|
+
ddo_path = Paths.DDOS / did
|
|
91
|
+
do(lambda: ddo_path.exists(), exc=FileNotFoundError("Missing DDO file"))
|
|
92
|
+
|
|
93
|
+
with open(ddo_path, "r") as f:
|
|
94
|
+
ddo = do(lambda: loads(f.read()), JSONDecodeError)
|
|
95
|
+
if not ddo:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
for service in do(lambda: ddo[DidKeys.SERVICE], KeyError, default=[]):
|
|
99
|
+
if service[DidKeys.SERVICE_TYPE] != ServiceType.METADATA:
|
|
100
|
+
continue # Only read the metadata of the services
|
|
101
|
+
|
|
102
|
+
files_n = do(lambda: len(_files_from_service(service)), KeyError)
|
|
103
|
+
ddo_path = Paths.INPUTS / did
|
|
104
|
+
files[did] = [ddo_path / str(idx) for idx in range(files_n)]
|
|
105
|
+
return files
|
|
106
|
+
|
|
107
|
+
def _algorithm(self) -> Optional[Algorithm]:
|
|
108
|
+
did = self._mapper.get(self._keys.ALGORITHM, None)
|
|
109
|
+
if not did:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
ddo = Paths.DDOS / did
|
|
113
|
+
|
|
114
|
+
return Algorithm(
|
|
115
|
+
did,
|
|
116
|
+
do(lambda: ddo.exists() and ddo, exc=FileNotFoundError("Missing DDO file")),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def _secret(self) -> Optional[str]:
|
|
120
|
+
return self._mapper.get(self._keys.SECRET, None)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from logging import WARNING, getLogger
|
|
2
|
+
from typing import Callable, TypeVar
|
|
3
|
+
|
|
4
|
+
logger = getLogger(__name__)
|
|
5
|
+
R = TypeVar("R")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def do(
|
|
9
|
+
function: Callable[[], R],
|
|
10
|
+
exception: Exception = Exception,
|
|
11
|
+
*,
|
|
12
|
+
log_level=WARNING,
|
|
13
|
+
default: R = None,
|
|
14
|
+
exc=False,
|
|
15
|
+
) -> R:
|
|
16
|
+
"""Executes a function and logs the exception if it fails
|
|
17
|
+
|
|
18
|
+
:param function: function to call
|
|
19
|
+
:type function: Callable
|
|
20
|
+
:param exception: exception to catch
|
|
21
|
+
:type exception: Exception
|
|
22
|
+
:param log_level: logging level to use
|
|
23
|
+
:type log_level: int
|
|
24
|
+
:param default: default value to return if the function fails
|
|
25
|
+
:type default: R
|
|
26
|
+
:param exc: if the exception should be raised
|
|
27
|
+
:type exc: bool
|
|
28
|
+
:return: result of the function and if it was successful
|
|
29
|
+
:rtype: R
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
return function()
|
|
34
|
+
except exception as e:
|
|
35
|
+
logger.log(log_level, e)
|
|
36
|
+
if exc:
|
|
37
|
+
if isinstance(exc, Exception):
|
|
38
|
+
raise exc from e
|
|
39
|
+
raise e
|
|
40
|
+
return default
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def execute_predicate(
|
|
44
|
+
function: Callable[[], R],
|
|
45
|
+
predicate: Callable[[], bool],
|
|
46
|
+
) -> R | bool:
|
|
47
|
+
"""Executes a function if the predicate is true"
|
|
48
|
+
|
|
49
|
+
:param function: function to call
|
|
50
|
+
:type function: Callable
|
|
51
|
+
:param predicate: predicate to check
|
|
52
|
+
:type predicate: Callable
|
|
53
|
+
:return: result of the function and if it was successful
|
|
54
|
+
:rtype: R | bool
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
return predicate() and function()
|
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
[project]
|
|
2
|
-
license = { file = "LICENSE" }
|
|
3
|
-
|
|
4
2
|
name = "oceanprotocol-job-details"
|
|
5
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.10"
|
|
4
|
+
description = "A Python package to get details from OceanProtocol jobs"
|
|
6
5
|
authors = [
|
|
7
6
|
{ name = "Christian López García", email = "christian.lopez@udl.cat" },
|
|
8
7
|
]
|
|
9
|
-
|
|
8
|
+
requires-python = ">=3.10"
|
|
10
9
|
readme = "README.md"
|
|
11
|
-
|
|
10
|
+
license = { file = "LICENSE" }
|
|
12
11
|
classifiers = [
|
|
13
12
|
"Programming Language :: Python :: 3",
|
|
14
13
|
"Operating System :: OS Independent",
|
|
15
14
|
"License :: OSI Approved :: MIT License",
|
|
16
15
|
]
|
|
16
|
+
dependencies = ["orjson>=3.10.15", "pydantic>=2.10.6", "pytest>=8.3.4,<9"]
|
|
17
17
|
|
|
18
18
|
[project.urls]
|
|
19
19
|
Homepage = "https://github.com/AgrospAI/oceanprotocol-job-details"
|
|
20
20
|
Issues = "https://github.com/AgrospAI/oceanprotocol-job-details/issues"
|
|
21
21
|
|
|
22
22
|
[build-system]
|
|
23
|
-
requires = ["
|
|
24
|
-
build-backend = "
|
|
23
|
+
requires = ["hatchling"]
|
|
24
|
+
build-backend = "hatchling.build"
|
|
25
25
|
|
|
26
|
-
[tool.
|
|
27
|
-
|
|
26
|
+
[tool.hatch.build.targets.sdist]
|
|
27
|
+
include = ["oceanprotocol_job_details"]
|
|
28
28
|
|
|
29
|
-
[tool.
|
|
30
|
-
|
|
31
|
-
pytest = "^8.3.4"
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
include = ["oceanprotocol_job_details"]
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: oceanprotocol-job-details
|
|
3
|
-
Version: 0.0.8
|
|
4
|
-
Summary: A Python package to get details from OceanProtocol jobs
|
|
5
|
-
License: Copyright 2025 Agrospai
|
|
6
|
-
|
|
7
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
8
|
-
|
|
9
|
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
10
|
-
|
|
11
|
-
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
12
|
-
Author: Christian López García
|
|
13
|
-
Author-email: christian.lopez@udl.cat
|
|
14
|
-
Requires-Python: >=3.9
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Operating System :: OS Independent
|
|
17
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
-
Requires-Dist: pytest (>=8.3.4,<9.0.0)
|
|
19
|
-
Project-URL: Homepage, https://github.com/AgrospAI/oceanprotocol-job-details
|
|
20
|
-
Project-URL: Issues, https://github.com/AgrospAI/oceanprotocol-job-details/issues
|
|
21
|
-
Description-Content-Type: text/markdown
|
|
22
|
-
|
|
23
|
-
A Python package to get details from OceanProtocol jobs
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## Installation
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
pip install oceanprotocol-job-details
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Usage
|
|
34
|
-
|
|
35
|
-
As a simple library, we only need to import the main object and use it once:
|
|
36
|
-
|
|
37
|
-
```Python
|
|
38
|
-
from oceanprotocol_job_details.job_details import OceanProtocolJobDetails
|
|
39
|
-
|
|
40
|
-
# Using default parameters
|
|
41
|
-
job_details = OceanProtocolJobDetails().load()
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### Advanced Usage (not recommended)
|
|
45
|
-
|
|
46
|
-
If instead of the environment variables, we want to use another kind of mapping, can pass it as a parameter and it will work as long as it has the same key values (Can be implemented in a more generic way, but there is no need right now).
|
|
47
|
-
|
|
48
|
-
```Python
|
|
49
|
-
from oceanprotocol_job_details.job_details import OceanProtocolJobDetails
|
|
50
|
-
from oceanprotocol_job_details.loaders.impl.environment import Keys
|
|
51
|
-
|
|
52
|
-
# Fill in with values that will be used instead of env
|
|
53
|
-
custom_mapper = {
|
|
54
|
-
Keys.ALGORITHM: " ... ",
|
|
55
|
-
Keys.DIDS: " ... ",
|
|
56
|
-
Keys.ROOT: " ... ",
|
|
57
|
-
Keys.SECRET: " ... ",
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
job_details = OceanProtocolJobDetails(mapper=custom_mapper).load()
|
|
61
|
-
```
|
|
62
|
-
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
"""Loads the current Job Details from the environment variables, could be abstracted to a more general 'mapper loader' but won't, since right now it fits our needs"""
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from json import JSONDecodeError, load, loads
|
|
5
|
-
from logging import getLogger
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Mapping, Optional, Sequence, final
|
|
8
|
-
|
|
9
|
-
from oceanprotocol_job_details.dataclasses.constants import DidKeys, Paths, ServiceType
|
|
10
|
-
from oceanprotocol_job_details.dataclasses.job_details import Algorithm, JobDetails
|
|
11
|
-
from oceanprotocol_job_details.loaders.loader import Loader
|
|
12
|
-
|
|
13
|
-
logger = getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@dataclass(frozen=True)
|
|
17
|
-
class Keys:
|
|
18
|
-
"""Environment keys passed to the algorithm"""
|
|
19
|
-
|
|
20
|
-
ROOT: str = "ROOT_FOLDER"
|
|
21
|
-
SECRET: str = "secret"
|
|
22
|
-
ALGORITHM: str = "TRANSFORMATION_DID"
|
|
23
|
-
DIDS: str = "DIDS"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@final
|
|
27
|
-
class Map(Loader[JobDetails]):
|
|
28
|
-
"""Loads the current Job Details from the environment variables"""
|
|
29
|
-
|
|
30
|
-
def __init__(self, mapper: Mapping[str, str], keys: Keys, *args, **kwargs):
|
|
31
|
-
super().__init__(*args, **kwargs)
|
|
32
|
-
|
|
33
|
-
self._mapper = mapper
|
|
34
|
-
self._keys = keys
|
|
35
|
-
|
|
36
|
-
def load(self, *args, **kwargs) -> JobDetails:
|
|
37
|
-
root, dids = self._root(), self._dids()
|
|
38
|
-
|
|
39
|
-
return JobDetails(
|
|
40
|
-
root=root,
|
|
41
|
-
dids=dids,
|
|
42
|
-
files=self._files(root, dids),
|
|
43
|
-
algorithm=self._algorithm(root),
|
|
44
|
-
secret=self._secret(),
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
def _root(self) -> Path:
|
|
48
|
-
"""
|
|
49
|
-
Retrieves the root of the data from the default keys, defaults to '/'
|
|
50
|
-
|
|
51
|
-
Raises:
|
|
52
|
-
FileNotFoundError: If the given root folder does not exist
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
Path: the root folder
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
root = Path(self._mapper.get(self._keys.ROOT, Path("/")))
|
|
59
|
-
|
|
60
|
-
if not root.exists():
|
|
61
|
-
raise FileNotFoundError(f"Root folder {root} does not exist")
|
|
62
|
-
|
|
63
|
-
return root
|
|
64
|
-
|
|
65
|
-
def _dids(self) -> Sequence[Path]:
|
|
66
|
-
return (
|
|
67
|
-
loads(self._mapper.get(self._keys.DIDS))
|
|
68
|
-
if self._keys.DIDS in self._mapper
|
|
69
|
-
else []
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
def _files(
|
|
73
|
-
self,
|
|
74
|
-
root: Path,
|
|
75
|
-
dids: Optional[Sequence[Path]],
|
|
76
|
-
) -> Mapping[str, Sequence[Path]]:
|
|
77
|
-
files: Mapping[str, Sequence[Path]] = {}
|
|
78
|
-
|
|
79
|
-
for did in dids:
|
|
80
|
-
# Retrieve DDO from disk
|
|
81
|
-
file_path = root / Paths.DDOS / did
|
|
82
|
-
if not file_path.exists():
|
|
83
|
-
raise FileNotFoundError(f"DDO file {file_path} does not exist")
|
|
84
|
-
|
|
85
|
-
with open(file_path, "r") as f:
|
|
86
|
-
try:
|
|
87
|
-
ddo = load(f)
|
|
88
|
-
except JSONDecodeError as e:
|
|
89
|
-
logger.warning(f"Error loading DDO file {file_path}: {e}")
|
|
90
|
-
continue
|
|
91
|
-
|
|
92
|
-
for service in ddo[DidKeys.SERVICE]:
|
|
93
|
-
if service[DidKeys.SERVICE_TYPE] != ServiceType.METADATA:
|
|
94
|
-
continue
|
|
95
|
-
|
|
96
|
-
did_path = root / Paths.INPUTS / did
|
|
97
|
-
files[did] = [
|
|
98
|
-
did_path / str(idx)
|
|
99
|
-
for idx in range(
|
|
100
|
-
len(
|
|
101
|
-
service[DidKeys.ATTRIBUTES][DidKeys.MAIN][DidKeys.FILES]
|
|
102
|
-
)
|
|
103
|
-
)
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
return files
|
|
107
|
-
|
|
108
|
-
def _algorithm(self, root: Path) -> Optional[Algorithm]:
|
|
109
|
-
did = self._mapper.get(self._keys.ALGORITHM, None)
|
|
110
|
-
|
|
111
|
-
if not did:
|
|
112
|
-
return None
|
|
113
|
-
|
|
114
|
-
ddo = root / Paths.DDOS / did
|
|
115
|
-
if not ddo.exists():
|
|
116
|
-
raise FileNotFoundError(f"DDO file {ddo} does not exist")
|
|
117
|
-
|
|
118
|
-
return Algorithm(did, ddo)
|
|
119
|
-
|
|
120
|
-
def _secret(self) -> Optional[str]:
|
|
121
|
-
return self._mapper.get(self._keys.SECRET, None)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|