bw-essentials-core 0.1.3__tar.gz → 0.1.42__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.
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/PKG-INFO +1 -1
- bw_essentials_core-0.1.42/bw_essentials/EventNotification/EventBridgePublisher.py +106 -0
- bw_essentials_core-0.1.42/bw_essentials/EventNotification/NotificationType.py +314 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/constants/services.py +15 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/email_client/email_client.py +31 -18
- bw_essentials_core-0.1.42/bw_essentials/services/__init__.py +0 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/services/api_client.py +50 -1
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/services/broker.py +61 -4
- bw_essentials_core-0.1.42/bw_essentials/services/compliance.py +78 -0
- bw_essentials_core-0.1.42/bw_essentials/services/job_scheduler.py +62 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/services/market_pricer.py +71 -17
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/services/master_data.py +49 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/services/model_portfolio_reporting.py +30 -1
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/services/notification.py +19 -15
- bw_essentials_core-0.1.42/bw_essentials/services/payment.py +84 -0
- bw_essentials_core-0.1.42/bw_essentials/services/portfolio_catalogue.py +272 -0
- bw_essentials_core-0.1.42/bw_essentials/services/portfolio_content.py +93 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/services/trade_placement.py +262 -9
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/services/user_app.py +87 -5
- bw_essentials_core-0.1.42/bw_essentials/services/user_portfolio.py +1148 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/services/user_portfolio_reporting.py +37 -1
- bw_essentials_core-0.1.42/bw_essentials/services/user_referrals.py +251 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials_core.egg-info/PKG-INFO +1 -1
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials_core.egg-info/SOURCES.txt +9 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/setup.py +1 -1
- bw_essentials_core-0.1.3/bw_essentials/services/user_portfolio.py +0 -406
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/README.md +0 -0
- {bw_essentials_core-0.1.3/bw_essentials → bw_essentials_core-0.1.42/bw_essentials/EventNotification}/__init__.py +0 -0
- {bw_essentials_core-0.1.3/bw_essentials/constants → bw_essentials_core-0.1.42/bw_essentials}/__init__.py +0 -0
- {bw_essentials_core-0.1.3/bw_essentials/data_loch → bw_essentials_core-0.1.42/bw_essentials/constants}/__init__.py +0 -0
- {bw_essentials_core-0.1.3/bw_essentials/email_client → bw_essentials_core-0.1.42/bw_essentials/data_loch}/__init__.py +0 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/data_loch/data_loch.py +0 -0
- {bw_essentials_core-0.1.3/bw_essentials/notifications → bw_essentials_core-0.1.42/bw_essentials/email_client}/__init__.py +0 -0
- {bw_essentials_core-0.1.3/bw_essentials/s3_utils → bw_essentials_core-0.1.42/bw_essentials/notifications}/__init__.py +0 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/notifications/teams_notification_schemas.py +0 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/notifications/teams_notifications.py +0 -0
- {bw_essentials_core-0.1.3/bw_essentials/services → bw_essentials_core-0.1.42/bw_essentials/s3_utils}/__init__.py +0 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/s3_utils/s3_utils.py +0 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials_core.egg-info/dependency_links.txt +0 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials_core.egg-info/requires.txt +0 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials_core.egg-info/top_level.txt +0 -0
- {bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/setup.cfg +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event notification module for publishing events to AWS EventBridge.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to send events to AWS EventBridge for
|
|
5
|
+
asynchronous processing and integration with other AWS services.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from importlib.util import spec_from_file_location, module_from_spec
|
|
12
|
+
from ..notifications.teams_notifications import Notifications as ErrorNotifications
|
|
13
|
+
|
|
14
|
+
import boto3
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EventBridgePublisher(object):
|
|
20
|
+
"""
|
|
21
|
+
Handles publishing events to AWS EventBridge.
|
|
22
|
+
|
|
23
|
+
This class manages AWS credentials from environment configuration and provides
|
|
24
|
+
methods to publish events to an AWS EventBridge event bus for downstream processing.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self.region_name = self._get_env_var("AWS_REGION")
|
|
29
|
+
self.aws_access_key_id = self._get_env_var("AWS_ACCESS_KEY_ID")
|
|
30
|
+
self.aws_secret_access_key = self._get_env_var("AWS_SECRET_ACCESS_KEY")
|
|
31
|
+
|
|
32
|
+
def _get_env_var(self, key: str) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Fetch a required variable from bw_config.py located in the root directory.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
FileNotFoundError: If bw_config.py is not found.
|
|
38
|
+
AttributeError: If the requested key is not defined in the config.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
str: The value of the config variable.
|
|
42
|
+
"""
|
|
43
|
+
config_path = os.path.join(os.getcwd(), "bw_config.py")
|
|
44
|
+
|
|
45
|
+
if not os.path.exists(config_path):
|
|
46
|
+
raise FileNotFoundError("`bw_config.py` file not found in the root directory. "
|
|
47
|
+
"Please ensure the config file exists.")
|
|
48
|
+
|
|
49
|
+
spec = spec_from_file_location("bw_config", config_path)
|
|
50
|
+
bw_config = module_from_spec(spec)
|
|
51
|
+
sys.modules["bw_config"] = bw_config
|
|
52
|
+
spec.loader.exec_module(bw_config)
|
|
53
|
+
|
|
54
|
+
if not hasattr(bw_config, key):
|
|
55
|
+
raise AttributeError(f"`{key}` not found in bw_config.py. Please define it in the config.")
|
|
56
|
+
|
|
57
|
+
return getattr(bw_config, key)
|
|
58
|
+
|
|
59
|
+
def send_event_notification(self, event, source='user-event'):
|
|
60
|
+
"""
|
|
61
|
+
Publish an event to AWS EventBridge.
|
|
62
|
+
|
|
63
|
+
This method sends an event to the configured AWS EventBridge event bus.
|
|
64
|
+
If an error occurs during publishing, it sends an error notification
|
|
65
|
+
via Teams.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
event (dict): The event payload to publish. Will be JSON-serialized.
|
|
69
|
+
source (str, optional): The event source identifier. Defaults to 'user-event'.
|
|
70
|
+
|
|
71
|
+
Note:
|
|
72
|
+
- Requires AWS_EVENT_BUS_ARN configuration variable
|
|
73
|
+
- Errors are logged and sent as Teams notifications
|
|
74
|
+
- AWS client connection is properly closed after use
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
logger.info(f"In publish event")
|
|
78
|
+
# Create AWS EventBridge client with configured credentials
|
|
79
|
+
client = boto3.client("events",
|
|
80
|
+
region_name=self.region_name,
|
|
81
|
+
aws_access_key_id=self.aws_access_key_id,
|
|
82
|
+
aws_secret_access_key=self.aws_secret_access_key)
|
|
83
|
+
event_bus_arn = self._get_env_var("AWS_EVENT_BUS_ARN")
|
|
84
|
+
|
|
85
|
+
if not event_bus_arn:
|
|
86
|
+
raise ValueError("EventBusName is empty — invalid configuration.")
|
|
87
|
+
|
|
88
|
+
# Publish event to EventBridge
|
|
89
|
+
response = client.put_events(
|
|
90
|
+
Entries=[
|
|
91
|
+
{
|
|
92
|
+
'Source': source,
|
|
93
|
+
'DetailType': 'user-preferences',
|
|
94
|
+
'Detail': json.dumps(event),
|
|
95
|
+
'EventBusName': event_bus_arn
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
)
|
|
99
|
+
client.close()
|
|
100
|
+
logger.info(f"Event published - {response = }")
|
|
101
|
+
except Exception as e:
|
|
102
|
+
# Notify error via Teams on publishing failure
|
|
103
|
+
error_notifications = ErrorNotifications('BW-essentials_error')
|
|
104
|
+
error_notifications.notify_error(
|
|
105
|
+
message="error via BW essentials while sending a event notification" + str(e),
|
|
106
|
+
summary=f"event publish error {event = }")
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Notification type definitions for different event types.
|
|
3
|
+
|
|
4
|
+
This module defines structured notification types for various business events
|
|
5
|
+
such as subscriptions, investments, withdrawals, and alerts.
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NotificationType:
|
|
13
|
+
"""
|
|
14
|
+
Defines all available notification event types and their structures.
|
|
15
|
+
|
|
16
|
+
This class provides methods to create standardized event payloads for
|
|
17
|
+
different types of user actions and system events. Each method returns
|
|
18
|
+
a dictionary with a consistent schema including version, broker info,
|
|
19
|
+
event name, and event-specific data.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.profit_target_event_name = "profit_target"
|
|
24
|
+
self.stop_loss_alert_event_name = "stop_loss_alert"
|
|
25
|
+
|
|
26
|
+
def available_notifications(self):
|
|
27
|
+
"""
|
|
28
|
+
Get a list of all available notification methods.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
list: Names of all public notification methods in this class.
|
|
32
|
+
"""
|
|
33
|
+
return [m for m in dir(self) if
|
|
34
|
+
callable(getattr(self, m)) and m != 'available_notifications' and (not m.startswith('_'))]
|
|
35
|
+
|
|
36
|
+
def subscription_successful(self,
|
|
37
|
+
user_id: str,
|
|
38
|
+
phone_number: str,
|
|
39
|
+
basket_name: str,
|
|
40
|
+
basket_fee: float,
|
|
41
|
+
transaction_amount: float,
|
|
42
|
+
start_date: str,
|
|
43
|
+
expiry_date: str,
|
|
44
|
+
broker_name: str,
|
|
45
|
+
product: str,
|
|
46
|
+
platform: str):
|
|
47
|
+
"""
|
|
48
|
+
Create a subscription successful event notification.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
user_id (str): The ID of the user who subscribed.
|
|
52
|
+
phone_number (str): User's phone number.
|
|
53
|
+
basket_name (str): Name of the basket subscribed to.
|
|
54
|
+
basket_fee (float): Fee charged for the basket.
|
|
55
|
+
transaction_amount (float): Total transaction amount.
|
|
56
|
+
start_date (str): Subscription start date.
|
|
57
|
+
expiry_date (str): Subscription expiry date.
|
|
58
|
+
broker_name (str): Name of the broker handling the subscription.
|
|
59
|
+
product (str): Product identifier.
|
|
60
|
+
platform (str): Platform identifier.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
dict: Structured event payload with subscription details.
|
|
64
|
+
"""
|
|
65
|
+
result = {"version": "2",
|
|
66
|
+
"broker": broker_name,
|
|
67
|
+
"event_name": "subscription_successful",
|
|
68
|
+
"user_id": user_id,
|
|
69
|
+
"product": product,
|
|
70
|
+
"platform": platform,
|
|
71
|
+
"data": {
|
|
72
|
+
"phone_number": phone_number,
|
|
73
|
+
"basket_name": basket_name,
|
|
74
|
+
"basket_fee": basket_fee,
|
|
75
|
+
"transaction_amount": transaction_amount,
|
|
76
|
+
"start_date": start_date,
|
|
77
|
+
"expiry_date": expiry_date,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
def subscription_cancelled(self,
|
|
83
|
+
user_id: str,
|
|
84
|
+
broker_name: str,
|
|
85
|
+
product: str,
|
|
86
|
+
platform: str
|
|
87
|
+
):
|
|
88
|
+
"""
|
|
89
|
+
Create a subscription cancelled event notification.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
user_id (str): The ID of the user whose subscription was cancelled.
|
|
93
|
+
broker_name (str): Name of the broker handling the subscription.
|
|
94
|
+
product (str): Product identifier.
|
|
95
|
+
platform (str): Platform identifier.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
dict: Structured event payload indicating subscription cancellation.
|
|
99
|
+
"""
|
|
100
|
+
result = {
|
|
101
|
+
"version": "2",
|
|
102
|
+
"broker": broker_name,
|
|
103
|
+
"event_name": "subscription_cancelled",
|
|
104
|
+
"user_id": user_id,
|
|
105
|
+
"product": product,
|
|
106
|
+
"platform": platform,
|
|
107
|
+
"data": {}
|
|
108
|
+
}
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
def investment_successful(self,
|
|
112
|
+
user_id: str,
|
|
113
|
+
phone_number: str,
|
|
114
|
+
basket_name: str,
|
|
115
|
+
basket_fee: float,
|
|
116
|
+
transaction_amount: float,
|
|
117
|
+
investment_amount: float,
|
|
118
|
+
broker_name: str,
|
|
119
|
+
product: str,
|
|
120
|
+
platform: str
|
|
121
|
+
):
|
|
122
|
+
"""
|
|
123
|
+
Create an investment successful event notification.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
user_id (str): The ID of the user who made the investment.
|
|
127
|
+
phone_number (str): User's phone number.
|
|
128
|
+
basket_name (str): Name of the basket invested in.
|
|
129
|
+
basket_fee (float): Fee charged for the basket.
|
|
130
|
+
transaction_amount (float): Total transaction amount.
|
|
131
|
+
investment_amount (float): The amount invested.
|
|
132
|
+
broker_name (str): Name of the broker handling the investment.
|
|
133
|
+
product (str): Product identifier.
|
|
134
|
+
platform (str): Platform identifier.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
dict: Structured event payload with investment details.
|
|
138
|
+
"""
|
|
139
|
+
result = {
|
|
140
|
+
"version": "2",
|
|
141
|
+
"broker": broker_name,
|
|
142
|
+
"event_name": "investment_successful",
|
|
143
|
+
"user_id": user_id,
|
|
144
|
+
"product": product,
|
|
145
|
+
"platform": platform,
|
|
146
|
+
"data": {
|
|
147
|
+
"phone_number": phone_number,
|
|
148
|
+
"basket_name": basket_name,
|
|
149
|
+
"basket_fee": basket_fee,
|
|
150
|
+
"transaction_amount": transaction_amount,
|
|
151
|
+
"investment_amount": investment_amount,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
def withdrawal_successful(self,
|
|
157
|
+
user_id: str,
|
|
158
|
+
phone_number: str,
|
|
159
|
+
basket_name: str,
|
|
160
|
+
sell_trade_value: float,
|
|
161
|
+
broker_name: str,
|
|
162
|
+
product: str,
|
|
163
|
+
platform: str
|
|
164
|
+
):
|
|
165
|
+
"""
|
|
166
|
+
Create a withdrawal successful event notification.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
user_id (str): The ID of the user who made the withdrawal.
|
|
170
|
+
phone_number (str): User's phone number.
|
|
171
|
+
basket_name (str): Name of the basket withdrawn from.
|
|
172
|
+
sell_trade_value (float): The value of the sold trade.
|
|
173
|
+
broker_name (str): Name of the broker handling the withdrawal.
|
|
174
|
+
product (str): Product identifier.
|
|
175
|
+
platform (str): Platform identifier.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
dict: Structured event payload with withdrawal details.
|
|
179
|
+
"""
|
|
180
|
+
result = {
|
|
181
|
+
"version": "2",
|
|
182
|
+
"broker": broker_name,
|
|
183
|
+
"event_name": "withdrawal_successful",
|
|
184
|
+
"user_id": user_id,
|
|
185
|
+
"product": product,
|
|
186
|
+
"platform": platform,
|
|
187
|
+
"data": {
|
|
188
|
+
"sell_trade_value": sell_trade_value,
|
|
189
|
+
"phone_number": phone_number,
|
|
190
|
+
"basket_name": basket_name,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
def loss_alert_updated(self,
|
|
196
|
+
user_id: str,
|
|
197
|
+
basket_name: str,
|
|
198
|
+
start_date: str,
|
|
199
|
+
stop_loss_limit: float,
|
|
200
|
+
broker_name: str,
|
|
201
|
+
product: str,
|
|
202
|
+
platform: str
|
|
203
|
+
):
|
|
204
|
+
"""
|
|
205
|
+
Create a loss alert updated event notification.
|
|
206
|
+
|
|
207
|
+
This event is triggered when a stop-loss limit is updated for a basket.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
user_id (str): The ID of the user whose alert was updated.
|
|
211
|
+
basket_name (str): Name of the basket with the loss alert.
|
|
212
|
+
start_date (str): The start date associated with the alert.
|
|
213
|
+
stop_loss_limit (float): The stop-loss limit value.
|
|
214
|
+
broker_name (str): Name of the broker managing the alert.
|
|
215
|
+
product (str): Product identifier.
|
|
216
|
+
platform (str): Platform identifier.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
dict: Structured event payload with loss alert details.
|
|
220
|
+
"""
|
|
221
|
+
result = {
|
|
222
|
+
"version": "2",
|
|
223
|
+
"broker": broker_name,
|
|
224
|
+
"event_name": "loss_alert_updated",
|
|
225
|
+
"user_id": user_id,
|
|
226
|
+
"product": product,
|
|
227
|
+
"platform": platform,
|
|
228
|
+
"data": {
|
|
229
|
+
"start_date": start_date,
|
|
230
|
+
"stop_loss_limit": stop_loss_limit,
|
|
231
|
+
"product": product,
|
|
232
|
+
"basket_name": basket_name
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
def profit_target(
|
|
238
|
+
self,
|
|
239
|
+
user_id: str,
|
|
240
|
+
triggered_at: str,
|
|
241
|
+
target_percentage: float,
|
|
242
|
+
symbol: str,
|
|
243
|
+
broker_name: str,
|
|
244
|
+
product: str,
|
|
245
|
+
platform: str,
|
|
246
|
+
) -> dict:
|
|
247
|
+
"""
|
|
248
|
+
Build Profit Target notification payload.
|
|
249
|
+
"""
|
|
250
|
+
logger.info(
|
|
251
|
+
"ProfitTarget payload build started | user=%s | symbol=%s",
|
|
252
|
+
user_id,
|
|
253
|
+
symbol,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
result = {
|
|
257
|
+
"version": "2",
|
|
258
|
+
"broker": broker_name,
|
|
259
|
+
"event_name": self.profit_target_event_name,
|
|
260
|
+
"user_id": user_id,
|
|
261
|
+
"product": product,
|
|
262
|
+
"platform": platform,
|
|
263
|
+
"data": {
|
|
264
|
+
"triggered_at": triggered_at,
|
|
265
|
+
"target_percentage": target_percentage,
|
|
266
|
+
"symbol": symbol,
|
|
267
|
+
"product": product,
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
logger.info(
|
|
272
|
+
"ProfitTarget payload build completed | user=%s | symbol=%s | payload=%s",
|
|
273
|
+
user_id,
|
|
274
|
+
symbol,
|
|
275
|
+
result,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
def stop_loss_alert(
|
|
281
|
+
self,
|
|
282
|
+
user_id: str,
|
|
283
|
+
triggered_at: str,
|
|
284
|
+
target_percentage: float,
|
|
285
|
+
symbol: str,
|
|
286
|
+
broker_name: str,
|
|
287
|
+
product: str,
|
|
288
|
+
platform: str,
|
|
289
|
+
) -> dict:
|
|
290
|
+
"""
|
|
291
|
+
Build Stop Loss notification payload.
|
|
292
|
+
"""
|
|
293
|
+
logger.info("StopLoss payload build started | user=%s | symbol=%s", user_id, symbol)
|
|
294
|
+
result = {
|
|
295
|
+
"version": "2",
|
|
296
|
+
"broker": broker_name,
|
|
297
|
+
"event_name": self.stop_loss_alert_event_name,
|
|
298
|
+
"user_id": user_id,
|
|
299
|
+
"product": product,
|
|
300
|
+
"platform": platform,
|
|
301
|
+
"data": {
|
|
302
|
+
"triggered_at": triggered_at,
|
|
303
|
+
"target_percentage": target_percentage,
|
|
304
|
+
"symbol": symbol,
|
|
305
|
+
"product": product,
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
logger.info("StopLoss payload build completed | user=%s | symbol=%s | payload=%s", user_id,
|
|
310
|
+
symbol,
|
|
311
|
+
result,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return result
|
|
@@ -11,6 +11,7 @@ class Services(Enum):
|
|
|
11
11
|
MARKET_PRICER = "Market_Pricer"
|
|
12
12
|
BROKER = 'Broker'
|
|
13
13
|
USER_PORTFOLIO = 'User_Portfolio'
|
|
14
|
+
FINN_USER_PORTFOLIO = 'Finn_User_Portfolio'
|
|
14
15
|
TRADE_PLACEMENT = 'Trade_Placement'
|
|
15
16
|
CONTENT = 'Portfolio_Content'
|
|
16
17
|
NOTIFICATION = 'Notification'
|
|
@@ -18,3 +19,17 @@ class Services(Enum):
|
|
|
18
19
|
PAYMENT = 'Payment'
|
|
19
20
|
MODEL_PORTFOLIO = "Model_Portfolio"
|
|
20
21
|
PROMETHEUS_USER_APP = "Prometheus_User_App"
|
|
22
|
+
PORTFOLIO_CATALOGUE = "Portfolio_Catalogue"
|
|
23
|
+
PORTFOLIO_CONTENT = "Portfolio_Content"
|
|
24
|
+
JOB_SCHEDULER = "Job_Scheduler"
|
|
25
|
+
COMPLIANCE = "Compliance"
|
|
26
|
+
|
|
27
|
+
class PortfolioStatus(Enum):
|
|
28
|
+
"""
|
|
29
|
+
Enum representing Status of Basket.
|
|
30
|
+
"""
|
|
31
|
+
ACTIVE = "active"
|
|
32
|
+
INACTIVE = "inactive"
|
|
33
|
+
PAUSED = "paused"
|
|
34
|
+
|
|
35
|
+
ACTIVE_FOR_SUBSCRIBED_USER = (ACTIVE, PAUSED)
|
{bw_essentials_core-0.1.3 → bw_essentials_core-0.1.42}/bw_essentials/email_client/email_client.py
RENAMED
|
@@ -132,7 +132,7 @@ class EmailClient:
|
|
|
132
132
|
cc_addresses: Union[str, List[str], None],
|
|
133
133
|
subject: str,
|
|
134
134
|
body: str,
|
|
135
|
-
attachment_path: str,
|
|
135
|
+
attachment_path: Optional[Union[str, List[str]]],
|
|
136
136
|
is_html: bool = True,
|
|
137
137
|
):
|
|
138
138
|
"""
|
|
@@ -143,7 +143,7 @@ class EmailClient:
|
|
|
143
143
|
cc_addresses (Union[str, List[str], None]): One or more CC addresses (optional).
|
|
144
144
|
subject (str): Email subject line.
|
|
145
145
|
body (str): Email body content.
|
|
146
|
-
attachment_path (str):
|
|
146
|
+
attachment_path (Optional[Union[str, List[str]]]): File path(s) for attachments.
|
|
147
147
|
is_html (bool): If True, body is interpreted as HTML; otherwise, plain text.
|
|
148
148
|
"""
|
|
149
149
|
logger.info(
|
|
@@ -158,7 +158,7 @@ class EmailClient:
|
|
|
158
158
|
cc_addresses: Union[str, List[str], None],
|
|
159
159
|
subject: str,
|
|
160
160
|
body: str,
|
|
161
|
-
attachment_path: Optional[str],
|
|
161
|
+
attachment_path: Optional[Union[str, List[str]]],
|
|
162
162
|
is_html: bool,
|
|
163
163
|
):
|
|
164
164
|
"""
|
|
@@ -169,7 +169,7 @@ class EmailClient:
|
|
|
169
169
|
cc_addresses (Union[str, List[str], None]): CC addresses.
|
|
170
170
|
subject (str): Subject of the email.
|
|
171
171
|
body (str): Email body content.
|
|
172
|
-
attachment_path (Optional[str]): File path for
|
|
172
|
+
attachment_path (Optional[Union[str, List[str]]]): File path(s) for attachments.
|
|
173
173
|
is_html (bool): True if the body is HTML-formatted.
|
|
174
174
|
"""
|
|
175
175
|
msg = MIMEMultipart()
|
|
@@ -180,23 +180,36 @@ class EmailClient:
|
|
|
180
180
|
|
|
181
181
|
msg.attach(MIMEText(body, 'html' if is_html else 'plain'))
|
|
182
182
|
logger.debug("Email headers and body constructed")
|
|
183
|
+
attachment_paths = []
|
|
183
184
|
|
|
184
185
|
if attachment_path:
|
|
185
|
-
if
|
|
186
|
-
|
|
186
|
+
if isinstance(attachment_path, list):
|
|
187
|
+
attachment_paths = attachment_path
|
|
187
188
|
else:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
189
|
+
attachment_paths = [attachment_path]
|
|
190
|
+
|
|
191
|
+
for path in attachment_paths:
|
|
192
|
+
if not path:
|
|
193
|
+
continue
|
|
194
|
+
if not os.path.exists(path):
|
|
195
|
+
logger.warning("Attachment file not found: %s", path)
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
with open(path, 'rb') as attachment:
|
|
200
|
+
part = MIMEBase('application', 'octet-stream')
|
|
201
|
+
part.set_payload(attachment.read())
|
|
202
|
+
encoders.encode_base64(part)
|
|
203
|
+
part.add_header(
|
|
204
|
+
'Content-Disposition',
|
|
205
|
+
'attachment',
|
|
206
|
+
filename=os.path.basename(path)
|
|
207
|
+
)
|
|
208
|
+
msg.attach(part)
|
|
209
|
+
logger.info("Attachment added: %s", os.path.basename(path))
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.exception("Failed to read attachment file: %s", path)
|
|
212
|
+
raise e
|
|
200
213
|
|
|
201
214
|
try:
|
|
202
215
|
with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
|
|
File without changes
|
|
@@ -15,6 +15,7 @@ import logging
|
|
|
15
15
|
import os
|
|
16
16
|
import sys
|
|
17
17
|
import time
|
|
18
|
+
import httpx
|
|
18
19
|
from importlib.util import spec_from_file_location, module_from_spec
|
|
19
20
|
from typing import Optional, Dict, Any
|
|
20
21
|
|
|
@@ -80,6 +81,19 @@ class ApiClient:
|
|
|
80
81
|
env_key = f"{service_name.upper()}_BASE_URL"
|
|
81
82
|
return self._get_env_var(env_key)
|
|
82
83
|
|
|
84
|
+
def get_api_key(self, service_name: str) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Resolve the service API Key for a given service name using environment variables.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
service_name (str): The logical name of the service.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
str: The resolved api key from the environment.
|
|
93
|
+
"""
|
|
94
|
+
env_key = f"{service_name.upper()}_API_KEY"
|
|
95
|
+
return self._get_env_var(env_key)
|
|
96
|
+
|
|
83
97
|
def set_tenant_id(self, tenant_id: str) -> None:
|
|
84
98
|
"""
|
|
85
99
|
Set the tenant ID in the request headers.
|
|
@@ -88,7 +102,7 @@ class ApiClient:
|
|
|
88
102
|
tenant_id (str): The tenant identifier to include in the headers.
|
|
89
103
|
"""
|
|
90
104
|
logger.info(f"Setting tenant ID: {tenant_id}")
|
|
91
|
-
self._update_headers({"
|
|
105
|
+
self._update_headers({"X-Tenant-ID": tenant_id})
|
|
92
106
|
logger.info(f"Updated headers: {self.headers}")
|
|
93
107
|
|
|
94
108
|
def set_headers(self, headers: dict) -> None:
|
|
@@ -182,6 +196,41 @@ class ApiClient:
|
|
|
182
196
|
"""
|
|
183
197
|
return self._request("get", url, endpoint, params=params)
|
|
184
198
|
|
|
199
|
+
async def _async_get(self, url: str, endpoint: str, headers: dict | None = None, params: dict | None = None):
|
|
200
|
+
"""
|
|
201
|
+
Async GET request, aligned with the sync _request() style.
|
|
202
|
+
"""
|
|
203
|
+
headers = self._update_headers(headers or {})
|
|
204
|
+
params = params or {}
|
|
205
|
+
formatted_url = f"{url.rstrip('/')}/{endpoint.lstrip('/')}"
|
|
206
|
+
|
|
207
|
+
logger.info(f"GET {formatted_url} | Headers: {headers} | Params: {params}")
|
|
208
|
+
|
|
209
|
+
start = time.time()
|
|
210
|
+
try:
|
|
211
|
+
async with httpx.AsyncClient() as client:
|
|
212
|
+
response = await client.get(formatted_url, headers=headers, params=params)
|
|
213
|
+
|
|
214
|
+
elapsed_time_ms = (time.time() - start) * 1000
|
|
215
|
+
|
|
216
|
+
# parse JSON safely
|
|
217
|
+
try:
|
|
218
|
+
json_data = response.json()
|
|
219
|
+
except ValueError:
|
|
220
|
+
logger.error(f"Non-JSON response from {formatted_url}")
|
|
221
|
+
json_data = None
|
|
222
|
+
|
|
223
|
+
self._log_response("GET", formatted_url, response.status_code, elapsed_time_ms, json_data)
|
|
224
|
+
|
|
225
|
+
response.raise_for_status()
|
|
226
|
+
return json_data
|
|
227
|
+
|
|
228
|
+
except Exception as exc:
|
|
229
|
+
elapsed_time_ms = (time.time() - start) * 1000
|
|
230
|
+
logger.error(f"GET {formatted_url} failed after {elapsed_time_ms:.2f}ms")
|
|
231
|
+
logger.exception(exc)
|
|
232
|
+
raise
|
|
233
|
+
|
|
185
234
|
def _post(
|
|
186
235
|
self,
|
|
187
236
|
url: str,
|