esd-services-api-client 2.1.3__py3-none-any.whl → 2.2.1__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.
Files changed (24) hide show
  1. esd_services_api_client/_version.py +1 -1
  2. esd_services_api_client/nexus/README.md +62 -37
  3. esd_services_api_client/nexus/abstractions/algrorithm_cache.py +100 -0
  4. esd_services_api_client/nexus/abstractions/input_object.py +63 -0
  5. esd_services_api_client/nexus/abstractions/nexus_object.py +18 -10
  6. esd_services_api_client/nexus/algorithms/_baseline_algorithm.py +14 -6
  7. esd_services_api_client/nexus/algorithms/_remote_algorithm.py +118 -0
  8. esd_services_api_client/nexus/algorithms/forked_algorithm.py +124 -0
  9. esd_services_api_client/nexus/algorithms/minimalistic.py +8 -1
  10. esd_services_api_client/nexus/algorithms/recursive.py +5 -1
  11. esd_services_api_client/nexus/core/app_core.py +9 -0
  12. esd_services_api_client/nexus/core/app_dependencies.py +19 -0
  13. esd_services_api_client/nexus/exceptions/cache_errors.py +49 -0
  14. esd_services_api_client/nexus/exceptions/startup_error.py +15 -0
  15. esd_services_api_client/nexus/input/__init__.py +0 -1
  16. esd_services_api_client/nexus/input/input_processor.py +11 -58
  17. esd_services_api_client/nexus/input/input_reader.py +9 -5
  18. esd_services_api_client/nexus/telemetry/__init__.py +0 -0
  19. esd_services_api_client/nexus/telemetry/recorder.py +97 -0
  20. {esd_services_api_client-2.1.3.dist-info → esd_services_api_client-2.2.1.dist-info}/METADATA +1 -1
  21. {esd_services_api_client-2.1.3.dist-info → esd_services_api_client-2.2.1.dist-info}/RECORD +23 -17
  22. esd_services_api_client/nexus/input/_functions.py +0 -89
  23. {esd_services_api_client-2.1.3.dist-info → esd_services_api_client-2.2.1.dist-info}/LICENSE +0 -0
  24. {esd_services_api_client-2.1.3.dist-info → esd_services_api_client-2.2.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,124 @@
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
+ import asyncio
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.nexus.abstractions.algrorithm_cache import InputCache
28
+ from esd_services_api_client.nexus.abstractions.nexus_object import (
29
+ NexusObject,
30
+ TPayload,
31
+ AlgorithmResult,
32
+ )
33
+ from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
34
+ from esd_services_api_client.nexus.algorithms._remote_algorithm import RemoteAlgorithm
35
+ from esd_services_api_client.nexus.input.input_processor import (
36
+ InputProcessor,
37
+ )
38
+
39
+
40
+ class ForkedAlgorithm(NexusObject[TPayload, AlgorithmResult]):
41
+ """
42
+ Forked algorithm is an algorithm that returns a result (main scenario run) and then fires off one or more forked runs
43
+ with different configurations as specified in fork class implementation.
44
+
45
+ Forked algorithm only awaits scheduling of forked runs, but never their results.
46
+
47
+ Q: How do I spawn a ForkedAlgorithm run as a remote algorithm w/o ending in an infinite loop?
48
+ A: Provide class names for forks from your algorithm configuration and construct forks with locate(fork_class)(**kwargs) calls.
49
+
50
+ Q: Can I build execution trees with this?
51
+ A: Yes, they will look like this (F(N) - Forked with N forks):
52
+
53
+ graph TB
54
+ F3["F(3)"] --> F2["F(2)"]
55
+ F3 --> F0["F(0)"]
56
+ F3 --> F1["F(1)"]
57
+ F2 --> F1_1["F(1)"]
58
+ F2 --> F0_1["F(0)"]
59
+ F1 --> F0_2["F(0)"]
60
+ F1_1 --> F0_3["F(0)"]
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ metrics_provider: MetricsProvider,
66
+ logger_factory: LoggerFactory,
67
+ forks: list[RemoteAlgorithm],
68
+ *input_processors: InputProcessor,
69
+ cache: InputCache,
70
+ ):
71
+ super().__init__(metrics_provider, logger_factory)
72
+ self._input_processors = input_processors
73
+ self._forks = forks
74
+ self._cache = cache
75
+
76
+ @abstractmethod
77
+ async def _run(self, **kwargs) -> AlgorithmResult:
78
+ """
79
+ Core logic for this algorithm. Implementing this method is mandatory.
80
+ """
81
+
82
+ @property
83
+ def _metric_tags(self) -> dict[str, str]:
84
+ return {"algorithm": self.__class__.alias()}
85
+
86
+ async def run(self, **kwargs) -> AlgorithmResult:
87
+ """
88
+ Coroutine that executes the algorithm logic.
89
+ """
90
+
91
+ @run_time_metrics_async(
92
+ metric_name="algorthm_run",
93
+ on_finish_message_template="Finished running algorithm {algorithm} in {elapsed:.2f}s seconds",
94
+ template_args={
95
+ "algorithm": self.__class__.alias().upper(),
96
+ },
97
+ )
98
+ async def _measured_run(**run_args) -> AlgorithmResult:
99
+ return await self._run(**run_args)
100
+
101
+ if len(self._forks) > 0:
102
+ self._logger.info(
103
+ "This algorithm has forks attached: {forks}. They will be executed after the main run",
104
+ forks=",".join([fork.alias() for fork in self._forks]),
105
+ )
106
+ else:
107
+ self._logger.info(
108
+ "This algorithm supports forks but none were injected. Proceeding with a main run only"
109
+ )
110
+
111
+ results = await self._cache.resolve(*self._input_processors, **kwargs)
112
+
113
+ run_result = await partial(
114
+ _measured_run,
115
+ **results,
116
+ metric_tags=self._metric_tags,
117
+ metrics_provider=self._metrics_provider,
118
+ logger=self._logger,
119
+ )()
120
+
121
+ # now await callback scheduling
122
+ await asyncio.wait([fork.run(**kwargs) for fork in self._forks])
123
+
124
+ return run_result
@@ -22,6 +22,7 @@ from abc import ABC
22
22
  from adapta.metrics import MetricsProvider
23
23
  from injector import inject
24
24
 
25
+ from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
25
26
  from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
26
27
  from esd_services_api_client.nexus.abstractions.nexus_object import TPayload
27
28
  from esd_services_api_client.nexus.algorithms._baseline_algorithm import (
@@ -41,5 +42,11 @@ class MinimalisticAlgorithm(BaselineAlgorithm[TPayload], ABC):
41
42
  metrics_provider: MetricsProvider,
42
43
  logger_factory: LoggerFactory,
43
44
  *input_processors: InputProcessor,
45
+ cache: InputCache,
44
46
  ):
45
- super().__init__(metrics_provider, logger_factory, *input_processors)
47
+ super().__init__(
48
+ metrics_provider,
49
+ logger_factory,
50
+ *input_processors,
51
+ cache=cache,
52
+ )
@@ -23,6 +23,7 @@ from abc import abstractmethod
23
23
  from adapta.metrics import MetricsProvider
24
24
  from injector import inject
25
25
 
26
+ from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
26
27
  from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
27
28
  from esd_services_api_client.nexus.abstractions.nexus_object import (
28
29
  TPayload,
@@ -45,8 +46,11 @@ class RecursiveAlgorithm(BaselineAlgorithm[TPayload]):
45
46
  metrics_provider: MetricsProvider,
46
47
  logger_factory: LoggerFactory,
47
48
  *input_processors: InputProcessor,
49
+ cache: InputCache,
48
50
  ):
49
- super().__init__(metrics_provider, logger_factory, *input_processors)
51
+ super().__init__(
52
+ metrics_provider, logger_factory, *input_processors, cache=cache
53
+ )
50
54
 
51
55
  @abstractmethod
52
56
  async def _is_finished(self, **kwargs) -> bool:
@@ -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()
@@ -28,6 +28,7 @@ from adapta.storage.query_enabled_store import QueryEnabledStore
28
28
  from injector import Module, singleton, provider
29
29
 
30
30
  from esd_services_api_client.crystal import CrystalConnector
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
32
33
  from esd_services_api_client.nexus.abstractions.socket_provider import (
33
34
  ExternalSocketProvider,
@@ -43,6 +44,7 @@ from esd_services_api_client.nexus.input.input_reader import InputReader
43
44
  from esd_services_api_client.nexus.input.payload_reader import (
44
45
  AlgorithmPayload,
45
46
  )
47
+ from esd_services_api_client.nexus.telemetry.recorder import TelemetryRecorder
46
48
 
47
49
 
48
50
  @final
@@ -165,6 +167,21 @@ class ExternalSocketsModule(Module):
165
167
  )
166
168
 
167
169
 
170
+ @final
171
+ class CacheModule(Module):
172
+ """
173
+ Storage client module.
174
+ """
175
+
176
+ @singleton
177
+ @provider
178
+ def provide(self) -> InputCache:
179
+ """
180
+ Dependency provider.
181
+ """
182
+ return InputCache()
183
+
184
+
168
185
  @final
169
186
  class ServiceConfigurator:
170
187
  """
@@ -178,6 +195,8 @@ class ServiceConfigurator:
178
195
  QueryEnabledStoreModule(),
179
196
  StorageClientModule(),
180
197
  ExternalSocketsModule(),
198
+ CacheModule(),
199
+ type(f"{TelemetryRecorder.__name__}Module", (Module,), {})(),
181
200
  ]
