esd-services-api-client 2.1.1__py3-none-any.whl → 2.1.2__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.
@@ -1 +1 @@
1
- __version__ = '2.1.1'
1
+ __version__ = '2.1.2'
@@ -95,5 +95,8 @@ class NexusObject(Generic[TPayload, TResult], ABC):
95
95
  return re.sub(
96
96
  r"(?<!^)(?=[A-Z])",
97
97
  "_",
98
- cls.__name__.lower().replace("reader", "").replace("processor", ""),
98
+ cls.__name__.lower()
99
+ .replace("reader", "")
100
+ .replace("processor", "")
101
+ .replace("algorithm", ""),
99
102
  )
@@ -1,7 +1,6 @@
1
1
  """
2
2
  Socket provider for all data sockets used by algorithms.
3
3
  """
4
- import json
5
4
 
6
5
  # Copyright (c) 2023-2024. ECCO Sneaks & Data
7
6
  #
@@ -18,10 +17,15 @@ import json
18
17
  # limitations under the License.
19
18
  #
20
19
 
21
- from typing import final, Optional
20
+ import json
21
+ from typing import final
22
22
 
23
23
  from adapta.process_communication import DataSocket
24
24
 
25
+ from esd_services_api_client.nexus.exceptions.startup_error import (
26
+ FatalStartupConfigurationError,
27
+ )
28
+
25
29
 
26
30
  @final
27
31
  class ExternalSocketProvider:
@@ -32,11 +36,16 @@ class ExternalSocketProvider:
32
36
  def __init__(self, *sockets: DataSocket):
33
37
  self._sockets = {socket.alias: socket for socket in sockets}
34
38
 
35
- def socket(self, name: str) -> Optional[DataSocket]:
39
+ def socket(self, name: str) -> DataSocket:
36
40
  """
37
41
  Retrieve a socket if it exists.
38
42
  """
39
- return self._sockets.get(name, None)
43
+ if name in self._sockets:
44
+ return self._sockets[name]
45
+
46
+ raise FatalStartupConfigurationError(
47
+ missing_entry=f"socket with alias `{name}`"
48
+ )
40
49
 
41
50
  @classmethod
42
51
  def from_serialized(cls, socket_list_ser: str) -> "ExternalSocketProvider":
@@ -1,7 +1,6 @@
1
1
  """
2
2
  Base algorithm
3
3
  """
4
- import asyncio
5
4
 
6
5
  # Copyright (c) 2023-2024. ECCO Sneaks & Data
7
6
  #
@@ -20,18 +19,21 @@ import asyncio
20
19
 
21
20
 
22
21
  from abc import abstractmethod
23
- from functools import reduce
22
+ from functools import reduce, partial
24
23
 
25
24
  from adapta.metrics import MetricsProvider
25
+ from adapta.utils.decorators import run_time_metrics_async
26
26
 
27
27
  from esd_services_api_client.nexus.abstractions.nexus_object import (
28
28
  NexusObject,
29
29
  TPayload,
30
- TResult,
31
30
  AlgorithmResult,
32
31
  )
33
32
  from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
34
- from esd_services_api_client.nexus.input.input_processor import InputProcessor
33
+ from esd_services_api_client.nexus.input.input_processor import (
34
+ InputProcessor,
35
+ resolve_processors,
36
+ )
35
37
 
36
38
 
37
39
  class BaselineAlgorithm(NexusObject[TPayload, AlgorithmResult]):
@@ -54,24 +56,31 @@ class BaselineAlgorithm(NexusObject[TPayload, AlgorithmResult]):
54
56
  Core logic for this algorithm. Implementing this method is mandatory.
55
57
  """
56
58
 
59
+ @property
60
+ def _metric_tags(self) -> dict[str, str]:
61
+ return {"algorithm": self.__class__.alias()}
62
+
57
63
  async def run(self, **kwargs) -> AlgorithmResult:
58
64
  """
59
65
  Coroutine that executes the algorithm logic.
