esd-services-api-client 2.0.0__py3-none-any.whl → 2.0.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.
- esd_services_api_client/_version.py +1 -1
- esd_services_api_client/crystal/_connector.py +7 -7
- esd_services_api_client/nexus/README.md +280 -0
- esd_services_api_client/nexus/__init__.py +18 -0
- esd_services_api_client/nexus/abstractions/__init__.py +18 -0
- esd_services_api_client/nexus/abstractions/logger_factory.py +64 -0
- esd_services_api_client/nexus/abstractions/nexus_object.py +64 -0
- esd_services_api_client/nexus/abstractions/socket_provider.py +51 -0
- esd_services_api_client/nexus/algorithms/__init__.py +22 -0
- esd_services_api_client/nexus/algorithms/_baseline_algorithm.py +56 -0
- esd_services_api_client/nexus/algorithms/distributed.py +53 -0
- esd_services_api_client/nexus/algorithms/minimalistic.py +44 -0
- esd_services_api_client/nexus/algorithms/recursive.py +58 -0
- esd_services_api_client/nexus/core/__init__.py +18 -0
- esd_services_api_client/nexus/core/app_core.py +259 -0
- esd_services_api_client/nexus/core/app_dependencies.py +205 -0
- esd_services_api_client/nexus/exceptions/__init__.py +20 -0
- esd_services_api_client/nexus/exceptions/_nexus_error.py +30 -0
- esd_services_api_client/nexus/exceptions/input_reader_error.py +51 -0
- esd_services_api_client/nexus/exceptions/startup_error.py +48 -0
- esd_services_api_client/nexus/input/__init__.py +22 -0
- esd_services_api_client/nexus/input/input_processor.py +94 -0
- esd_services_api_client/nexus/input/input_reader.py +109 -0
- esd_services_api_client/nexus/input/payload_reader.py +83 -0
- {esd_services_api_client-2.0.0.dist-info → esd_services_api_client-2.0.2.dist-info}/METADATA +5 -2
- esd_services_api_client-2.0.2.dist-info/RECORD +43 -0
- esd_services_api_client-2.0.0.dist-info/RECORD +0 -21
- {esd_services_api_client-2.0.0.dist-info → esd_services_api_client-2.0.2.dist-info}/LICENSE +0 -0
- {esd_services_api_client-2.0.0.dist-info → esd_services_api_client-2.0.2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
"""
|
2
|
+
Simple algorithm with a single train/predict/solve iteration.
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Copyright (c) 2023. 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 abc import ABC
|
21
|
+
|
22
|
+
from adapta.metrics import MetricsProvider
|
23
|
+
from injector import inject
|
24
|
+
|
25
|
+
from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
|
26
|
+
from esd_services_api_client.nexus.algorithms._baseline_algorithm import (
|
27
|
+
BaselineAlgorithm,
|
28
|
+
)
|
29
|
+
from esd_services_api_client.nexus.input import InputProcessor
|
30
|
+
|
31
|
+
|
32
|
+
class MinimalisticAlgorithm(BaselineAlgorithm, ABC):
|
33
|
+
"""
|
34
|
+
Simple algorithm base class.
|
35
|
+
"""
|
36
|
+
|
37
|
+
@inject
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
input_processor: InputProcessor,
|
41
|
+
metrics_provider: MetricsProvider,
|
42
|
+
logger_factory: LoggerFactory,
|
43
|
+
):
|
44
|
+
super().__init__(input_processor, metrics_provider, logger_factory)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
"""
|
2
|
+
Algorithm that uses its output to alter the input for the next iteration, until a certain condition is met.
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Copyright (c) 2023. 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
|
+
|
23
|
+
from adapta.metrics import MetricsProvider
|
24
|
+
from injector import inject
|
25
|
+
from pandas import DataFrame as PandasDataFrame
|
26
|
+
|
27
|
+
from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
|
28
|
+
from esd_services_api_client.nexus.algorithms._baseline_algorithm import (
|
29
|
+
BaselineAlgorithm,
|
30
|
+
)
|
31
|
+
from esd_services_api_client.nexus.input import InputProcessor
|
32
|
+
|
33
|
+
|
34
|
+
class RecursiveAlgorithm(BaselineAlgorithm):
|
35
|
+
"""
|
36
|
+
Recursive algorithm base class.
|
37
|
+
"""
|
38
|
+
|
39
|
+
@inject
|
40
|
+
def __init__(
|
41
|
+
self,
|
42
|
+
input_processor: InputProcessor,
|
43
|
+
metrics_provider: MetricsProvider,
|
44
|
+
logger_factory: LoggerFactory,
|
45
|
+
):
|
46
|
+
super().__init__(input_processor, metrics_provider, logger_factory)
|
47
|
+
|
48
|
+
@abstractmethod
|
49
|
+
async def _is_finished(self, **kwargs) -> bool:
|
50
|
+
""" """
|
51
|
+
|
52
|
+
async def run(self, **kwargs) -> PandasDataFrame:
|
53
|
+
result = await self._run(
|
54
|
+
**(await self._input_processor.process_input(**kwargs))
|
55
|
+
)
|
56
|
+
if self._is_finished(**result):
|
57
|
+
return result
|
58
|
+
return await self.run(**result)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
"""
|
2
|
+
Import index.
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Copyright (c) 2023. 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
|
+
#
|
@@ -0,0 +1,259 @@
|
|
1
|
+
"""
|
2
|
+
Nexus Core.
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Copyright (c) 2023. 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
|
+
import os
|
22
|
+
import platform
|
23
|
+
import signal
|
24
|
+
import sys
|
25
|
+
import traceback
|
26
|
+
from typing import final, Type, Optional, Coroutine
|
27
|
+
|
28
|
+
import backoff
|
29
|
+
import urllib3.exceptions
|
30
|
+
import azure.core.exceptions
|
31
|
+
from adapta.process_communication import DataSocket
|
32
|
+
from adapta.storage.blob.base import StorageClient
|
33
|
+
from adapta.storage.models.format import DataFrameJsonSerializationFormat
|
34
|
+
from injector import Injector
|
35
|
+
|
36
|
+
from pandas import DataFrame as PandasDataFrame
|
37
|
+
|
38
|
+
import esd_services_api_client.nexus.exceptions
|
39
|
+
from esd_services_api_client.crystal import (
|
40
|
+
add_crystal_args,
|
41
|
+
extract_crystal_args,
|
42
|
+
CrystalConnector,
|
43
|
+
AlgorithmRunResult,
|
44
|
+
CrystalEntrypointArguments,
|
45
|
+
)
|
46
|
+
from esd_services_api_client.nexus.algorithms._baseline_algorithm import (
|
47
|
+
BaselineAlgorithm,
|
48
|
+
)
|
49
|
+
from esd_services_api_client.nexus.core.app_dependencies import (
|
50
|
+
ServiceConfigurator,
|
51
|
+
)
|
52
|
+
from esd_services_api_client.nexus.input.input_processor import InputProcessor
|
53
|
+
from esd_services_api_client.nexus.input.input_reader import InputReader
|
54
|
+
from esd_services_api_client.nexus.input.payload_reader import (
|
55
|
+
AlgorithmPayloadReader,
|
56
|
+
AlgorithmPayload,
|
57
|
+
)
|
58
|
+
|
59
|
+
|
60
|
+
def is_transient_exception(exception: Optional[BaseException]) -> Optional[bool]:
|
61
|
+
"""
|
62
|
+
Check if the exception is retryable.
|
63
|
+
"""
|
64
|
+
if not exception:
|
65
|
+
return None
|
66
|
+
match type(exception):
|
67
|
+
case esd_services_api_client.nexus.exceptions.FatalNexusError:
|
68
|
+
return False
|
69
|
+
case esd_services_api_client.nexus.exceptions.TransientNexusError:
|
70
|
+
return True
|
71
|
+
case _:
|
72
|
+
return False
|
73
|
+
|
74
|
+
|
75
|
+
async def graceful_shutdown():
|
76
|
+
"""
|
77
|
+
Gracefully stops the event loop.
|
78
|
+
"""
|
79
|
+
for task in asyncio.all_tasks():
|
80
|
+
if task is not asyncio.current_task():
|
81
|
+
task.cancel()
|
82
|
+
|
83
|
+
asyncio.get_event_loop().stop()
|
84
|
+
|
85
|
+
|
86
|
+
def attach_signal_handlers():
|
87
|
+
"""
|
88
|
+
Signal handlers for the event loop graceful shutdown.
|
89
|
+
"""
|
90
|
+
if platform.system() != "Windows":
|
91
|
+
asyncio.get_event_loop().add_signal_handler(
|
92
|
+
signal.SIGTERM, lambda: asyncio.create_task(graceful_shutdown())
|
93
|
+
)
|
94
|
+
|
95
|
+
|
96
|
+
@final
|
97
|
+
class Nexus:
|
98
|
+
"""
|
99
|
+
Nexus is the object that manages everything related to running algorithms through Crystal.
|
100
|
+
It takes care of result submission, signal handling, result recording, post-processing, metrics, logging etc.
|
101
|
+
"""
|
102
|
+
|
103
|
+
def __init__(self, args: CrystalEntrypointArguments):
|
104
|
+
self._configurator = ServiceConfigurator()
|
105
|
+
self._injector: Optional[Injector] = None
|
106
|
+
self._algorithm_class: Optional[Type[BaselineAlgorithm]] = None
|
107
|
+
self._run_args = args
|
108
|
+
self._algorithm_run_task: Optional[asyncio.Task] = None
|
109
|
+
self._on_complete_tasks: list[Coroutine] = []
|
110
|
+
|
111
|
+
attach_signal_handlers()
|
112
|
+
|
113
|
+
@property
|
114
|
+
def algorithm_class(self) -> Type[BaselineAlgorithm]:
|
115
|
+
"""
|
116
|
+
Class of the algorithm used by this Nexus instance.
|
117
|
+
"""
|
118
|
+
return self._algorithm_class
|
119
|
+
|
120
|
+
def on_complete(self, coro: Coroutine) -> "Nexus":
|
121
|
+
"""
|
122
|
+
Attaches a coroutine to run on algorithm completion.
|
123
|
+
"""
|
124
|
+
self._on_complete_tasks.append(coro)
|
125
|
+
return self
|
126
|
+
|
127
|
+
def add_reader(self, reader: Type[InputReader]) -> "Nexus":
|
128
|
+
"""
|
129
|
+
Adds an input data reader for the algorithm.
|
130
|
+
"""
|
131
|
+
self._configurator = self._configurator.with_input_reader(reader)
|
132
|
+
return self
|
133
|
+
|
134
|
+
def use_processor(self, input_processor: Type[InputProcessor]) -> "Nexus":
|
135
|
+
"""
|
136
|
+
Initialises an input processor for the algorithm.
|
137
|
+
"""
|
138
|
+
self._configurator = self._configurator.with_input_processor(input_processor)
|
139
|
+
return self
|
140
|
+
|
141
|
+
def use_algorithm(self, algorithm: Type[BaselineAlgorithm]) -> "Nexus":
|
142
|
+
"""
|
143
|
+
Algorithm to use for this Nexus instance
|
144
|
+
"""
|
145
|
+
self._algorithm_class = algorithm
|
146
|
+
return self
|
147
|
+
|
148
|
+
async def inject_payload(self, *payload_types: Type[AlgorithmPayload]) -> "Nexus":
|
149
|
+
"""
|
150
|
+
Adds payloads processed into the specified types to the DI container
|
151
|
+
"""
|
152
|
+
for payload_type in payload_types:
|
153
|
+
|
154
|
+
async def get_payload() -> payload_type: # pylint: disable=W0640
|
155
|
+
async with AlgorithmPayloadReader(
|
156
|
+
payload_uri=self._run_args.sas_uri,
|
157
|
+
payload_type=payload_type, # pylint: disable=W0640
|
158
|
+
) as reader:
|
159
|
+
return reader.payload
|
160
|
+
|
161
|
+
# pylint warnings are silenced here because closure is called inside the same loop it is defined, thus each value of a loop variable is used
|
162
|
+
|
163
|
+
self._configurator = self._configurator.with_payload((await get_payload()))
|
164
|
+
return self
|
165
|
+
|
166
|
+
async def _submit_result(
|
167
|
+
self,
|
168
|
+
result: Optional[PandasDataFrame] = None,
|
169
|
+
ex: Optional[BaseException] = None,
|
170
|
+
) -> None:
|
171
|
+
@backoff.on_exception(
|
172
|
+
wait_gen=backoff.expo,
|
173
|
+
exception=(
|
174
|
+
azure.core.exceptions.HttpResponseError,
|
175
|
+
urllib3.exceptions.HTTPError,
|
176
|
+
),
|
177
|
+
max_time=10,
|
178
|
+
raise_on_giveup=True,
|
179
|
+
)
|
180
|
+
def save_result(data: PandasDataFrame) -> str:
|
181
|
+
"""
|
182
|
+
Saves blob and returns the uri
|
183
|
+
|
184
|
+
:param: path: path to save the blob
|
185
|
+
:param: output_consumer_df: Formatted dataframe into ECCO format
|
186
|
+
:param: storage_client: Azure storage client
|
187
|
+
|
188
|
+
:return: blob uri
|
189
|
+
"""
|
190
|
+
storage_client = self._injector.get(StorageClient)
|
191
|
+
output_path = f"{os.getenv('NEXUS__ALGORITHM_OUTPUT_PATH')}/{self._run_args.request_id}.json"
|
192
|
+
blob_path = DataSocket(
|
193
|
+
data_path=output_path, alias="output", data_format="null"
|
194
|
+
).parse_data_path()
|
195
|
+
storage_client.save_data_as_blob(
|
196
|
+
data=data,
|
197
|
+
blob_path=blob_path,
|
198
|
+
serialization_format=DataFrameJsonSerializationFormat,
|
199
|
+
overwrite=True,
|
200
|
+
)
|
201
|
+
return storage_client.get_blob_uri(blob_path=blob_path)
|
202
|
+
|
203
|
+
receiver = self._injector.get(CrystalConnector)
|
204
|
+
|
205
|
+
match is_transient_exception(ex):
|
206
|
+
case None:
|
207
|
+
receiver.submit_result(
|
208
|
+
result=AlgorithmRunResult(sas_uri=save_result(result)),
|
209
|
+
run_id=self._run_args.request_id,
|
210
|
+
algorithm=os.getenv("CRYSTAL__ALGORITHM_NAME"),
|
211
|
+
debug=os.getenv("IS_LOCAL_RUN") == "1",
|
212
|
+
)
|
213
|
+
case True:
|
214
|
+
sys.exit(1)
|
215
|
+
case False:
|
216
|
+
receiver.submit_result(
|
217
|
+
result=AlgorithmRunResult(
|
218
|
+
message=f"{type(ex)}: {ex})", cause=traceback.format_exc()
|
219
|
+
),
|
220
|
+
run_id=self._run_args.request_id,
|
221
|
+
algorithm=os.getenv("CRYSTAL__ALGORITHM_NAME"),
|
222
|
+
debug=os.getenv("IS_LOCAL_RUN") == "1",
|
223
|
+
)
|
224
|
+
case _:
|
225
|
+
sys.exit(1)
|
226
|
+
|
227
|
+
async def activate(self):
|
228
|
+
"""
|
229
|
+
Activates the run sequence.
|
230
|
+
"""
|
231
|
+
self._injector = Injector(self._configurator.injection_binds)
|
232
|
+
|
233
|
+
algorithm: BaselineAlgorithm = self._injector.get(self._algorithm_class)
|
234
|
+
|
235
|
+
async with algorithm as instance:
|
236
|
+
self._algorithm_run_task = asyncio.create_task(
|
237
|
+
instance.run(**self._run_args.__dict__)
|
238
|
+
)
|
239
|
+
await self._algorithm_run_task
|
240
|
+
ex = self._algorithm_run_task.exception()
|
241
|
+
on_complete_tasks = [
|
242
|
+
asyncio.create_task(on_complete_task)
|
243
|
+
for on_complete_task in self._on_complete_tasks
|
244
|
+
]
|
245
|
+
|
246
|
+
await self._submit_result(
|
247
|
+
self._algorithm_run_task.result() if not ex else None,
|
248
|
+
self._algorithm_run_task.exception(),
|
249
|
+
)
|
250
|
+
if len(on_complete_tasks) > 0:
|
251
|
+
await asyncio.wait(on_complete_tasks)
|
252
|
+
|
253
|
+
@classmethod
|
254
|
+
def create(cls) -> "Nexus":
|
255
|
+
"""
|
256
|
+
Creates a Nexus instance with Crystal CLI arguments parsed into input.
|
257
|
+
"""
|
258
|
+
parser = add_crystal_args()
|
259
|
+
return Nexus(extract_crystal_args(parser.parse_args()))
|
@@ -0,0 +1,205 @@
|
|
1
|
+
"""
|
2
|
+
Dependency injections.
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Copyright (c) 2023. 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 json
|
21
|
+
import os
|
22
|
+
from pydoc import locate
|
23
|
+
from typing import final, Type
|
24
|
+
|
25
|
+
from adapta.metrics import MetricsProvider
|
26
|
+
from adapta.storage.blob.base import StorageClient
|
27
|
+
from adapta.storage.query_enabled_store import QueryEnabledStore
|
28
|
+
from injector import Module, singleton, provider
|
29
|
+
|
30
|
+
from esd_services_api_client.crystal import CrystalConnector
|
31
|
+
from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
|
32
|
+
from esd_services_api_client.nexus.abstractions.socket_provider import (
|
33
|
+
ExternalSocketProvider,
|
34
|
+
)
|
35
|
+
from esd_services_api_client.nexus.exceptions.startup_error import (
|
36
|
+
FatalStartupConfigurationError,
|
37
|
+
)
|
38
|
+
from esd_services_api_client.nexus.input.input_processor import InputProcessor
|
39
|
+
from esd_services_api_client.nexus.input.input_reader import InputReader
|
40
|
+
from esd_services_api_client.nexus.input.payload_reader import (
|
41
|
+
AlgorithmPayload,
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
class MetricsModule(Module):
|
46
|
+
"""
|
47
|
+
Metrics provider module.
|
48
|
+
"""
|
49
|
+
|
50
|
+
@singleton
|
51
|
+
@provider
|
52
|
+
def provide(self) -> MetricsProvider:
|
53
|
+
"""
|
54
|
+
DI factory method.
|
55
|
+
"""
|
56
|
+
metrics_class: Type[MetricsProvider] = locate(
|
57
|
+
os.getenv(
|
58
|
+
"NEXUS__METRIC_PROVIDER_CLASS",
|
59
|
+
"adapta.metrics.providers.datadog_provider.DatadogMetricsProvider",
|
60
|
+
)
|
61
|
+
)
|
62
|
+
metrics_settings = json.loads(os.getenv("NEXUS__METRIC_PROVIDER_CONFIGURATION"))
|
63
|
+
return metrics_class(**metrics_settings)
|
64
|
+
|
65
|
+
|
66
|
+
class LoggerFactoryModule(Module):
|
67
|
+
"""
|
68
|
+
Logger factory module.
|
69
|
+
"""
|
70
|
+
|
71
|
+
@singleton
|
72
|
+
@provider
|
73
|
+
def provide(self) -> LoggerFactory:
|
74
|
+
"""
|
75
|
+
DI factory method.
|
76
|
+
"""
|
77
|
+
return LoggerFactory()
|
78
|
+
|
79
|
+
|
80
|
+
class CrystalReceiverClientModule(Module):
|
81
|
+
"""
|
82
|
+
Crystal receiver module.
|
83
|
+
"""
|
84
|
+
|
85
|
+
@singleton
|
86
|
+
@provider
|
87
|
+
def provide(self) -> CrystalConnector:
|
88
|
+
"""
|
89
|
+
DI factory method.
|
90
|
+
"""
|
91
|
+
return CrystalConnector.create_anonymous(
|
92
|
+
receiver_base_url=os.getenv("NEXUS__ALGORITHM_METRIC_NAMESPACE"),
|
93
|
+
)
|
94
|
+
|
95
|
+
|
96
|
+
class QueryEnabledStoreModule(Module):
|
97
|
+
"""
|
98
|
+
QES module.
|
99
|
+
"""
|
100
|
+
|
101
|
+
@singleton
|
102
|
+
@provider
|
103
|
+
def provide(self) -> QueryEnabledStore:
|
104
|
+
"""
|
105
|
+
DI factory method.
|
106
|
+
"""
|
107
|
+
return QueryEnabledStore.from_string(os.getenv("NEXUS__QES_CONNECTION_STRING"))
|
108
|
+
|
109
|
+
|
110
|
+
class StorageClientModule(Module):
|
111
|
+
"""
|
112
|
+
Storage client module.
|
113
|
+
"""
|
114
|
+
|
115
|
+
@singleton
|
116
|
+
@provider
|
117
|
+
def provide(self) -> StorageClient:
|
118
|
+
"""
|
119
|
+
DI factory method.
|
120
|
+
"""
|
121
|
+
storage_client_class: Type[StorageClient] = locate(
|
122
|
+
os.getenv(
|
123
|
+
"NEXUS__STORAGE_CLIENT_CLASS",
|
124
|
+
)
|
125
|
+
)
|
126
|
+
if not storage_client_class:
|
127
|
+
raise FatalStartupConfigurationError("NEXUS__STORAGE_CLIENT_CLASS")
|
128
|
+
if "NEXUS__ALGORITHM_OUTPUT_PATH" not in os.environ:
|
129
|
+
raise FatalStartupConfigurationError("NEXUS__ALGORITHM_OUTPUT_PATH")
|
130
|
+
|
131
|
+
return storage_client_class.for_storage_path(
|
132
|
+
path=os.getenv("NEXUS__ALGORITHM_OUTPUT_PATH")
|
133
|
+
)
|
134
|
+
|
135
|
+
|
136
|
+
@final
|
137
|
+
class ExternalSocketsModule(Module):
|
138
|
+
"""
|
139
|
+
Storage client module.
|
140
|
+
"""
|
141
|
+
|
142
|
+
@singleton
|
143
|
+
@provider
|
144
|
+
def provide(self) -> ExternalSocketProvider:
|
145
|
+
"""
|
146
|
+
Dependency provider.
|
147
|
+
"""
|
148
|
+
if "NEXUS__ALGORITHM_INPUT_EXTERNAL_DATA_SOCKETS" not in os.environ:
|
149
|
+
raise FatalStartupConfigurationError(
|
150
|
+
"NEXUS__ALGORITHM_INPUT_EXTERNAL_DATA_SOCKETS"
|
151
|
+
)
|
152
|
+
|
153
|
+
return ExternalSocketProvider.from_serialized(
|
154
|
+
os.getenv("NEXUS__ALGORITHM_INPUT_EXTERNAL_DATA_SOCKETS")
|
155
|
+
)
|
156
|
+
|
157
|
+
|
158
|
+
@final
|
159
|
+
class ServiceConfigurator:
|
160
|
+
"""
|
161
|
+
Runtime DI support.
|
162
|
+
"""
|
163
|
+
|
164
|
+
def __init__(self):
|
165
|
+
self._injection_binds = [
|
166
|
+
MetricsModule(),
|
167
|
+
CrystalReceiverClientModule(),
|
168
|
+
QueryEnabledStoreModule(),
|
169
|
+
StorageClientModule(),
|
170
|
+
ExternalSocketsModule(),
|
171
|
+
]
|
172
|
+
|
173
|
+
@property
|
174
|
+
def injection_binds(self) -> list:
|
175
|
+
"""
|
176
|
+
Currently configured injection bindings
|
177
|
+
"""
|
178
|
+
return self._injection_binds
|
179
|
+
|
180
|
+
def with_input_reader(self, reader: Type[InputReader]) -> "ServiceConfigurator":
|
181
|
+
"""
|
182
|
+
Adds the input reader implementation to the DI.
|
183
|
+
"""
|
184
|
+
self._injection_binds.append(type(f"{reader.__name__}Module", (Module,), {})())
|
185
|
+
return self
|
186
|
+
|
187
|
+
def with_input_processor(
|
188
|
+
self, input_processor: Type[InputProcessor]
|
189
|
+
) -> "ServiceConfigurator":
|
190
|
+
"""
|
191
|
+
Adds the input processor implementation
|
192
|
+
"""
|
193
|
+
self._injection_binds.append(
|
194
|
+
type(f"{input_processor.__name__}Module", (Module,), {})()
|
195
|
+
)
|
196
|
+
return self
|
197
|
+
|
198
|
+
def with_payload(self, payload: AlgorithmPayload) -> "ServiceConfigurator":
|
199
|
+
"""
|
200
|
+
Adds the specified payload instance to the DI container.
|
201
|
+
"""
|
202
|
+
self._injection_binds.append(
|
203
|
+
lambda binder: binder.bind(payload.__class__, to=payload, scope=singleton)
|
204
|
+
)
|
205
|
+
return self
|
@@ -0,0 +1,20 @@
|
|
1
|
+
"""
|
2
|
+
Import index.
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Copyright (c) 2023. 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 *
|
@@ -0,0 +1,30 @@
|
|
1
|
+
"""
|
2
|
+
Base classes for custom exceptions.
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Copyright (c) 2023. 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
|
+
class FatalNexusError(BaseException):
|
22
|
+
"""
|
23
|
+
Base exception class for all non-retryable application errors.
|
24
|
+
"""
|
25
|
+
|
26
|
+
|
27
|
+
class TransientNexusError(BaseException):
|
28
|
+
"""
|
29
|
+
Base exception class for all retryable application errors.
|
30
|
+
"""
|
@@ -0,0 +1,51 @@
|
|
1
|
+
"""
|
2
|
+
Input reader exceptions.
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Copyright (c) 2023. 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 FatalInputReaderError(FatalNexusError):
|
27
|
+
"""
|
28
|
+
Input reader exception that shuts down the Nexus.
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self, failed_reader: str, underlying: BaseException):
|
32
|
+
super().__init__()
|
33
|
+
self.with_traceback(underlying.__traceback__)
|
34
|
+
self._failed_reader = failed_reader
|
35
|
+
|
36
|
+
def __str__(self) -> str:
|
37
|
+
return f"Reader for alias '{self._failed_reader}' failed to fetch the data and this operation cannot be retried. Review traceback for more information"
|
38
|
+
|
39
|
+
|
40
|
+
class TransientInputReaderError(TransientNexusError):
|
41
|
+
"""
|
42
|
+
Input reader exception that will initiate a retry in Crystal.
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(self, failed_reader: str, underlying: BaseException):
|
46
|
+
super().__init__()
|
47
|
+
self.with_traceback(underlying.__traceback__)
|
48
|
+
self._failed_reader = failed_reader
|
49
|
+
|
50
|
+
def __str__(self) -> str:
|
51
|
+
return f"Reader for alias '{self._failed_reader}' failed to fetch the data. This error can be resolved by retrying the operation"
|