esd-services-api-client 2.2.0__py3-none-any.whl → 2.2.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.2.0'
1
+ __version__ = '2.2.2'
@@ -37,6 +37,7 @@ from esd_services_api_client.crystal._models import (
37
37
  AlgorithmRequest,
38
38
  AlgorithmConfiguration,
39
39
  RequestLifeCycleStage,
40
+ ParentRequest,
40
41
  )
41
42
 
42
43
  T = TypeVar("T") # pylint: disable=C0103
@@ -154,6 +155,7 @@ class CrystalConnector:
154
155
  algorithm: str,
155
156
  payload: Dict,
156
157
  custom_config: Optional[AlgorithmConfiguration] = None,
158
+ parent_request: Optional[ParentRequest] = None,
157
159
  tag: Optional[str] = None,
158
160
  ) -> str:
159
161
  """
@@ -162,6 +164,7 @@ class CrystalConnector:
162
164
  :param algorithm: Name of a connected algorithm.
163
165
  :param payload: Algorithm payload.
164
166
  :param custom_config: Customized config for this run.
167
+ :param parent_request: Parent request for this run.
165
168
  :param tag: Client-side submission identifier.
166
169
  :return: Request identifier assigned to the job by Crystal.
167
170
  """
@@ -176,6 +179,7 @@ class CrystalConnector:
176
179
  algorithm_name=algorithm,
177
180
  algorithm_parameters=payload,
178
181
  custom_configuration=custom_config,
182
+ parent_request=parent_request,
179
183
  tag=tag,
180
184
  ).to_dict()
181
185
 
@@ -133,6 +133,17 @@ class AlgorithmConfiguration(DataClassJsonMixin):
133
133
  speculative_attempts: Optional[int] = None
134
134
 
135
135
 
136
+ @dataclass_json(letter_case=LetterCase.CAMEL)
137
+ @dataclass
138
+ class ParentRequest(DataClassJsonMixin):
139
+ """
140
+ Used to specify crystal parent job for a new crystal job.
141
+ """
142
+
143
+ request_id: Optional[str] = None
144
+ algorithm_name: Optional[str] = None
145
+
146
+
136
147
  @dataclass_json(letter_case=LetterCase.CAMEL)
137
148
  @dataclass
138
149
  class AlgorithmRequest(DataClassJsonMixin):
@@ -143,4 +154,5 @@ class AlgorithmRequest(DataClassJsonMixin):
143
154
  algorithm_parameters: Dict
144
155
  algorithm_name: Optional[str] = None
145
156
  custom_configuration: Optional[AlgorithmConfiguration] = None
157
+ parent_request: Optional[ParentRequest] = None
146
158
  tag: Optional[str] = None
@@ -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,6 +87,12 @@ 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
  """
@@ -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,7 +16,6 @@
17
16
  # limitations under the License.
18
17
  #
19
18
 
20
-
21
19
  from abc import abstractmethod
22
20
  from functools import partial
23
21
 
@@ -51,6 +49,14 @@ class BaselineAlgorithm(NexusObject[TPayload, AlgorithmResult]):
51
49
  super().__init__(metrics_provider, logger_factory)
52
50
  self._input_processors = input_processors
53
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
54
60
 
55
61
  @abstractmethod
56
62
  async def _run(self, **kwargs) -> AlgorithmResult:
@@ -77,11 +83,11 @@ class BaselineAlgorithm(NexusObject[TPayload, AlgorithmResult]):
77
83
  async def _measured_run(**run_args):
78
84
  return await self._run(**run_args)
79
85
 
80
- results = await self._cache.resolve(*self._input_processors, **kwargs)
86
+ self._inputs = await self._cache.resolve(*self._input_processors, **kwargs)
81
87
 
82
88
  return await partial(
83
89
  _measured_run,
84
- **results,
90
+ **self._inputs,
85
91
  metric_tags=self._metric_tags,
86
92
  metrics_provider=self._metrics_provider,
87
93
  logger=self._logger,
@@ -45,5 +45,8 @@ class MinimalisticAlgorithm(BaselineAlgorithm[TPayload], ABC):
45
45
  cache: InputCache,
46
46
  ):
47
47
  super().__init__(
48
- metrics_provider, logger_factory, *input_processors, cache=cache
48
+ metrics_provider,
49
+ logger_factory,
50
+ *input_processors,
51
+ cache=cache,
49
52
  )
@@ -58,6 +58,7 @@ from esd_services_api_client.nexus.input.payload_reader import (
58
58
  AlgorithmPayloadReader,
59
59
  AlgorithmPayload,
60
60
  )
61
+ from esd_services_api_client.nexus.telemetry.recorder import TelemetryRecorder
61
62
 
62
63
 
63
64
  def is_transient_exception(exception: Optional[BaseException]) -> Optional[bool]:
@@ -244,9 +245,11 @@ class Nexus:
244
245
  """
