esd-services-api-client 2.1.3__tar.gz → 2.2.1__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 (54) hide show
  1. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/PKG-INFO +1 -1
  2. esd_services_api_client-2.2.1/esd_services_api_client/_version.py +1 -0
  3. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/README.md +62 -37
  4. esd_services_api_client-2.2.1/esd_services_api_client/nexus/abstractions/algrorithm_cache.py +100 -0
  5. esd_services_api_client-2.2.1/esd_services_api_client/nexus/abstractions/input_object.py +63 -0
  6. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/abstractions/nexus_object.py +18 -10
  7. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/algorithms/_baseline_algorithm.py +14 -6
  8. esd_services_api_client-2.2.1/esd_services_api_client/nexus/algorithms/_remote_algorithm.py +118 -0
  9. esd_services_api_client-2.2.1/esd_services_api_client/nexus/algorithms/forked_algorithm.py +124 -0
  10. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/algorithms/minimalistic.py +8 -1
  11. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/algorithms/recursive.py +5 -1
  12. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/core/app_core.py +9 -0
  13. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/core/app_dependencies.py +19 -0
  14. esd_services_api_client-2.2.1/esd_services_api_client/nexus/exceptions/cache_errors.py +49 -0
  15. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/exceptions/startup_error.py +15 -0
  16. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/input/__init__.py +0 -1
  17. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/input/input_processor.py +11 -58
  18. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/input/input_reader.py +9 -5
  19. esd_services_api_client-2.2.1/esd_services_api_client/nexus/telemetry/__init__.py +0 -0
  20. esd_services_api_client-2.2.1/esd_services_api_client/nexus/telemetry/recorder.py +97 -0
  21. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/pyproject.toml +1 -1
  22. esd_services_api_client-2.1.3/esd_services_api_client/_version.py +0 -1
  23. esd_services_api_client-2.1.3/esd_services_api_client/nexus/input/_functions.py +0 -89
  24. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/LICENSE +0 -0
  25. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/README.md +0 -0
  26. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/__init__.py +0 -0
  27. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/beast/__init__.py +0 -0
  28. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/beast/v3/__init__.py +0 -0
  29. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/beast/v3/_connector.py +0 -0
  30. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/beast/v3/_models.py +0 -0
  31. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/boxer/README.md +0 -0
  32. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/boxer/__init__.py +0 -0
  33. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/boxer/_auth.py +0 -0
  34. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/boxer/_base.py +0 -0
  35. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/boxer/_connector.py +0 -0
  36. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/boxer/_models.py +0 -0
  37. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/common/__init__.py +0 -0
  38. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/crystal/__init__.py +0 -0
  39. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/crystal/_api_versions.py +0 -0
  40. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/crystal/_connector.py +0 -0
  41. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/crystal/_models.py +0 -0
  42. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/__init__.py +0 -0
  43. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/abstractions/__init__.py +0 -0
  44. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/abstractions/logger_factory.py +0 -0
  45. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/abstractions/socket_provider.py +0 -0
  46. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/algorithms/__init__.py +0 -0
  47. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/algorithms/distributed.py +0 -0
  48. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/configurations/__init__.py +0 -0
  49. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/configurations/algorithm_configuration.py +0 -0
  50. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/core/__init__.py +0 -0
  51. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/exceptions/__init__.py +0 -0
  52. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/exceptions/_nexus_error.py +0 -0
  53. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/exceptions/input_reader_error.py +0 -0
  54. {esd_services_api_client-2.1.3 → esd_services_api_client-2.2.1}/esd_services_api_client/nexus/input/payload_reader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esd-services-api-client
3
- Version: 2.1.3
3
+ Version: 2.2.1
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
@@ -0,0 +1 @@
1
+ __version__ = '2.2.1'
@@ -28,7 +28,9 @@ from adapta.storage.query_enabled_store import QueryEnabledStore
28
28
  from dataclasses_json import DataClassJsonMixin
29
29
  from injector import inject
30
30
 
31
+ from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
31
32
  from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
33
+ from esd_services_api_client.nexus.abstractions.nexus_object import AlgorithmResult
32
34
  from esd_services_api_client.nexus.abstractions.socket_provider import (
33
35
  ExternalSocketProvider,
34
36
  )
@@ -36,7 +38,6 @@ from esd_services_api_client.nexus.configurations.algorithm_configuration import
36
38
  NexusConfiguration,
