oceanprotocol-job-details 0.1.4__py3-none-any.whl → 0.2.0__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.
@@ -0,0 +1,44 @@
1
+ from dependency_injector import containers, providers
2
+
3
+ from oceanprotocol_job_details.loaders.impl.ddo import DDOLoader
4
+ from oceanprotocol_job_details.loaders.impl.files import FilesLoader
5
+ from oceanprotocol_job_details.loaders.impl.job_details import JobDetailsLoader
6
+ from oceanprotocol_job_details.paths import Paths
7
+
8
+
9
+ class Container(containers.DeclarativeContainer):
10
+
11
+ config = providers.Configuration()
12
+
13
+ paths = providers.Singleton(Paths)
14
+
15
+ file_loader = providers.Factory(
16
+ FilesLoader,
17
+ dids=config.dids,
18
+ transformation_did=config.transformation_did,
19
+ paths=paths,
20
+ )
21
+
22
+ files = providers.Factory(
23
+ lambda loader: loader.load(),
24
+ loader=file_loader,
25
+ )
26
+
27
+ # DDOLoader depends on Files loaded from FilesLoader
28
+ ddo_loader = providers.Factory(
29
+ DDOLoader,
30
+ files=files,
31
+ )
32
+
33
+ ddos = providers.Factory(
34
+ lambda loader: loader.load(),
35
+ loader=ddo_loader,
36
+ )
37
+
38
+ job_details_loader = providers.Factory(
39
+ JobDetailsLoader,
40
+ files=files,
41
+ secret=config.secret,
42
+ paths=paths,
43
+ ddos=ddos,
44
+ )
@@ -1,24 +1,30 @@
1
+ from __future__ import annotations
2
+
1
3
  from dataclasses import InitVar, dataclass, field
2
4
  from pathlib import Path
3
- from typing import final
5
+ from typing import TYPE_CHECKING, final
4
6
 
5
- from oceanprotocol_job_details.ocean import DDO
7
+ if TYPE_CHECKING:
8
+ from oceanprotocol_job_details.ocean import DDO, Files
6
9
 
7
10
 
8
11
  @final
9
12
  @dataclass(frozen=True)
10
13
  class DDOLoader:
11
- ddo_paths: InitVar[list[Path]]
14
+
15
+ files: InitVar[list[Files]]
12
16
  """The files to load the DDOs from"""
13
17
 
14
18
  _ddo_paths: list[Path] = field(init=False)
15
19
 
16
- def __post_init__(self, ddo_paths: list[Path]) -> None:
17
- assert ddo_paths, "Missing DDO paths"
20
+ def __post_init__(self, files: list[Files]) -> None:
21
+ assert files, "Missing files"
18
22
 
19
- object.__setattr__(self, "_ddo_paths", ddo_paths)
23
+ object.__setattr__(self, "_ddo_paths", [f.ddo for f in files])
20
24
 
21
25
  def load(self) -> list[DDO]:
26
+ from oceanprotocol_job_details.ocean import DDO
27
+
22
28
  ddos = []
23
29
  for path in self._ddo_paths:
24
30
  with open(path, "r") as f:
@@ -1,53 +1,28 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
  from dataclasses import InitVar, dataclass, field
3
- from pathlib import Path
4
- from typing import Iterator, Sequence, final
5
-
6
- from oceanprotocol_job_details.config import config
7
-
8
-
9
- @dataclass(frozen=True)
10
- class DIDPaths:
11
- did: str
12
- ddo: Path
13
- input_files: Sequence[Path]
14
-
15
- def __post_init__(self) -> None:
16
- assert self.ddo.exists(), f"DDO {self.ddo} does not exist"
17
- for input_file in self.input_files:
18
- assert input_file.exists(), f"File {input_file} does not exist"
5
+ from typing import TYPE_CHECKING, Sequence, final
19
6
 
20
- def __len__(self) -> int:
21
- return len(self.input_files)
7
+ from oceanprotocol_job_details.paths import Paths
22
8
 