182
201
 
183
202
  @property
@@ -0,0 +1,49 @@
1
+ """
2
+ Cache module exceptions.
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
+ from esd_services_api_client.nexus.exceptions._nexus_error import (
21
+ FatalNexusError,
22
+ TransientNexusError,
23
+ )
24
+
25
+
26
+ class FatalCachingError(FatalNexusError):
27
+ """
28
+ Cache-level exception that shuts down the Nexus.
29
+ """
30
+
31
+ def __init__(self, failed_object: str):
32
+ super().__init__()
33
+ self._failed_object = failed_object
34
+
35
+ def __str__(self) -> str:
36
+ return f"Nexus object with alias '{self._failed_object}' failed the caching operation that cannot be retried. Review traceback for more information"
37
+
38
+
39
+ class TransientCachingError(TransientNexusError):
40
+ """
41
+ Cache-level exception that will initiate a retry with the Nexus re-activation.
42
+ """
43
+
44
+ def __init__(self, failed_object):
45
+ super().__init__()
46
+ self._failed_object = failed_object
47
+
48
+ def __str__(self) -> str:
49
+ return f"Nexus object with alias '{self._failed_object}' failed the caching operation and it will be retried. Review traceback for more information"
@@ -1,6 +1,7 @@
1
1
  """
2
2
  App startup exceptions.