60
66
  """
61
67
 
62
- async def _process(
63
- processor: InputProcessor[TPayload, TResult]
64
- ) -> dict[str, TResult]:
65
- async with processor as instance:
66
- return await instance.process_input(**kwargs)
68
+ @run_time_metrics_async(
69
+ metric_name="algorthm_run",
70
+ on_finish_message_template="Finished running {algorithm} in {elapsed:.2f}s seconds",
71
+ template_args={
72
+ "algorithm": self.__class__.alias().upper(),
73
+ },
74
+ )
75
+ async def _measured_run(**run_args):
76
+ return await self._run(**run_args)
67
77
 
68
- process_tasks: dict[str, asyncio.Task] = {
69
- input_processor.__class__.__name__.lower(): asyncio.create_task(
70
- _process(input_processor)
71
- )
72
- for input_processor in self._input_processors
73
- }
74
- await asyncio.wait(fs=process_tasks.values())
75
- results = [task.result() for task in process_tasks.values()]
78
+ results = await resolve_processors(*self._input_processors, **kwargs)
76
79
 
77
- return await self._run(**reduce(lambda a, b: a | b, results))
80
+ return await partial(
81
+ _measured_run,
82
+ **reduce(lambda a, b: a | b, [result for _, result in results.items()]),
83
+ metric_tags=self._metric_tags,
84
+ metrics_provider=self._metrics_provider,
85
+ logger=self._logger,
86
+ )()
@@ -1,5 +1,5 @@
1
1
  """
2
- Custom exceptions.
2
+ App startup exceptions.
3
3
  """
4
4
 
5
5
  # Copyright (c) 2023-2024. ECCO Sneaks & Data
@@ -30,6 +30,9 @@ from esd_services_api_client.nexus.exceptions.input_reader_error import (
30
30
  from esd_services_api_client.nexus.input.input_reader import InputReader
31
31
 
32
32
 
33
+ _reader_cache = {}
34
+
35
+
33
36
  def resolve_reader_exc_type(
34
37
  ex: BaseException,
35
38
  ) -> Union[Type[FatalInputReaderError], Type[TransientInputReaderError]]:
@@ -61,12 +64,26 @@ async def resolve_readers(
61
64
 
62
65
  async def _read(input_reader: InputReader):
63
66
  async with input_reader as instance:
64
- return await instance.read()
67
+ result = await instance.read()
68
+ _reader_cache[input_reader.__class__.alias()] = result
69
+ return result
70
+
71
+ cached = {
72
+ reader.__class__.alias(): reader.data
73
+ for reader in readers
74
+ if reader.__class__.alias() in _reader_cache
75
+ }
76
+ if len(cached) == len(readers):
77
+ return cached
65
78
 
66
79
  read_tasks: dict[str, asyncio.Task] = {
67
80
  reader.__class__.alias(): asyncio.create_task(_read(reader))
68
81
  for reader in readers
82
+ if reader.__class__.alias() not in _reader_cache
69
83
  }
70
- await asyncio.wait(fs=read_tasks.values())
84
+ if len(read_tasks) > 0:
85
+ await asyncio.wait(fs=read_tasks.values())
71
86
 
72
- return {alias: get_result(alias, task) for alias, task in read_tasks.items()}
87
+ return {
88
+ alias: get_result(alias, task) for alias, task in read_tasks.items()
89
+ } | cached
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Input processing.
3
3
  """
4
+ import asyncio
4
5
 
5
6
  # Copyright (c) 2023-2024. ECCO Sneaks & Data
6
7
  #
@@ -18,8 +19,11 @@
18
19
  #
19
20
 
20
21
  from abc import abstractmethod
22
+ from functools import partial
23
+ from typing import Optional
21
24
 
22
25
  from adapta.metrics import MetricsProvider
26
+ from adapta.utils.decorators import run_time_metrics_async
23
27
 
