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.
Files changed (18) hide show
  1. oceanprotocol_job_details-0.0.10/.gitignore +6 -0
  2. oceanprotocol_job_details-0.0.10/PKG-INFO +83 -0
  3. {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/README.md +21 -1
  4. {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/dataclasses/constants.py +2 -2
  5. {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/dataclasses/job_details.py +3 -9
  6. oceanprotocol_job_details-0.0.10/oceanprotocol_job_details/dataclasses/ocean.py +67 -0
  7. {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/job_details.py +27 -9
  8. oceanprotocol_job_details-0.0.10/oceanprotocol_job_details/loaders/impl/map.py +120 -0
  9. oceanprotocol_job_details-0.0.10/oceanprotocol_job_details/loaders/impl/utils.py +57 -0
  10. {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/pyproject.toml +11 -12
  11. oceanprotocol_job_details-0.0.8/PKG-INFO +0 -62
  12. oceanprotocol_job_details-0.0.8/oceanprotocol_job_details/loaders/impl/map.py +0 -121
  13. {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/LICENSE +0 -0
  14. {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/__init__.py +0 -0
  15. {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/dataclasses/__init__.py +0 -0
  16. {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/loaders/__init__.py +0 -0
  17. {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/loaders/impl/__init__.py +0 -0
  18. {oceanprotocol_job_details-0.0.8 → oceanprotocol_job_details-0.0.10}/oceanprotocol_job_details/loaders/loader.py +0 -0
@@ -0,0 +1,6 @@
1
+ .env
2
+ dist
3
+
4
+ *egg-info
5
+ __pycache__
6
+ .pytest_cache
@@ -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(frozen=True)
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(self.root / Paths.LOGS, exist_ok=True)
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 = self.root / Paths.ALGORITHM_CUSTOM_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["env"]
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
- if implementation == "map":
33
- # As there are not more implementations, we can use the EnvironmentLoader directly
34
- self._loader = lambda: Map(mapper=mapper, keys=keys, *args, **kwargs)
35
- else:
36
- raise NotImplementedError(f"Implementation {implementation} not supported")
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.8"
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
- description = "A Python package to get details from OceanProtocol jobs"
8
+ requires-python = ">=3.10"
10
9
  readme = "README.md"
11
- requires-python = ">=3.9"
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 = ["poetry-core>=2.0.0,<3.0.0"]
24
- build-backend = "poetry.core.masonry.api"
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
25
 
26
- [tool.poetry]
27
- packages = [{ include = "oceanprotocol_job_details" }]
26
+ [tool.hatch.build.targets.sdist]
27
+ include = ["oceanprotocol_job_details"]
28
28
 
29
- [tool.poetry.dependencies]
30
- python = ">3.9"
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)