3
3
  """
4
+ from typing import Type
4
5
 
5
6
  # Copyright (c) 2023-2024. ECCO Sneaks & Data
6
7
  #
@@ -46,3 +47,17 @@ class FatalStartupConfigurationError(FatalNexusError):
46
47
 
47
48
  def __str__(self) -> str:
48
49
  return f"Algorithm initialization failed due to a missing configuration entry: {self._missing_entry}."
50
+
51
+
52
+ class FatalAlgorithmConfigurationError(FatalNexusError):
53
+ """
54
+ Service configuration error that shuts down the Nexus.
55
+ """
56
+
57
+ def __init__(self, message: str, algorithm_class: Type):
58
+ super().__init__()
59
+ self._message = message
60
+ self._type_name = str(algorithm_class)
61
+
62
+ def __str__(self) -> str:
63
+ return f"Algorithm {self._type_name} misconfigured: {self._message}."
@@ -20,4 +20,3 @@
20
20
 
21
21
  from esd_services_api_client.nexus.input.input_processor import *
22
22
  from esd_services_api_client.nexus.input.input_reader import *
23
- from esd_services_api_client.nexus.input._functions import *
@@ -1,7 +1,6 @@
1
1
  """
2
2
  Input processing.
3
3
  """
4
- import asyncio
5
4
 
6
5
  # Copyright (c) 2023-2024. ECCO Sneaks & Data
7
6
  #
@@ -25,22 +24,17 @@ from typing import Optional
25
24
  from adapta.metrics import MetricsProvider
26
25
  from adapta.utils.decorators import run_time_metrics_async
27
26
 
27
+ from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
28
+ from esd_services_api_client.nexus.abstractions.input_object import InputObject
28
29
  from esd_services_api_client.nexus.abstractions.nexus_object import (
29
- NexusObject,
30
30
  TPayload,
31
31
  TResult,
32
32
  )
33
33
  from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
34
- from esd_services_api_client.nexus.input._functions import (
35
- resolve_readers,
36
- resolve_reader_exc_type,
37
- )
38
34
  from esd_services_api_client.nexus.input.input_reader import InputReader
39
35
 
40
- _processor_cache = {}
41
-
42
36
 
43
- class InputProcessor(NexusObject[TPayload, TResult]):
37
+ class InputProcessor(InputObject[TPayload, TResult]):
44
38
  """