23
-
24
- @dataclass(frozen=True)
25
- class Files:
26
- _files: Sequence[DIDPaths]
27
-
28
- @property
29
- def files(self) -> Sequence[DIDPaths]:
30
- return self._files
31
-
32
- def __getitem__(self, index: int) -> DIDPaths:
33
- return self.files[index]
34
-
35
- def __iter__(self) -> Iterator[DIDPaths]:
36
- return iter(self.files)
37
-
38
- def __len__(self) -> int:
39
- return len(self.files)
9
+ if TYPE_CHECKING:
10
+ from oceanprotocol_job_details.ocean import DIDPaths, Files
40
11
 
41
12
 
42
13
  @final
43
14
  @dataclass(frozen=True)
44
15
  class FilesLoader:
16
+
45
17
  dids: InitVar[str | None]
46
18
  """Input DIDs"""
47
19
 
48
20
  transformation_did: InitVar[str | None]
49
21
  """DID for the transformation algorithm"""
50
22
 
23
+ paths: Paths
24
+ """Path configurations of the project"""
25
+
51
26
  _dids: Sequence[str] = field(init=False)
52
27
  _transformation_did: str = field(init=False)
53
28
 
@@ -63,13 +38,15 @@ class FilesLoader:
63
38
  object.__setattr__(self, "_transformation_did", transformation_did)
64
39
 
65
40
  def load(self) -> Files:
41
+ from oceanprotocol_job_details.ocean import DIDPaths, Files
42
+
66
43
  files: list[DIDPaths] = []
67
44
  for did in self._dids:
68
- base = Path(config.path_inputs) / did
45
+ base = self.paths.inputs / did
69
46
  files.append(
70
47
  DIDPaths(
71
48
  did=did,
72
- ddo=Path(config.path_ddos) / did,
49
+ ddo=self.paths.ddos / did,
73
50
  input_files=list(base.iterdir()),
74
51
  )
75
52
  )
@@ -1,10 +1,13 @@
1
- import os
1
+ from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass, field
3
- from typing import Generic, Type, TypeVar, final
4
+ from typing import TYPE_CHECKING, Generic, Type, TypeVar, final
5
+
6
+ from oceanprotocol_job_details.paths import Paths
7
+
8
+ if TYPE_CHECKING:
9
+ from oceanprotocol_job_details.ocean import DDO, Files, JobDetails
4
10
 
5
- from oceanprotocol_job_details.loaders.impl.ddo import DDOLoader
6
- from oceanprotocol_job_details.loaders.impl.files import FilesLoader
7
- from oceanprotocol_job_details.ocean import JobDetails
8
11
 
9
12
  T = TypeVar("T")
10
13
 
@@ -12,14 +15,21 @@ T = TypeVar("T")
12
15
  @final
13
16
  @dataclass(frozen=True)
14
17
  class JobDetailsLoader(Generic[T]):
18
+
15
19
  _type: Type[T] = field(repr=False)
16
20
 
17
- def load(self) -> JobDetails[T]:
18
- dids = os.environ.get("DIDS")
19
- transformation_did = os.environ.get("TRANSFORMATION_DID")
20
- secret = os.environ.get("SECRET")
21
+ files: Files
22
+ secret: str
23
+ paths: Paths
24
+ ddos: list[DDO]
21
25
 
22
- files = FilesLoader(dids, transformation_did).load()
23
- ddos = DDOLoader([f.ddo for f in files]).load()
26
+ def load(self) -> JobDetails[T]:
27
+ from oceanprotocol_job_details.ocean import JobDetails
24
28
 
25
- return JobDetails(files=files, secret=secret, ddos=ddos, _type=self._type)
29
+ return JobDetails(
30
+ files=self.files,
31
+ secret=self.secret,
32
+ ddos=self.ddos,
33
+ paths=self.paths,
34
+ _type=self._type,
35
+ )
@@ -1,17 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
1
5
  from dataclasses import dataclass, field
2
6
  from functools import cached_property