37
39
  )
38
40
  from esd_services_api_client.nexus.core.app_core import Nexus
39
- from esd_services_api_client.nexus.abstractions.nexus_object import AlgorithmResult
40
41
  from esd_services_api_client.nexus.algorithms import MinimalisticAlgorithm
41
42
  from esd_services_api_client.nexus.input import InputReader, InputProcessor
42
43
 
@@ -127,12 +128,6 @@ class MockRequestHandler(BaseHTTPRequestHandler):
127
128
 
128
129
 
129
130
  class XReader(InputReader[MyAlgorithmPayload, pandas.DataFrame]):
130
- async def _context_open(self):
131
- pass
132
-
133
- async def _context_close(self):
134
- pass
135
-
136
131
  @inject
137
132
  def __init__(
138
133
  self,
@@ -141,7 +136,8 @@ class XReader(InputReader[MyAlgorithmPayload, pandas.DataFrame]):
141
136
  logger_factory: LoggerFactory,
142
137
  payload: MyAlgorithmPayload,
143
138
  socket_provider: ExternalSocketProvider,
144
- *readers: "InputReader"
139
+ *readers: "InputReader",
140
+ cache: InputCache
145
141
  ):
146
142
  super().__init__(
147
143
  socket=socket_provider.socket("x"),
@@ -149,10 +145,11 @@ class XReader(InputReader[MyAlgorithmPayload, pandas.DataFrame]):
149
145
  metrics_provider=metrics_provider,
150
146
  logger_factory=logger_factory,
151
147
  payload=payload,
148
+ cache=cache,
152
149
  *readers
153
150
  )
154
151
 