45
39
  Base class for raw data processing into algorithm input.
46
40
  """
@@ -51,24 +45,23 @@ class InputProcessor(NexusObject[TPayload, TResult]):
51
45
  payload: TPayload,
52
46
  metrics_provider: MetricsProvider,
53
47
  logger_factory: LoggerFactory,
48
+ cache: InputCache
54
49
  ):
55
50
  super().__init__(metrics_provider, logger_factory)
56
51
  self._readers = readers
57
52
  self._payload = payload
58
53
  self._result: Optional[TResult] = None
59
-
60
- async def _read_input(self) -> dict[str, TResult]:
61
- return await resolve_readers(*self._readers)
54
+ self._cache = cache
62
55
 
63
56
  @property
64
- def result(self) -> dict[str, TResult]:
57
+ def data(self) -> Optional[TResult]:
65
58
  """
66
59
  Data returned by this processor
67
60
  """
68
61
  return self._result
69
62
 
70
63
  @abstractmethod
71
- async def _process_input(self, **kwargs) -> dict[str, TResult]:
64
+ async def _process_input(self, **kwargs) -> TResult:
72
65
  """
73
66
  Input processing logic. Implement this method to prepare data for your algorithm code.
74
67
  """
@@ -77,7 +70,7 @@ class InputProcessor(NexusObject[TPayload, TResult]):
77
70
  def _metric_tags(self) -> dict[str, str]:
78
71
  return {"processor": self.__class__.alias()}
79
72
 
80
- async def process_input(self, **kwargs) -> dict[str, TResult]:
73
+ async def process(self, **kwargs) -> TResult:
81
74
  """
82
75
  Input processing coroutine. Do not override this method.
83
76
  """
@@ -89,8 +82,9 @@ class InputProcessor(NexusObject[TPayload, TResult]):
89
82
  "processor": self.__class__.alias().upper(),
90
83
  },
91
84
  )
92
- async def _process(**_) -> dict[str, TResult]:
93
- return await self._process_input(**kwargs)
85
+ async def _process(**_) -> TResult:
86
+ readers = await self._cache.resolve(*self._readers)
87
+ return await self._process_input(**(kwargs | readers))
94
88
 
95
89
  if self._result is None:
96
90
  self._result = await partial(
@@ -101,44 +95,3 @@ class InputProcessor(NexusObject[TPayload, TResult]):
101
95
  )()
102
96
 
103
97
  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
@@ -26,15 +26,16 @@ from adapta.process_communication import DataSocket
26
26
  from adapta.storage.query_enabled_store import QueryEnabledStore
27
27
  from adapta.utils.decorators import run_time_metrics_async
28
28
 
29
+ from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
30
+ from esd_services_api_client.nexus.abstractions.input_object import InputObject
29
31
  from esd_services_api_client.nexus.abstractions.nexus_object import (
30
- NexusObject,
31
32
  TPayload,
32
33
  TResult,
33
34
  )
34
35
  from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
35
36
 
36
37
 
37
- class InputReader(NexusObject[TPayload, TResult]):
38
+ class InputReader(InputObject[TPayload, TResult]):
38
39
  """