245
246
  Activates the run sequence.
246
247
  """
248
+
247
249
  self._injector = Injector(self._configurator.injection_binds)
248
250
 
249
251
  algorithm: BaselineAlgorithm = self._injector.get(self._algorithm_class)
252
+ telemetry_recorder: TelemetryRecorder = self._injector.get(TelemetryRecorder)
250
253
 
251
254
  async with algorithm as instance:
252
255
  self._algorithm_run_task = asyncio.create_task(
@@ -266,6 +269,12 @@ class Nexus:
266
269
  if len(on_complete_tasks) > 0:
267
270
  await asyncio.wait(on_complete_tasks)
268
271
 
272
+ # record telemetry
273
+ async with telemetry_recorder as recorder:
274
+ await recorder.record(
275
+ run_id=self._run_args.request_id, **algorithm.inputs
276
+ )
277
+
269
278
  # dispose of QES instance gracefully as it might hold open connections
270
279
  qes = self._injector.get(QueryEnabledStore)
271
280
  qes.close()
@@ -44,6 +44,7 @@ from esd_services_api_client.nexus.input.input_reader import InputReader
44
44
  from esd_services_api_client.nexus.input.payload_reader import (
45
45
  AlgorithmPayload,
46
46
  )
47
+ from esd_services_api_client.nexus.telemetry.recorder import TelemetryRecorder
47
48
 
48
49
 
49
50
  @final
@@ -195,6 +196,7 @@ class ServiceConfigurator:
195
196
  StorageClientModule(),
196
197
  ExternalSocketsModule(),
197
198
  CacheModule(),
199
+ type(f"{TelemetryRecorder.__name__}Module", (Module,), {})(),
198
200
  ]
199
201
 
200
202
  @property
File without changes
@@ -0,0 +1,105 @@
1
+ """
2
+ Telemetry recording module.
3
+ """
4
+ import asyncio
5
+ import os
6
+ from functools import partial
7
+ from typing import final
8
+
9
+ import pandas as pd
10
+ from adapta.metrics import MetricsProvider
11
+ from adapta.process_communication import DataSocket
12
+ from adapta.storage.blob.base import StorageClient
13
+ from adapta.storage.models.format import (
14
+ DictJsonSerializationFormat,
15
+ DataFrameParquetSerializationFormat,
16
+ )
17
+ from injector import inject, singleton
18
+
19
+ from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
20
+ from esd_services_api_client.nexus.abstractions.nexus_object import NexusCoreObject
21
+
22
+
23
+ @final
24
+ @singleton
25
+ class TelemetryRecorder(NexusCoreObject):
26
+ """
27
+ Class for instantiating a telemetry recorder that will save all algorithm inputs (run method arguments) to a persistent location.
28
+ """
29
+
30
+ async def _context_open(self):
31
+ pass
32
+
33
+ async def _context_close(self):
34
+ pass
35
+
36
+ @inject
37
+ def __init__(
38
+ self,
39
+ storage_client: StorageClient,
40
+ metrics_provider: MetricsProvider,
41
+ logger_factory: LoggerFactory,
42
+ ):
43
+ super().__init__(metrics_provider, logger_factory)
44
+ self._storage_client = storage_client
45
+ self._telemetry_base_path = os.getenv("NEXUS__TELEMETRY_PATH")
46
+
47
+ async def record(self, run_id: str, **telemetry_args):
48
+ """
49
+ Record all data in telemetry args for the provided run_id.
50
+ """
51
+
52
+ async def _record(
53
+ entity_to_record: pd.DataFrame | dict,
54
+ entity_name: str,
55
+ **_,
56
+ ) -> None:
57
+ self._logger.debug(
58
+ "Recording telemetry for {entity_name} in the run {run_id}",
59
+ entity_name=entity_name,
60
+ run_id=run_id,
61
+ )
62
+ if not isinstance(entity_to_record, dict) and not isinstance(
63
+ entity_to_record, pd.DataFrame
64
+ ):
65
+ self._logger.warning(
66
+ "Unsupported data type: {telemetry_entity_type}. Telemetry recording skipped.",
67
+ telemetry_entity_type=type(entity_to_record),
68
+ )
69
+ else:
70
+ self._storage_client.save_data_as_blob(
71
+ data=entity_to_record,
72
+ blob_path=DataSocket(
73
+ alias="telemetry",
74
+ data_path=f"{self._telemetry_base_path}/{entity_name}/{run_id}",
75
+ data_format="null",
76
+ ).parse_data_path(),
77
+ serialization_format=DictJsonSerializationFormat
78
+ if isinstance(entity_to_record, dict)
79
+ else DataFrameParquetSerializationFormat,
80
+ overwrite=True,
81
+ )
82
+
83
+ telemetry_tasks = [
84
+ asyncio.create_task(
85
+ partial(
86
+ _record,
87
+ entity_to_record=telemetry_value,
88
+ entity_name=telemetry_key,
89
+ run_id=run_id,
90
+ )()
91
+ )
92
+ for telemetry_key, telemetry_value in telemetry_args.items()
93
+ ]
94
+
95
+ done, pending = await asyncio.wait(telemetry_tasks)
96
+ if len(pending) > 0:
97
+ self._logger.warning(
98
+ "Some telemetry recording operations did not complete within specified time. This run might lack observability coverage."
99
+ )
100
+ for done_telemetry_task in done:
101
+ telemetry_exc = done_telemetry_task.exception()
102
+ if telemetry_exc:
103
+ self._logger.warning(
104
+ "Telemetry recoding failed", exception=telemetry_exc
105
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esd-services-api-client
3
- Version: 2.2.0
3
+ Version: 2.2.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=Vyf6P6UCZKFeQtRzYujPmFfdlqSfnc01VEMWE3O0ZrA,22
2
+ esd_services_api_client/_version.py,sha256=jsJ9CNIuUt8dDFB4i0PiBf07nzBU0RtG1CVRQ7TdoQ0,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
@@ -13,28 +13,28 @@ esd_services_api_client/boxer/_models.py,sha256=ursQIR_c9jcVfRKc0LH1OuVL2KFD6kqo
13
13
  esd_services_api_client/common/__init__.py,sha256=L-cEW1mVbnTJLCLG5V6Ucw7zBgx1zf0t1bYcQC1heyw,603
14
14
  esd_services_api_client/crystal/__init__.py,sha256=oeyJjdQ9EpTnIq6XnjPq5v0DWPdHqi4uEfRIcD1mNZA,792
15
15
  esd_services_api_client/crystal/_api_versions.py,sha256=GHbmV_5lP9fP72TZE0j_ZeQSeJjMRcRaBRxNJbz-MWQ,837
16
- esd_services_api_client/crystal/_connector.py,sha256=wT8SahCkkRWPoHcUSLz0I-sjeK_9OSYtt07zJKie0CU,12875
17
- esd_services_api_client/crystal/_models.py,sha256=OCaidMqipl-TA8VLs7v12BppyBR3QpRA5jU2KjZxU_Q,4030
16
+ esd_services_api_client/crystal/_connector.py,sha256=U2cmgmOhXtKWlDljg8G20W4pTBOjgfb-F4gH7AgXBYg,13053
17
+ esd_services_api_client/crystal/_models.py,sha256=4ZizNNfMij31eV87cOog724gJWGWgfQAiKJ1nHKlNw0,4338
18
18
  esd_services_api_client/nexus/README.md,sha256=QQgvkhRwZtktxlzTmHkbp8KNnUvDvFGEVqvSM1QgpU8,9393
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/algrorithm_cache.py,sha256=3Umb9bKsl8Yo5a3FMrdO_7JTk2mrYJf9MLR-_C0yzFo,3338
22
22
  esd_services_api_client/nexus/abstractions/input_object.py,sha256=RUKnhekuZwd_RVvnLGAxHa4wYDFJf6wEwWQI9f-o0lM,1761
23
23
  esd_services_api_client/nexus/abstractions/logger_factory.py,sha256=9biONvCqNrP__yrmeRkoDL05TMA5v-LyrcKwgiKG59U,2019
24
- esd_services_api_client/nexus/abstractions/nexus_object.py,sha256=P5lQ5jhIk4nTLESseBy-G5HPILpBd75PWykD12jn6eQ,2938
24
+ esd_services_api_client/nexus/abstractions/nexus_object.py,sha256=TFRsiqfrIRGdoZsUDrMIWSHL9LsNdrcCXO-dEliRaAA,3039
25
25
  esd_services_api_client/nexus/abstractions/socket_provider.py,sha256=Rwa_aPErI4Es5AdyCd3EoGze7mg2D70u8kuc2UGEBaI,1729
26
26
  esd_services_api_client/nexus/algorithms/__init__.py,sha256=yMvLFSqg5eUKOXI0zMFX69Ni0ibKQHOqAnrZsxQqhOo,903
27
- esd_services_api_client/nexus/algorithms/_baseline_algorithm.py,sha256=hGj_qNNNtz8DRhbyUDOG7ouuv8HmqAZsM-ccJKFXTH4,2739
27
+ esd_services_api_client/nexus/algorithms/_baseline_algorithm.py,sha256=24ALLx4Bxlgi0EwZB1a0SJeEwBUWKj7CGad-CpIygU0,2925
28
28
  esd_services_api_client/nexus/algorithms/_remote_algorithm.py,sha256=nQDQ2si-_-B2QdtBC8IwSM8YyNwfIhrCMto6g87BcnQ,3900
29
29
  esd_services_api_client/nexus/algorithms/distributed.py,sha256=vkKSCsd480RKwrtu3uZ2iU1bh593fkgBcOBrcb9cLjA,1702
30
30
  esd_services_api_client/nexus/algorithms/forked_algorithm.py,sha256=Y1BFCbEMmLFmQlvq0Ot_8RAlvSbFqvZFWa_RLIYvb2Y,4310
31
- esd_services_api_client/nexus/algorithms/minimalistic.py,sha256=te8h2SQiAB8xn9OsGciZl51b_oROOodgHIX6408Lz2s,1607
31
+ esd_services_api_client/nexus/algorithms/minimalistic.py,sha256=tSYXodIW-_Aje-_ZyYUoWAThcZIeE4_kMvMINsT4Lb8,1644
32
32
  esd_services_api_client/nexus/algorithms/recursive.py,sha256=uaCCl4q-st_KqbcmkdOJedJ0nAjbJvn6jdZEdW0_0ss,2007
33
33
  esd_services_api_client/nexus/configurations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  esd_services_api_client/nexus/configurations/algorithm_configuration.py,sha256=eE7diX2PATCGkmqhvFOcZwXrr6vns4fqnJGmgNvhhZM,1091
35
35
  esd_services_api_client/nexus/core/__init__.py,sha256=sOgKKq3_LZGbLmQMtMS7lDw2hv027qownTmNIRV0BB8,627
36
- esd_services_api_client/nexus/core/app_core.py,sha256=gs1oIwc9KEog46vOZp6g_JVzTa8LLfcCpuauTLGhYVM,9654
37
- esd_services_api_client/nexus/core/app_dependencies.py,sha256=BVihH0gqfAGY851hzv4GLajC8eJx3zn8uYnOzW6-2_8,6521
36
+ esd_services_api_client/nexus/core/app_core.py,sha256=jW6eISWBboaDe94Sy9r4w-K2-TcL9uFGJv5JOym6Brg,10037
37
+ esd_services_api_client/nexus/core/app_dependencies.py,sha256=6jrFR5x7lLSDL1rc6YlSxMpwe641rb1YAvDE455zRbQ,6674
38
38
  esd_services_api_client/nexus/exceptions/__init__.py,sha256=feN33VdqB5-2bD9aJesJl_OlsKrNNo3hZCnQgKuaU9k,696
39
39
  esd_services_api_client/nexus/exceptions/_nexus_error.py,sha256=QvtY38mNoIA6t26dUN6UIsaPfljhtVNsbQVS7ksMb-Q,895
40
40
  esd_services_api_client/nexus/exceptions/cache_errors.py,sha256=IO_rBQKXfIRHHXQuC8kAHejgZZw9yvSJk5BPYBnDYbc,1622
@@ -44,7 +44,9 @@ esd_services_api_client/nexus/input/__init__.py,sha256=ODYhZ791tPC4-eVxSRRlh8FLD
44
44
  esd_services_api_client/nexus/input/input_processor.py,sha256=vqzeQrtRFqBKTPSEiWX_JZJTF9itMwwvWjPnJVLrSwQ,3132
45
45
  esd_services_api_client/nexus/input/input_reader.py,sha256=aXNMGxrdUX5RDYR666GSGkcZqYMFYoZ8zGVDuUFFFZQ,3505
46
46
  esd_services_api_client/nexus/input/payload_reader.py,sha256=Kq0xN1Shyqv71v6YkcrqVTDbmsEjZc8ithsXYpyu87M,2516
47
- esd_services_api_client-2.2.0.dist-info/LICENSE,sha256=0gS6zXsPp8qZhzi1xaGCIYPzb_0e8on7HCeFJe8fOpw,10693
48
- esd_services_api_client-2.2.0.dist-info/METADATA,sha256=-FllzJEL9xB4GA_iM0zRfW3tmwuaAGbL_YhqQ7WfPys,1292
49
- esd_services_api_client-2.2.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
50
- esd_services_api_client-2.2.0.dist-info/RECORD,,
47
+ esd_services_api_client/nexus/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
+ esd_services_api_client/nexus/telemetry/recorder.py,sha256=P6DDr6cQ9FYTZhgY1YMzez0Y6c6ac2FnYktapNssyJo,3644
49
+ esd_services_api_client-2.2.2.dist-info/LICENSE,sha256=0gS6zXsPp8qZhzi1xaGCIYPzb_0e8on7HCeFJe8fOpw,10693
50
+ esd_services_api_client-2.2.2.dist-info/METADATA,sha256=IQRjdfLWt5AZ89_7w-iHJuvglGwPcFhUYwtMewJVL64,1292
51
+ esd_services_api_client-2.2.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
52
+ esd_services_api_client-2.2.2.dist-info/RECORD,,