155
- async def _read_input(self) -> pandas.DataFrame:
152
+ async def _read_input(self, **_) -> pandas.DataFrame:
156
153
  self._logger.info(
157
154
  "Payload: {payload}; Socket path: {socket_path}",
158
155
  payload=self._payload.to_json(),
@@ -162,12 +159,6 @@ class XReader(InputReader[MyAlgorithmPayload, pandas.DataFrame]):
162
159
 
163
160
 
164
161
  class YReader(InputReader[MyAlgorithmPayload2, pandas.DataFrame]):
165
- async def _context_open(self):
166
- pass
167
-
168
- async def _context_close(self):
169
- pass
170
-
171
162
  @inject
172
163
  def __init__(
173
164
  self,
@@ -176,7 +167,8 @@ class YReader(InputReader[MyAlgorithmPayload2, pandas.DataFrame]):
176
167
  logger_factory: LoggerFactory,
177
168
  payload: MyAlgorithmPayload2,
178
169
  socket_provider: ExternalSocketProvider,
179
- *readers: "InputReader"
170
+ *readers: "InputReader",
171
+ cache: InputCache
180
172
  ):
181
173
  super().__init__(
182
174
  socket=socket_provider.socket("y"),
@@ -184,10 +176,11 @@ class YReader(InputReader[MyAlgorithmPayload2, pandas.DataFrame]):
184
176
  metrics_provider=metrics_provider,
185
177
  logger_factory=logger_factory,
186
178
  payload=payload,
179
+ cache=cache,
187
180
  *readers
188
181
  )
189
182
 
190
- async def _read_input(self) -> pandas.DataFrame:
183
+ async def _read_input(self, **_) -> pandas.DataFrame:
191
184
  self._logger.info(
192
185
  "Payload: {payload}; Socket path: {socket_path}",
193
186
  payload=self._payload.to_json(),
@@ -196,39 +189,59 @@ class YReader(InputReader[MyAlgorithmPayload2, pandas.DataFrame]):
196
189
  return pandas.DataFrame([{"a": 10, "b": 12}, {"a": 11, "b": 13}])
197
190
 
198
191
 
199
- class MyInputProcessor(InputProcessor):
200
- async def _context_open(self):
201
- pass
192
+ class XProcessor(InputProcessor[MyAlgorithmPayload, pandas.DataFrame]):
193
+ @inject
194
+ def __init__(
195
+ self,
196
+ x: XReader,
197
+ metrics_provider: MetricsProvider,
198
+ logger_factory: LoggerFactory,
199
+ my_conf: MyAlgorithmConfiguration,
200
+ cache: InputCache,
201
+ ):
202
+ super().__init__(
203
+ x,
204
+ metrics_provider=metrics_provider,
205
+ logger_factory=logger_factory,
206
+ payload=None,
207
+ cache=cache,
208
+ )
209
+
210
+ self.conf = my_conf
211
+
212
+ async def _process_input(
213
+ self, x: pandas.DataFrame, **_
214
+ ) -> pandas.DataFrame:
215
+ self._logger.info("Config: {config}", config=self.conf.to_json())
216
+ return x.assign(c=[-1, 1])
202
217
 
203
- async def _context_close(self):
204
- pass
205
218
 
219
+ class YProcessor(InputProcessor[MyAlgorithmPayload, pandas.DataFrame]):
206
220
  @inject
207
221
  def __init__(
208
222
  self,
209
- x: XReader,
210
223
  y: YReader,
211
224
  metrics_provider: MetricsProvider,
212
225
  logger_factory: LoggerFactory,
213
226
  my_conf: MyAlgorithmConfiguration,
227
+ cache: InputCache,
214
228
  ):
215
229
  super().__init__(
216
- x,
217
230
  y,
218
231
  metrics_provider=metrics_provider,
219
232
  logger_factory=logger_factory,
220
233
  payload=None,
234
+ cache=cache,
221
235
  )
222
236
 
223
237
  self.conf = my_conf
224
238
 
225
- async def process_input(self, **_) -> Dict[str, pandas.DataFrame]:
239
+ async def _process_input(
240
+ self, y: pandas.DataFrame, **_
241
+ ) -> pandas.DataFrame:
226
242
  self._logger.info("Config: {config}", config=self.conf.to_json())
227
- inputs = await self._read_input()
228
- return {
229
- "x_ready": inputs["x"].assign(c=[-1, 1]),
230
- "y_ready": inputs["y"].assign(c=[-1, 1]),
231
- }
243
+ return y.assign(c=[-1, 1])
244
+
232
245
 
233
246
  @dataclass
234
247
  class MyResult(AlgorithmResult):
@@ -240,8 +253,8 @@ class MyResult(AlgorithmResult):
240
253
 
241
254
  def to_kwargs(self) -> dict[str, Any]:
242
255
  pass
243
-
244
-
256
+
257
+
245
258
  class MyAlgorithm(MinimalisticAlgorithm[MyAlgorithmPayload]):
246
259
  async def _context_open(self):
247
260
  pass
@@ -250,11 +263,22 @@ class MyAlgorithm(MinimalisticAlgorithm[MyAlgorithmPayload]):
250
263
  pass
251
264
 
252
265
  @inject
253
- def __init__(self, metrics_provider: MetricsProvider, logger_factory: LoggerFactory, input_processor: MyInputProcessor):
254
- super().__init__(metrics_provider, logger_factory, input_processor)
266
+ def __init__(
267
+ self,
268
+ metrics_provider: MetricsProvider,
269
+ logger_factory: LoggerFactory,
270
+ x_processor: XProcessor,
271
+ y_processor: YProcessor,
272
+ cache: InputCache,
273
+ ):
274
+ super().__init__(
275
+ metrics_provider, logger_factory, x_processor, y_processor, cache=cache
276
+ )
255
277
 
256
- async def _run(self, x_ready: pandas.DataFrame, y_ready: pandas.DataFrame, **kwargs) -> MyResult:
257
- return MyResult(x_ready, y_ready)
278
+ async def _run(
279
+ self, x: pandas.DataFrame, y: pandas.DataFrame, **kwargs
280
+ ) -> MyResult:
281
+ return MyResult(x, y)
258
282
 
259
283
 
260
284
  async def main():
@@ -270,7 +294,8 @@ async def main():
270
294
  await Nexus.create()
271
295
  .add_reader(XReader)
272
296
  .add_reader(YReader)
273
- .use_processor(MyInputProcessor)
297
+ .use_processor(XProcessor)
298
+ .use_processor(YProcessor)
274
299
  .use_algorithm(MyAlgorithm)
275
300
  .inject_configuration(MyAlgorithmConfiguration)
276
301
  .inject_payload(MyAlgorithmPayload, MyAlgorithmPayload2)
@@ -0,0 +1,100 @@
1
+ """
2
+ Simple in-memory cache for readers and processors
3
+ """
4
+
5
+ # Copyright (c) 2023-2024. ECCO Sneaks & Data
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ import asyncio
21
+ from typing import final, Type
22
+
23
+ import azure.core.exceptions
24
+ import deltalake
25
+
26
+ from esd_services_api_client.nexus.abstractions.input_object import InputObject
27
+ from esd_services_api_client.nexus.abstractions.nexus_object import TResult, TPayload
28
+ from esd_services_api_client.nexus.exceptions.cache_errors import (
29
+ FatalCachingError,
30
+ TransientCachingError,
31
+ )
32
+
33
+
34
+ @final
35
+ class InputCache:
36
+ """
37
+ In-memory cache for Nexus input readers/processors
38
+ """
39
+
40
+ def __init__(self):
41
+ self._cache: dict[str, TResult] = {}
42
+
43
+ def _resolve_exc_type(
44
+ self, ex: BaseException
45
+ ) -> Type[FatalCachingError] | Type[TransientCachingError]:
46
+ """
47
+ Resolve base exception into a specific Nexus exception.
48
+ """
49
+ match type(ex):
50
+ case azure.core.exceptions.HttpResponseError, deltalake.PyDeltaTableError:
51
+ return TransientCachingError
52
+ case azure.core.exceptions.AzureError, azure.core.exceptions.ClientAuthenticationError:
53
+ return FatalCachingError
54
+ case _:
55
+ return FatalCachingError
56
+
57
+ async def resolve(
58
+ self,
59
+ *readers_or_processors: InputObject[TPayload, TResult],
60
+ **kwargs,
61
+ ) -> dict[str, TResult]:
62
+ """
63
+ Concurrently resolve `data` property of all readers by invoking their `read` method.
64
+ """
65
+
66
+ def get_result(alias: str, completed_task: asyncio.Task) -> TResult:
67
+ object_exc = completed_task.exception()
68
+ if object_exc:
69
+ raise self._resolve_exc_type(object_exc)(alias) from object_exc
70
+
71
+ return completed_task.result()
72
+
73
+ async def _execute(nexus_input: InputObject) -> TResult:
74
+ async with nexus_input as instance:
75
+ result = await nexus_input.process(**kwargs)
76
+
77
+ self._cache[instance.cache_key()] = result
78
+
79
+ return result
80
+
81
+ cached = {
82
+ reader_or_processor.__class__.alias(): reader_or_processor.data
83
+ for reader_or_processor in readers_or_processors
84
+ if reader_or_processor.cache_key() in self._cache
85
+ }
86
+ if len(cached) == len(readers_or_processors):
87
+ return cached
88
+
89
+ read_tasks: dict[str, asyncio.Task] = {
90
+ reader.__class__.alias(): asyncio.create_task(_execute(reader))
91
+ for reader in readers_or_processors
92
+ if reader.cache_key() not in self._cache
93
+ }
94
+
95
+ if len(read_tasks) > 0:
96
+ await asyncio.wait(fs=read_tasks.values())
97
+
98
+ return {
99
+ alias: get_result(alias, task) for alias, task in read_tasks.items()
100
+ } | cached
@@ -0,0 +1,63 @@
1
+ """
2
+ Base class for input reading/processing.
3
+ """
4
+
5
+ # Copyright (c) 2023-2024. ECCO Sneaks & Data
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ import base64
21
+ import os
22
+ from abc import ABC, abstractmethod
23
+
24
+ from esd_services_api_client.nexus.abstractions.nexus_object import (
25
+ TPayload,
26
+ TResult,
27
+ NexusObject,
28
+ )
29
+
30
+
31
+ class InputObject(NexusObject[TPayload, TResult], ABC):
32
+ """
33
+ Base class for input processing and reader objects.
34
+ """
35
+
36
+ async def _context_open(self):
37
+ """
38
+ Optional actions to perform on context activation.
39
+ """
40
+
41
+ async def _context_close(self):
42
+ """
43
+ Optional actions to perform on context closure.
44
+ """
45
+
46
+ def cache_key(self) -> str:
47
+ """
48
+ Unique identifier for this Nexus object, can be used to in-memory or external caching.
49
+ """
50
+ return f"{base64.b64encode(hex(id(self)).encode('utf-8')).decode('utf-8')}_{os.getpid()}_{self.__class__.__name__}"
51
+
52
+ @property
53
+ def data(self) -> TResult | None:
54
+ """
55
+ Data bound to this object.
56
+ """
57
+ return None
58
+
59
+ @abstractmethod
60
+ async def process(self, **kwargs) -> TResult:
61
+ """
62
+ Executes input processing logic (read or transform)
63
+ """
@@ -1,8 +1,6 @@
1
1
  """
2
2
  Base classes for all objects used by Nexus.
3
3
  """
4
- import re
5
-
6
4
  # Copyright (c) 2023-2024. ECCO Sneaks & Data
7
5
  #
8
6
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,11 +18,13 @@ import re
20
18
 
21
19
 
22
20
  from abc import ABC, abstractmethod
21
+ import re
23
22
  from typing import Generic, TypeVar, Union, Any
24
23
 
25
24
  import pandas
26
25
  import polars
27
26
  from adapta.metrics import MetricsProvider
27
+ from dataclasses_json.stringcase import snakecase
28
28
 
29
29
  from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
30
30
 
@@ -53,7 +53,7 @@ TResult = TypeVar( # pylint: disable=C0103
53
53
  )
54
54
 
55
55
 
56
- class NexusObject(Generic[TPayload, TResult], ABC):
56
+ class NexusCoreObject(ABC):
57
57
  """
58
58
  Base class for all Nexus objects.
59
59
  """
@@ -87,16 +87,24 @@ class NexusObject(Generic[TPayload, TResult], ABC):
87
87
  Optional actions to perform on context closure.
88
88
  """
89
89
 
90
+
91
+ class NexusObject(Generic[TPayload, TResult], NexusCoreObject, ABC):
92
+ """
93
+ Base class for all Nexus objects.
94
+ """
95
+
90
96
  @classmethod
91
97
  def alias(cls) -> str:
92
98
  """
93
99
  Alias to identify this reader's output
94
100
  """
95
- return re.sub(
96
- r"(?<!^)(?=[A-Z])",
97
- "_",
98
- cls.__name__.lower()
99
- .replace("reader", "")
100
- .replace("processor", "")
101
- .replace("algorithm", ""),
101
+ return snakecase(
102
+ re.sub(
103
+ r"(?<!^)(?=[A-Z])",
104
+ "_",
105
+ cls.__name__.lower()
106
+ .replace("reader", "")
107
+ .replace("processor", "")
108
+ .replace("algorithm", ""),
109
+ )
102
110
  )
@@ -1,7 +1,6 @@
1
1
  """
2
2
  Base algorithm
3
3
  """
4
-
5
4
  # Copyright (c) 2023-2024. ECCO Sneaks & Data
6
5
  #
7
6
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,13 +16,13 @@
17
16
  # limitations under the License.
18
17
  #
19
18
 
20
-
21
19
  from abc import abstractmethod
22
- from functools import reduce, partial
20
+ from functools import partial
23
21
 
24
22
  from adapta.metrics import MetricsProvider
25
23
  from adapta.utils.decorators import run_time_metrics_async
26
24
 
25
+ from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
27
26
  from esd_services_api_client.nexus.abstractions.nexus_object import (
28
27
  NexusObject,
29
28
  TPayload,
@@ -32,7 +31,6 @@ from esd_services_api_client.nexus.abstractions.nexus_object import (
32
31
  from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
33
32
  from esd_services_api_client.nexus.input.input_processor import (
34
33
  InputProcessor,
35
- resolve_processors,
36
34
  )
37
35
 
38
36
 
@@ -46,9 +44,19 @@ class BaselineAlgorithm(NexusObject[TPayload, AlgorithmResult]):
46
44
  metrics_provider: MetricsProvider,
47
45
  logger_factory: LoggerFactory,
48
46
  *input_processors: InputProcessor,
47
+ cache: InputCache,
49
48
  ):
50
49
  super().__init__(metrics_provider, logger_factory)
51
50
  self._input_processors = input_processors
51
+ self._cache = cache
52
+ self._inputs: dict = {}
53
+
54
+ @property
55
+ def inputs(self) -> dict:
56
+ """
57
+ Inputs generated for this algorithm run.
58
+ """
59
+ return self._inputs
52
60
 
53
61
  @abstractmethod
54
62
  async def _run(self, **kwargs) -> AlgorithmResult:
@@ -75,11 +83,11 @@ class BaselineAlgorithm(NexusObject[TPayload, AlgorithmResult]):
75
83
  async def _measured_run(**run_args):
76
84
  return await self._run(**run_args)
77
85
 
78
- results = await resolve_processors(*self._input_processors, **kwargs)
86
+ self._inputs = await self._cache.resolve(*self._input_processors, **kwargs)
79
87
 
80
88
  return await partial(
81
89
  _measured_run,
82
- **reduce(lambda a, b: a | b, [result for _, result in results.items()]),
90
+ **self._inputs,
83
91
  metric_tags=self._metric_tags,
84
92
  metrics_provider=self._metrics_provider,
85
93
  logger=self._logger,
@@ -0,0 +1,118 @@
1
+ """
2
+ Remotely executed algorithm
3
+ """
4
+
5
+ # Copyright (c) 2023-2024. ECCO Sneaks & Data
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+
21
+ from abc import abstractmethod
22
+ from functools import partial
23
+
24
+ from adapta.metrics import MetricsProvider
25
+ from adapta.utils.decorators import run_time_metrics_async
26
+
27
+ from esd_services_api_client.crystal import CrystalConnector, AlgorithmConfiguration
28
+ from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
29
+ from esd_services_api_client.nexus.abstractions.nexus_object import (
30
+ NexusObject,
31
+ TPayload,
32
+ AlgorithmResult,
33
+ )
34
+ from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
35
+ from esd_services_api_client.nexus.input.input_processor import (
36
+ InputProcessor,
37
+ )
38
+ from esd_services_api_client.nexus.input.payload_reader import AlgorithmPayload
39
+
40
+
41
+ class RemoteAlgorithm(NexusObject[TPayload, AlgorithmResult]):
42
+ """
43
+ Base class for all algorithm implementations.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ metrics_provider: MetricsProvider,
49
+ logger_factory: LoggerFactory,
50
+ remote_client: CrystalConnector,
51
+ remote_name: str,
52
+ remote_config: AlgorithmConfiguration,
53
+ *input_processors: InputProcessor,
54
+ cache: InputCache,
55
+ ):
56
+ super().__init__(metrics_provider, logger_factory)
57
+ self._input_processors = input_processors
58
+ self._remote_client = remote_client
59
+ self._remote_name = remote_name
60
+ self._remote_config = remote_config
61
+ self._cache = cache
62
+
63
+ @abstractmethod
64
+ def _generate_tag(self) -> str:
65
+ """
66
+ Generates a submission tag.
67
+ """
68
+
69
+ @abstractmethod
70
+ def _transform_submission_result(
71
+ self, request_id: str, tag: str
72
+ ) -> AlgorithmResult:
73
+ """
74
+ Called after submitting a remote run. Use this to enrich your output with remote run id and tag.
75
+ """
76
+
77
+ @abstractmethod
78
+ async def _run(self, **kwargs) -> AlgorithmPayload:
79
+ """
80
+ Core logic for this algorithm. Implementing this method is mandatory.
81
+ """
82
+
83
+ @property
84
+ def _metric_tags(self) -> dict[str, str]:
85
+ return {"algorithm": self.__class__.alias()}
86
+
87
+ async def run(self, **kwargs) -> AlgorithmResult:
88
+ """
89
+ Coroutine that executes the algorithm logic.
90
+ """
91
+
92
+ @run_time_metrics_async(
93
+ metric_name="algorthm_run",
94
+ on_finish_message_template="Launched a new remote {algorithm} in {elapsed:.2f}s seconds",
95
+ template_args={
96
+ "algorithm": self.__class__.alias().upper(),
97
+ },
98
+ )
99
+ async def _measured_run(**run_args) -> AlgorithmResult:
100
+ payload = await self._run(**run_args)
101
+ tag = self._generate_tag()
102
+ request_id = self._remote_client.create_run(
103
+ algorithm=self._remote_name,
104
+ payload=payload.to_dict(),
105
+ custom_config=self._remote_config,
106
+ tag=tag,
107
+ )
108
+ return self._transform_submission_result(request_id, tag)
109
+
110
+ results = await self._cache.resolve(*self._input_processors, **kwargs)
111
+
112
+ return await partial(
113
+ _measured_run,
114
+ **results,
115
+ metric_tags=self._metric_tags,
116
+ metrics_provider=self._metrics_provider,
117
+ logger=self._logger,
118
+ )()