39
40
  Base class for a raw data reader.
40
41
  """
@@ -47,6 +48,7 @@ class InputReader(NexusObject[TPayload, TResult]):
47
48
  payload: TPayload,
48
49
  *readers: "InputReader",
49
50
  socket: Optional[DataSocket] = None,
51
+ cache: InputCache
50
52
  ):
51
53
  super().__init__(metrics_provider, logger_factory)
52
54
  self.socket = socket
@@ -54,6 +56,7 @@ class InputReader(NexusObject[TPayload, TResult]):
54
56
  self._data: Optional[TResult] = None
55
57
  self._readers = readers
56
58
  self._payload = payload
59
+ self._cache = cache
57
60
 
58
61
  @property
59
62
  def data(self) -> Optional[TResult]:
@@ -63,7 +66,7 @@ class InputReader(NexusObject[TPayload, TResult]):
63
66
  return self._data
64
67
 
65
68
  @abstractmethod
66
- async def _read_input(self) -> TResult:
69
+ async def _read_input(self, **kwargs) -> TResult:
67
70
  """
68
71
  Actual data reader logic. Implementing this method is mandatory for the reader to work
69
72
  """
@@ -72,7 +75,7 @@ class InputReader(NexusObject[TPayload, TResult]):
72
75
  def _metric_tags(self) -> dict[str, str]:
73
76
  return {"entity": self.__class__.alias()}
74
77
 
75
- async def read(self) -> TResult:
78
+ async def process(self, **_) -> TResult:
76
79
  """
77
80
  Coroutine that reads the data from external store and converts it to a dataframe, or generates data locally. Do not override this method.
78
81
  """
@@ -88,7 +91,8 @@ class InputReader(NexusObject[TPayload, TResult]):
88
91
  | ({"data_path": self.socket.data_path} if self.socket else {}),
89
92
  )
90
93
  async def _read(**_) -> TResult:
91
- return await self._read_input()
94
+ readers = await self._cache.resolve(*self._readers)
95
+ return await self._read_input(**readers)
92
96
 
93
97
  if self._data is None:
94
98
  self._data = await partial(
File without changes
@@ -0,0 +1,97 @@
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
+ self._storage_client.save_data_as_blob(
63
+ data=entity_to_record,
64
+ blob_path=DataSocket(
65
+ alias="telemetry",
66
+ data_path=f"{self._telemetry_base_path}/{entity_name}/{run_id}",
67
+ data_format="null",
68
+ ).parse_data_path(),
69
+ serialization_format=DictJsonSerializationFormat
70
+ if isinstance(entity_to_record, dict)
71
+ else DataFrameParquetSerializationFormat,
72
+ overwrite=True,
73
+ )
74
+
75
+ telemetry_tasks = [
76
+ asyncio.create_task(
77
+ partial(
78
+ _record,
79
+ entity_to_record=telemetry_value,
80
+ entity_name=telemetry_key,
81
+ run_id=run_id,
82
+ )()
83
+ )
84
+ for telemetry_key, telemetry_value in telemetry_args.items()
85
+ ]
86
+
87
+ done, pending = await asyncio.wait(telemetry_tasks)
88
+ if len(pending) > 0:
89
+ self._logger.warning(
90
+ "Some telemetry recording operations did not complete within specified time. This run might lack observability coverage."
91
+ )
92
+ for done_telemetry_task in done:
93
+ telemetry_exc = done_telemetry_task.exception()
94
+ if telemetry_exc:
95
+ self._logger.warning(
96
+ "Telemetry recoding failed", exception=telemetry_exc
97
+ )
@@ -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