24
28
  from esd_services_api_client.nexus.abstractions.nexus_object import (
25
29
  NexusObject,
@@ -27,9 +31,14 @@ from esd_services_api_client.nexus.abstractions.nexus_object import (
27
31
  TResult,
28
32
  )
29
33
  from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
30
- from esd_services_api_client.nexus.input._functions import resolve_readers
34
+ from esd_services_api_client.nexus.input._functions import (
35
+ resolve_readers,
36
+ resolve_reader_exc_type,
37
+ )
31
38
  from esd_services_api_client.nexus.input.input_reader import InputReader
32
39
 
40
+ _processor_cache = {}
41
+
33
42
 
34
43
  class InputProcessor(NexusObject[TPayload, TResult]):
35
44
  """
@@ -46,12 +55,90 @@ class InputProcessor(NexusObject[TPayload, TResult]):
46
55
  super().__init__(metrics_provider, logger_factory)
47
56
  self._readers = readers
48
57
  self._payload = payload
58
+ self._result: Optional[TResult] = None
49
59
 
50
60
  async def _read_input(self) -> dict[str, TResult]:
51
61
  return await resolve_readers(*self._readers)
52
62
 
63
+ @property
64
+ def result(self) -> dict[str, TResult]:
65
+ """
66
+ Data returned by this processor
67
+ """
68
+ return self._result
69
+
53
70
  @abstractmethod
54
- async def process_input(self, **kwargs) -> dict[str, TResult]:
71
+ async def _process_input(self, **kwargs) -> dict[str, TResult]:
55
72
  """
56
73
  Input processing logic. Implement this method to prepare data for your algorithm code.
57
74
  """
75
+
76
+ @property
77
+ def _metric_tags(self) -> dict[str, str]:
78
+ return {"processor": self.__class__.alias()}
79
+
80
+ async def process_input(self, **kwargs) -> dict[str, TResult]:
81
+ """
82
+ Input processing coroutine. Do not override this method.
83
+ """
84
+
85
+ @run_time_metrics_async(
86
+ metric_name="input_process",
87
+ on_finish_message_template="Finished processing {processor} in {elapsed:.2f}s seconds",
88
+ template_args={
89
+ "processor": self.__class__.alias().upper(),
90
+ },
91
+ )
92
+ async def _process(**_) -> dict[str, TResult]:
93
+ return await self._process_input(**kwargs)
94
+
95
+ if self._result is None:
96
+ self._result = await partial(
97
+ _process,
98
+ metric_tags=self._metric_tags,
99
+ metrics_provider=self._metrics_provider,
100
+ logger=self._logger,
101
+ )()
102
+
103
+ return self._result
104
+
105
+
106
+ async def resolve_processors(
107
+ *processors: InputProcessor[TPayload, TResult], **kwargs
108
+ ) -> dict[str, dict[str, TResult]]:
109
+ """
110
+ Concurrently resolve `result` property of all processors by invoking their `process_input` method.
111
+ """
112
+
113
+ def get_result(alias: str, completed_task: asyncio.Task) -> dict[str, TResult]:
114
+ reader_exc = completed_task.exception()
115
+ if reader_exc:
116
+ raise resolve_reader_exc_type(reader_exc)(alias, reader_exc) from reader_exc
117
+
118
+ return completed_task.result()
119
+
120
+ async def _process(input_processor: InputProcessor):
121
+ async with input_processor as instance:
122
+ result = await instance.process_input(**kwargs)
123
+ _processor_cache[input_processor.__class__.alias()] = result
124
+ return result
125
+
126
+ cached = {
127
+ processor.__class__.alias(): processor.result
128
+ for processor in processors
129
+ if processor.__class__.alias() in _processor_cache
130
+ }
131
+ if len(cached) == len(processors):
132
+ return cached
133
+
134
+ process_tasks: dict[str, asyncio.Task] = {
135
+ processor.__class__.alias(): asyncio.create_task(_process(processor))
136
+ for processor in processors
137
+ if processor.__class__.alias() not in _processor_cache
138
+ }
139
+ if len(process_tasks) > 0:
140
+ await asyncio.wait(fs=process_tasks.values())
141
+
142
+ return {
143
+ alias: get_result(alias, task) for alias, task in process_tasks.items()
144
+ } | cached
@@ -55,6 +55,13 @@ class InputReader(NexusObject[TPayload, TResult]):
55
55
  self._readers = readers
56
56
  self._payload = payload
57
57
 
58
+ @property
59
+ def data(self) -> Optional[TResult]:
60
+ """
61
+ Data returned by this reader
62
+ """
63
+ return self._data
64
+
58
65
  @abstractmethod
59
66
  async def _read_input(self) -> TResult:
60
67
  """
@@ -67,11 +74,11 @@ class InputReader(NexusObject[TPayload, TResult]):
67
74
 
68
75
  async def read(self) -> TResult:
69
76
  """
70
- Coroutine that reads the data from external store and converts it to a dataframe.
77
+ Coroutine that reads the data from external store and converts it to a dataframe, or generates data locally. Do not override this method.
71
78
  """
72
79
 
73
80
  @run_time_metrics_async(
74
- metric_name="read_input",
81
+ metric_name="input_read",
75
82
  on_finish_message_template="Finished reading {entity} from path {data_path} in {elapsed:.2f}s seconds"
76
83
  if self.socket
77
84
  else "Finished reading {entity} in {elapsed:.2f}s seconds",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esd-services-api-client
3
- Version: 2.1.1
3
+ Version: 2.1.2
4
4
  Summary: Python clients for ESD services
5
5
  Home-page: https://github.com/SneaksAndData/esd-services-api-client
6
6
  License: Apache 2.0
@@ -1,5 +1,5 @@
1
1
  esd_services_api_client/__init__.py,sha256=L-cEW1mVbnTJLCLG5V6Ucw7zBgx1zf0t1bYcQC1heyw,603
2
- esd_services_api_client/_version.py,sha256=Aht2295j8FswZ-nPYofCYr3fBZ6Uyf0thTfl5Oc2mWA,22
2
+ esd_services_api_client/_version.py,sha256=m5qImnzcnIhayvILFVqEnXPYsN-vE0vxokygykKhRfw,22
3
3
  esd_services_api_client/beast/__init__.py,sha256=zNhXcHSP5w4P9quM1XP4oXVJEccvC_VScG41TZ0GzZ8,723
4
4
  esd_services_api_client/beast/v3/__init__.py,sha256=FtumtInoDyCCRE424Llqv8QZLRuwXzj-smyfu1od1nc,754
5
5
  esd_services_api_client/beast/v3/_connector.py,sha256=WNmCiTXFRb3q56mrr7ZbqBHWDUxbfyWhiWlBFLUIOnc,11478
@@ -19,10 +19,10 @@ esd_services_api_client/nexus/README.md,sha256=Q3laWkuCxqVBFsfP58mGAys-jVZeesroI
19
19
  esd_services_api_client/nexus/__init__.py,sha256=sOgKKq3_LZGbLmQMtMS7lDw2hv027qownTmNIRV0BB8,627
20
20
  esd_services_api_client/nexus/abstractions/__init__.py,sha256=sOgKKq3_LZGbLmQMtMS7lDw2hv027qownTmNIRV0BB8,627
21
21
  esd_services_api_client/nexus/abstractions/logger_factory.py,sha256=9biONvCqNrP__yrmeRkoDL05TMA5v-LyrcKwgiKG59U,2019
22
- esd_services_api_client/nexus/abstractions/nexus_object.py,sha256=h6mxolrfBF1BEz4FH2HRqiTcLU8oAteIVUgYAsoxI6U,2764
23
- esd_services_api_client/nexus/abstractions/socket_provider.py,sha256=pF7vWZNNE26B9ftNbAUeSEvYIE2JBIrvpzAhznr1_q8,1500
22
+ esd_services_api_client/nexus/abstractions/nexus_object.py,sha256=Be90h8iVDp2o1recnx8-Ousm3OjaVjIjthPqE5u5vik,2828
23
+ esd_services_api_client/nexus/abstractions/socket_provider.py,sha256=Rwa_aPErI4Es5AdyCd3EoGze7mg2D70u8kuc2UGEBaI,1729
24
24
  esd_services_api_client/nexus/algorithms/__init__.py,sha256=yMvLFSqg5eUKOXI0zMFX69Ni0ibKQHOqAnrZsxQqhOo,903
25
- esd_services_api_client/nexus/algorithms/_baseline_algorithm.py,sha256=dvoIcDj6c1AZW5_THrTS2qKWylRfTM89laIypijzUhU,2441
25
+ esd_services_api_client/nexus/algorithms/_baseline_algorithm.py,sha256=UxqGFzpl-8w4SLJX0GMxLzRinRnFVoBT2zBKzHOapBM,2694
26
26
  esd_services_api_client/nexus/algorithms/distributed.py,sha256=vkKSCsd480RKwrtu3uZ2iU1bh593fkgBcOBrcb9cLjA,1702
27
27
  esd_services_api_client/nexus/algorithms/minimalistic.py,sha256=PjLs_xhpd3rTViaLbWUEvJ1LWDPTKEZFj57iPk8wswo,1462
28
28
  esd_services_api_client/nexus/algorithms/recursive.py,sha256=MAyfj3gQZ0zQB5hU3RHIOfH889v_Wtp4GHBhB-trr2w,1862
@@ -34,13 +34,13 @@ esd_services_api_client/nexus/core/app_dependencies.py,sha256=GChNBRE-9CGqZ41xQO
34
34
  esd_services_api_client/nexus/exceptions/__init__.py,sha256=feN33VdqB5-2bD9aJesJl_OlsKrNNo3hZCnQgKuaU9k,696
35
35
  esd_services_api_client/nexus/exceptions/_nexus_error.py,sha256=QvtY38mNoIA6t26dUN6UIsaPfljhtVNsbQVS7ksMb-Q,895
36
36
  esd_services_api_client/nexus/exceptions/input_reader_error.py,sha256=Chy8XW6Ien4-bkZZ1CmP8CWU49mi2hobS6L_R59ONs8,1765
37
- esd_services_api_client/nexus/exceptions/startup_error.py,sha256=TJRRwDff3wQP5RkZPEfgqguuiC9q1PAqoHX6UiJslxQ,1551
37
+ esd_services_api_client/nexus/exceptions/startup_error.py,sha256=HPTlrg2voRbGXZsakWcVT7lcGSaccSR-GsBIo08SJ4A,1556
38
38
  esd_services_api_client/nexus/input/__init__.py,sha256=ahJ7yUuRTCNIYB5GitBRGAPZZX82ghISzF6lm_CkyEA,819
39
- esd_services_api_client/nexus/input/_functions.py,sha256=HeTu3JcUPudfWrfZRmNAaejVb6oPb8gZufEEq56eT-4,2485
40
- esd_services_api_client/nexus/input/input_processor.py,sha256=kNTmjZkUMbrb-8mpVyhIUbqKKwZaO0fOGoG5EQOLL_Q,1808
41
- esd_services_api_client/nexus/input/input_reader.py,sha256=tC7r5dXnovMtSOFos3Bv0tYTNyFOVNCvHrdAQXcVLTM,3015
39
+ esd_services_api_client/nexus/input/_functions.py,sha256=9bySbwcXGV7JIsQbSqg3YuW7avJUErsvsS2ZDHRWfT0,2918
40
+ esd_services_api_client/nexus/input/input_processor.py,sha256=ZUdpz2M6PfsF-99-DpCMDtD9Id0SQYhC-HbjosMz8O8,4609
41
+ esd_services_api_client/nexus/input/input_reader.py,sha256=MtZNdxjWLjoSqlSlwa6f7BIqsu-_GoQav5tCz2g6WrA,3214
42
42
  esd_services_api_client/nexus/input/payload_reader.py,sha256=Kq0xN1Shyqv71v6YkcrqVTDbmsEjZc8ithsXYpyu87M,2516
43
- esd_services_api_client-2.1.1.dist-info/LICENSE,sha256=0gS6zXsPp8qZhzi1xaGCIYPzb_0e8on7HCeFJe8fOpw,10693
44
- esd_services_api_client-2.1.1.dist-info/METADATA,sha256=oS20SFp1_sFJLcsyW5bqrcRdPbARdNlpfzA2JC13ja0,1292
45
- esd_services_api_client-2.1.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
46
- esd_services_api_client-2.1.1.dist-info/RECORD,,
43
+ esd_services_api_client-2.1.2.dist-info/LICENSE,sha256=0gS6zXsPp8qZhzi1xaGCIYPzb_0e8on7HCeFJe8fOpw,10693
44
+ esd_services_api_client-2.1.2.dist-info/METADATA,sha256=xr1Dw96DI1t-qKxCb1aq48Dyxt-co-BzwRePv-Jgoz0,1292
45
+ esd_services_api_client-2.1.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
46
+ esd_services_api_client-2.1.2.dist-info/RECORD,,