3
- from typing import Any, Generic, Optional, Type, TypeVar, final
7
+ from pathlib import Path
8
+ from typing import Any, Generic, Iterator, Optional, Sequence, Type, TypeVar, final
4
9
 
5
10
  import orjson
6
-
7
11
  from dataclasses_json import config as dc_config
8
12
  from dataclasses_json import dataclass_json
9
13
 
10
- from oceanprotocol_job_details.config import config
11
- from oceanprotocol_job_details.loaders.impl.files import Files
14
+ from oceanprotocol_job_details.di import Container
15
+ from oceanprotocol_job_details.paths import Paths
12
16
 
13
17
  T = TypeVar("T")
14
18
 
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format="%(asctime)s [%(threadName)s] [%(levelname)s] %(message)s",
22
+ handlers=[logging.StreamHandler()],
23
+ )
24
+ logger = logging.getLogger(__name__)
25
+
15
26
 
16
27
  @dataclass_json
17
28
  @dataclass
@@ -29,7 +40,7 @@ class Credentials:
29
40
 
30
41
  @dataclass_json
31
42
  @dataclass
32
- class Container:
43
+ class DockerContainer:
33
44
  image: str
34
45
  tag: str
35
46
  entrypoint: str
@@ -38,7 +49,7 @@ class Container:
38
49
  @dataclass_json
39
50
  @dataclass
40
51
  class Algorithm: # type: ignore
41
- container: Container
52
+ container: DockerContainer
42
53
  language: str
43
54
  version: str
44
55
  consumerParameters: Any # type: ignore
@@ -157,6 +168,39 @@ class DDO:
157
168
  purgatory: Purgatory
158
169
 
159
170
 
171
+ @dataclass(frozen=True)
172
+ class DIDPaths:
173
+ did: str
174
+ ddo: Path
175
+ input_files: Sequence[Path]
176
+
177
+ def __post_init__(self) -> None:
178
+ assert self.ddo.exists(), f"DDO {self.ddo} does not exist"
179
+ for input_file in self.input_files:
180
+ assert input_file.exists(), f"File {input_file} does not exist"
181
+
182
+ def __len__(self) -> int:
183
+ return len(self.input_files)
184
+
185
+
186
+ @dataclass(frozen=True)
187
+ class Files:
188
+ _files: Sequence[DIDPaths]
189
+
190
+ @property
191
+ def files(self) -> Sequence[DIDPaths]:
192
+ return self._files
193
+
194
+ def __getitem__(self, index: int) -> DIDPaths:
195
+ return self.files[index]
196
+
197
+ def __iter__(self) -> Iterator[DIDPaths]:
198
+ return iter(self.files)
199
+
200
+ def __len__(self) -> int:
201
+ return len(self.files)
202
+
203
+
160
204
  def _normalize_json(value):
161
205
  if isinstance(value, str):
162
206
  try:
@@ -171,6 +215,12 @@ def _normalize_json(value):
171
215
  return value
172
216
 
173
217
 
218
+ @final
219
+ @dataclass_json
220
+ @dataclass
221
+ class _EmptyJobDetails: ...
222
+
223
+
174
224
  @final
175
225
  @dataclass_json
176
226
  @dataclass(frozen=True)
@@ -181,6 +231,9 @@ class JobDetails(Generic[T]):
181
231
  ddos: list[DDO]
182
232
  """list of paths to the DDOs"""
183
233
 
234
+ paths: Paths
235
+ """Configuration paths"""
236
+
184
237
  # Store the type explicitly to avoid issues
185
238
  _type: Type[T] = field(repr=False)
186
239
 
@@ -195,11 +248,11 @@ class JobDetails(Generic[T]):
195
248
  def input_parameters(self) -> T:
196
249
  """Read the input parameters and return them in an instance of the dataclass T"""
197
250
 
198
- with open(config.path_algorithm_custom_parameters, "r") as f:
251
+ with open(self.paths.algorithm_custom_parameters, "r") as f:
199
252
  raw = f.read().strip()
200
253
  if not raw:
201
254
  raise ValueError(
202
- f"Custom parameters file {config.path_algorithm_custom_parameters} is empty"
255
+ f"Custom parameters file {self.paths.algorithm_custom_parameters} is empty"
203
256
  )
204
257
  try:
205
258
  parsed = _normalize_json(orjson.loads(raw))
@@ -209,3 +262,32 @@ class JobDetails(Generic[T]):
209
262
  f"Failed to parse input paramers into {self._type.__name__}: {e}\n"
210
263
  f"Raw content: {raw}"
211
264
  ) from e
265
+
266
+ @classmethod
267
+ def load(cls, _type: Type[T] | None = None) -> JobDetails[T]:
268
+ """Load a JobDetails instance that holds the runtime details.
269
+
270
+ Loading it will check the following:
271
+ 1. That the needed environment variables are set.
272
+ 1. That the ocean protocol contains the needed data based on the passed environment variables.
273
+
274
+ Those needed environment variables are:
275
+ - DIDS: The DIDs of the inputs
276
+ - TRANSFORMATION_DID: The DID of the transformation algorithm
277
+ - SECRET (optional): A really secret secret
278
+
279
+ """
280
+
281
+ if _type is None:
282
+ _type = _EmptyJobDetails
283
+
284
+ container = Container()
285
+ container.config.from_dict(
286
+ {
287
+ "dids": os.environ.get("DIDS"),
288
+ "transformation_did": os.environ.get("TRANSFORMATION_DID"),
289
+ "secret": os.environ.get("SECRET"),
290
+ }
291
+ )
292
+
293
+ return container.job_details_loader(_type=_type).load()
@@ -0,0 +1,28 @@
1
+ from dataclasses import dataclass
2
+ from logging import getLogger
3
+ from pathlib import Path
4
+
5
+ logger = getLogger(__name__)
6
+
7
+
8
+ @dataclass
9
+ class Paths:
10
+ """Configuration class for the Ocean Protocol Job Details"""
11
+
12
+ data: Path = Path("/data")
13
+ """The path to the data directory"""
14
+
15
+ inputs: Path = data / "inputs"
16
+ """The path to the inputs directory"""
17
+
18
+ ddos: Path = data / "ddos"
19
+ """The path to the DDOs directory"""
20
+
21
+ outputs: Path = data / "outputs"
22
+ """The path to the outputs directory"""
23
+
24
+ logs: Path = data / "logs"
25
+ """The path to the logs directory"""
26
+
27
+ algorithm_custom_parameters: Path = inputs / "algoCustomData.json"
28
+ """The path to the algorithm's custom parameters file"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oceanprotocol-job-details
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: A Python package to get details from OceanProtocol jobs
5
5
  Project-URL: Homepage, https://github.com/AgrospAI/oceanprotocol-job-details
6
6
  Project-URL: Issues, https://github.com/AgrospAI/oceanprotocol-job-details/issues
@@ -18,6 +18,8 @@ Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
19
  Requires-Python: >=3.10
20
20
  Requires-Dist: dataclasses-json>=0.6.7
21
+ Requires-Dist: dependency-injector>=4.48.2
22
+ Requires-Dist: orjson>=3.11.3
21
23
  Description-Content-Type: text/markdown
22
24
 
23
25
  A Python package to get details from OceanProtocol jobs
@@ -0,0 +1,14 @@
1
+ oceanprotocol_job_details/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ oceanprotocol_job_details/di.py,sha256=mLhxm-fiqC4TuPUIEG5eqsgmtmY3N64g6Ep8NgQR6pA,1131
3
+ oceanprotocol_job_details/ocean.py,sha256=l-MunbeDBZhPATLZL5nKyo2i7RAznTqMRjY-wJEkxVw,6686
4
+ oceanprotocol_job_details/paths.py,sha256=gzWj8HPKjJAQ6FUjOobWWzrW1Kzpxq2bXxQ9icDAy24,723
5
+ oceanprotocol_job_details/loaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ oceanprotocol_job_details/loaders/loader.py,sha256=HIzsVKCuGP7ghfM7ppN3ANVybvsA64wr3h8I68mqS6A,195
7
+ oceanprotocol_job_details/loaders/impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ oceanprotocol_job_details/loaders/impl/ddo.py,sha256=_xl0PozuvIm0n6jU--Znk6qfw1A6OLBq3SERbDGIF74,844
9
+ oceanprotocol_job_details/loaders/impl/files.py,sha256=oFkA_0Ma5NBgWvVEk_rhyDIDrAam_zjesh0-bxZaIU8,1443
10
+ oceanprotocol_job_details/loaders/impl/job_details.py,sha256=wf0xNAG4tESq57vqkdtMQ8BdiyS91j5f7FL8Gfwbjh4,770
11
+ oceanprotocol_job_details-0.2.0.dist-info/METADATA,sha256=1oFkqVCvdw0vue35sCM2EmkfbLFcwTIwptJLHxFo4DY,3214
12
+ oceanprotocol_job_details-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ oceanprotocol_job_details-0.2.0.dist-info/licenses/LICENSE,sha256=ni3ix7P_GxK1W3VGC4fJ3o6QoCngCEpSuTJwO4nkpbw,1055
14
+ oceanprotocol_job_details-0.2.0.dist-info/RECORD,,
@@ -1,54 +0,0 @@
1
- from dataclasses import dataclass, fields
2
- from logging import getLogger
3
- from pathlib import Path
4
-
5
- logger = getLogger(__name__)
6
-
7
-
8
- @dataclass
9
- class Config:
10
- """Configuration class for the Ocean Protocol Job Details"""
11
-
12
- path_data: Path = Path("/data")
13
- """The path to the data directory"""
14
-
15
- path_inputs: Path = path_data / "inputs"
16
- """The path to the inputs directory"""
17
-
18
- path_ddos: Path = path_data / "ddos"
19
- """The path to the DDOs directory"""
20
-
21
- path_outputs: Path = path_data / "outputs"
22
- """The path to the outputs directory"""
23
-
24
- path_logs: Path = path_data / "logs"
25
- """The path to the logs directory"""
26
-
27
- path_algorithm_custom_parameters: Path = path_inputs / "algoCustomData.json"
28
- """The path to the algorithm's custom parameters file"""
29
-
30
-
31
- config = Config()
32
-
33
-
34
- def update_config_from(base: Path) -> None:
35
- """Updates the configuration to use the new base path, ensures that the base path exists.
36
-
37
- Args:
38
- base (Path): The new base path to use.
39
- """
40
-
41
- logger.info(f"Updating config to use base path: {base}")
42
-
43
- base.mkdir(parents=True, exist_ok=True)
44
-
45
- for field in fields(config):
46
- current_value = getattr(config, field.name)
47
- if not isinstance(current_value, Path):
48
- raise ValueError(f"Field {field.name} is n|ot a Path")
49
-
50
- rel_path = Path(current_value).relative_to("/data")
51
- object.__setattr__(config, field.name, base / rel_path)
52
-
53
-
54
- __all__ = ["config"]
@@ -1,47 +0,0 @@
1
- import logging
2
- from dataclasses import dataclass
3
- from typing import Generic, Type, TypeVar
4
-
5
- from dataclasses_json import dataclass_json
6
-
7
- from oceanprotocol_job_details.loaders.impl.job_details import JobDetailsLoader
8
- from oceanprotocol_job_details.loaders.loader import Loader
9
- from oceanprotocol_job_details.ocean import JobDetails
10
-
11
- logging.basicConfig(
12
- level=logging.INFO,
13
- format="%(asctime)s [%(threadName)s] [%(levelname)s] %(message)s",
14
- handlers=[logging.StreamHandler()],
15
- )
16
- logger = logging.getLogger(__name__)
17
-
18
-
19
- @dataclass_json
20
- @dataclass
21
- class _EmptyJobDetails: ...
22
-
23
-
24
- T = TypeVar("T")
25
-
26
-
27
- class OceanProtocolJobDetails(Generic[T]):
28
- """The JobDetails class is a dataclass that holds the details of the current job.
29
-
30
- Loading it will check the following:
31
- 1. That the needed environment variables are set
32
- 1. That the ocean protocol contains the needed data based on the passed environment variables
33
-
34
- Those needed environment variables are:
35
- - DIDS: The DIDs of the inputs
36
- - TRANSFORMATION_DID: The DID of the transformation algorithm
37
- - SECRET (optional): A really secret secret
38
-
39
- """
40
-
41
- def __init__(self, _type: Type[T] | None = None) -> None:
42
- if _type is None:
43
- _type = _EmptyJobDetails # type: ignore[assignment]
44
- self.job_details_loader: Loader[JobDetails[T]] = JobDetailsLoader(_type) # type: ignore[arg-type]
45
-
46
- def load(self) -> JobDetails[T]:
47
- return self.job_details_loader.load()
@@ -1,33 +0,0 @@
1
- from logging import getLogger
2
- from typing import Mapping, Optional, TypeVar
3
-
4
- T = TypeVar("T")
5
- logger = getLogger(__name__)
6
-
7
-
8
- def get(
9
- map: Mapping[str, T],
10
- key: str,
11
- default: Optional[T] = None,
12
- ) -> T | None:
13
- """Get the value of a key from a dictionary, if not found return the default value if given, otherwise raise a KeyError
14
-
15
- :param map: original map to get the item from
16
- :type map: Mapping[str, T]
17
- :param key: key to get the value from
18
- :type key: str
19
- :param default: default value if missing, defaults to None
20
- :type default: Optional[T], optional
21
- :raises KeyError: if the value is missing and no default is provided
22
- :return: value of the key
23
- :rtype: T
24
- """
25
-
26
- if key in map.keys():
27
- return map.get(key)
28
-
29
- if default is None:
30
- raise KeyError(f"Key {key} not found")
31
-
32
- logger.info(f"Key {key} not found, returning default value {default}")
33
- return default
@@ -1,15 +0,0 @@
1
- oceanprotocol_job_details/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- oceanprotocol_job_details/config.py,sha256=e-V1ybwr-LzuceyhZalmPOTzpgTbuUxlwjbLu7EJMvc,1469
3
- oceanprotocol_job_details/job_details.py,sha256=98Uvx3jes1f75onYxT-65EOOOwcxdw32slAa3n1MqUc,1477
4
- oceanprotocol_job_details/ocean.py,sha256=N27O-qzE6NMYaTIe2O_ZNKdQGuFKtfPELy2-_IdEZls,4505
5
- oceanprotocol_job_details/utils.py,sha256=btgys1g4AKSADsde_JRofPVmI0VbR_jf85DIYhuMhgs,940
6
- oceanprotocol_job_details/loaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- oceanprotocol_job_details/loaders/loader.py,sha256=HIzsVKCuGP7ghfM7ppN3ANVybvsA64wr3h8I68mqS6A,195
8
- oceanprotocol_job_details/loaders/impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- oceanprotocol_job_details/loaders/impl/ddo.py,sha256=LcKoDuGnZw8JW68Q5P9gWGYeZqszC97HxnkzOuorHBA,707
10
- oceanprotocol_job_details/loaders/impl/files.py,sha256=hft3Y61D6eHpa9ZQ5i2C5tnCv9IZaTudOZaOIQUbkxo,2002
11
- oceanprotocol_job_details/loaders/impl/job_details.py,sha256=ERKQm1oH3jW8ebb8l4glL-Wm-9rV8mBfFalVvo0UomU,802
12
- oceanprotocol_job_details-0.1.4.dist-info/METADATA,sha256=mfNcO-GwsN59tmuWhGBUfc43bghmY46GqSp1Kk1QyY0,3141
13
- oceanprotocol_job_details-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- oceanprotocol_job_details-0.1.4.dist-info/licenses/LICENSE,sha256=ni3ix7P_GxK1W3VGC4fJ3o6QoCngCEpSuTJwO4nkpbw,1055
15
- oceanprotocol_job_details-0.1.4.dist-info/RECORD,,