bw-essentials-core 0.1.5__py3-none-any.whl → 0.1.33__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.
- bw_essentials/constants/services.py +12 -0
- bw_essentials/email_client/email_client.py +31 -18
- bw_essentials/services/api_client.py +49 -0
- bw_essentials/services/broker.py +30 -4
- bw_essentials/services/compliance.py +78 -0
- bw_essentials/services/job_scheduler.py +62 -0
- bw_essentials/services/market_pricer.py +71 -17
- bw_essentials/services/master_data.py +49 -0
- bw_essentials/services/model_portfolio_reporting.py +30 -1
- bw_essentials/services/notification.py +19 -15
- bw_essentials/services/payment.py +84 -0
- bw_essentials/services/portfolio_catalogue.py +169 -1
- bw_essentials/services/portfolio_content.py +24 -0
- bw_essentials/services/trade_placement.py +262 -9
- bw_essentials/services/user_app.py +23 -4
- bw_essentials/services/user_portfolio.py +562 -1
- bw_essentials/services/user_portfolio_reporting.py +37 -1
- {bw_essentials_core-0.1.5.dist-info → bw_essentials_core-0.1.33.dist-info}/METADATA +1 -1
- bw_essentials_core-0.1.33.dist-info/RECORD +32 -0
- bw_essentials_core-0.1.5.dist-info/RECORD +0 -29
- {bw_essentials_core-0.1.5.dist-info → bw_essentials_core-0.1.33.dist-info}/WHEEL +0 -0
- {bw_essentials_core-0.1.5.dist-info → bw_essentials_core-0.1.33.dist-info}/top_level.txt +0 -0
|
@@ -32,7 +32,7 @@ class NotificationService(ApiClient):
|
|
|
32
32
|
"email": "email"
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
def _whatsapp(self, title, template, platform, params, to, user_id) ->
|
|
35
|
+
def _whatsapp(self, title, template, platform, params, to, user_id) -> dict:
|
|
36
36
|
"""
|
|
37
37
|
Sends a WhatsApp notification.
|
|
38
38
|
|
|
@@ -46,7 +46,7 @@ class NotificationService(ApiClient):
|
|
|
46
46
|
- params: Parameters for the notification message.
|
|
47
47
|
|
|
48
48
|
Returns:
|
|
49
|
-
|
|
49
|
+
returns the response data from the notification service.
|
|
50
50
|
"""
|
|
51
51
|
logger.info(f"In - whatsapp {user_id =}, {to =}, "
|
|
52
52
|
f"{title =}, {template =}, {platform =}, {params =}")
|
|
@@ -66,8 +66,9 @@ class NotificationService(ApiClient):
|
|
|
66
66
|
self.set_headers(headers)
|
|
67
67
|
resp_data = self._post(url=self.base_url, endpoint=self.urls.get('whatsapp'), data=payload)
|
|
68
68
|
logger.info(f"Whatsapp response {resp_data =}")
|
|
69
|
+
return resp_data
|
|
69
70
|
|
|
70
|
-
def send_whatsapp(self, template, title, params, to, user_id) ->
|
|
71
|
+
def send_whatsapp(self, template, title, params, to, user_id) -> dict:
|
|
71
72
|
"""
|
|
72
73
|
|
|
73
74
|
Args:
|
|
@@ -78,17 +79,18 @@ class NotificationService(ApiClient):
|
|
|
78
79
|
user_id (str): The ID of the user receiving the notification.
|
|
79
80
|
|
|
80
81
|
Returns:
|
|
81
|
-
|
|
82
|
+
Returns the response of calling function
|
|
82
83
|
"""
|
|
83
84
|
logger.info(f"In - send_whatsapp_notification {user_id =} {title = } {params = } {to = }")
|
|
84
|
-
self._whatsapp(title=title,
|
|
85
|
+
response = self._whatsapp(title=title,
|
|
85
86
|
template=template,
|
|
86
87
|
platform=self._get_env_var(NotificationService.PLATFORM),
|
|
87
88
|
params=params,
|
|
88
89
|
to=to,
|
|
89
90
|
user_id=user_id)
|
|
91
|
+
return response
|
|
90
92
|
|
|
91
|
-
def _email(self, title: str, content: str, platform: str, to: str, user_id: str) ->
|
|
93
|
+
def _email(self, title: str, content: str, platform: str, to: str, user_id: str) -> dict:
|
|
92
94
|
"""
|
|
93
95
|
Sends an email notification using the internal notification service.
|
|
94
96
|
|
|
@@ -100,7 +102,7 @@ class NotificationService(ApiClient):
|
|
|
100
102
|
user_id (str): The ID of the user for tracking or logging purposes.
|
|
101
103
|
|
|
102
104
|
Returns:
|
|
103
|
-
|
|
105
|
+
returns the response data from the notification service.
|
|
104
106
|
|
|
105
107
|
"""
|
|
106
108
|
logger.info(f"In - email {user_id =}, {to =}, {title =}, {platform =}, {content =}")
|
|
@@ -119,8 +121,9 @@ class NotificationService(ApiClient):
|
|
|
119
121
|
self.set_headers(headers)
|
|
120
122
|
resp_data = self._post(url=self.base_url, endpoint=self.urls.get('email'), json=payload)
|
|
121
123
|
logger.info(f"Email response {resp_data =}")
|
|
124
|
+
return resp_data
|
|
122
125
|
|
|
123
|
-
def send_email(self, title: str, content: str, to: str, user_id: str) ->
|
|
126
|
+
def send_email(self, title: str, content: str, to: str, user_id: str) -> dict:
|
|
124
127
|
"""
|
|
125
128
|
Sends an email notification to the specified recipient.
|
|
126
129
|
|
|
@@ -131,13 +134,14 @@ class NotificationService(ApiClient):
|
|
|
131
134
|
user_id (str): The ID of the user associated with the notification.
|
|
132
135
|
|
|
133
136
|
Returns:
|
|
134
|
-
|
|
137
|
+
return the response of calling function
|
|
135
138
|
"""
|
|
136
139
|
logger.info(f"In - send_email {user_id =}, {title =}, {to =}")
|
|
137
|
-
self._email(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
response = self._email(
|
|
141
|
+
title=title,
|
|
142
|
+
content=content,
|
|
143
|
+
platform=self._get_env_var(NotificationService.PLATFORM),
|
|
144
|
+
to=to,
|
|
145
|
+
user_id=user_id
|
|
143
146
|
)
|
|
147
|
+
return response
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from bw_essentials.services.api_client import ApiClient
|
|
3
|
+
from bw_essentials.constants.services import Services
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Payment(ApiClient):
|
|
9
|
+
|
|
10
|
+
def __init__(self, service_user: str):
|
|
11
|
+
super().__init__(service_user)
|
|
12
|
+
self.urls = {
|
|
13
|
+
"get_subscription": "user-subscription",
|
|
14
|
+
"update_otp_facilitator_code": "order",
|
|
15
|
+
"update_subscription_facilitator_code": "user-subscription-update-data"
|
|
16
|
+
}
|
|
17
|
+
self.name = Services.PAYMENT.value
|
|
18
|
+
self.base_url = self.get_base_url(Services.PAYMENT.value)
|
|
19
|
+
|
|
20
|
+
def get_subscription(self, user_id):
|
|
21
|
+
"""
|
|
22
|
+
Fetch subscription details for a given user.
|
|
23
|
+
|
|
24
|
+
This method constructs the request URL using the configured base URL
|
|
25
|
+
and the `get_subscription` endpoint. It sends a GET request with
|
|
26
|
+
the provided `user_id` as a query parameter, logs the process,
|
|
27
|
+
and returns the subscription data payload.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
user_id (str | int): Unique identifier of the user whose
|
|
31
|
+
subscription details need to be fetched.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict | None: A dictionary containing the subscription data if
|
|
35
|
+
available, otherwise `None`.
|
|
36
|
+
"""
|
|
37
|
+
logger.info(f"Received request to get subscription with {user_id =}")
|
|
38
|
+
url = f"{self.base_url}"
|
|
39
|
+
subscription = self._get(url=url, endpoint=self.urls.get('get_subscription'), params={'userId': user_id})
|
|
40
|
+
logger.info(f"Successfully fetched subscription data. {subscription =}")
|
|
41
|
+
return subscription.get('data')
|
|
42
|
+
|
|
43
|
+
def update_otp_facilitator_code(self, payment_reference_id: str, analyst_id: str, payload: dict):
|
|
44
|
+
"""
|
|
45
|
+
Updates the facilitator code on the Payment Service for a given payment_reference_id.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
payment_reference_id (str): The payment reference ID (order ID)
|
|
49
|
+
analyst_id (str): Analyst UUID
|
|
50
|
+
payload (dict): Body containing facilitatorCode
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
dict: Response data
|
|
54
|
+
"""
|
|
55
|
+
logger.info(f"In - update_facilitator_code payment_reference_id={payment_reference_id}, "
|
|
56
|
+
f""f"analyst_id={analyst_id}, payload={payload}")
|
|
57
|
+
response = self._put(
|
|
58
|
+
url=f"{self.base_url}",
|
|
59
|
+
endpoint=f"{self.urls.get('update_otp_facilitator_code')}/{payment_reference_id}",
|
|
60
|
+
json=payload,
|
|
61
|
+
params={"analyst": analyst_id},
|
|
62
|
+
)
|
|
63
|
+
logger.info(f"Out - update_facilitator_code response={response}")
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
def update_subscription_facilitator_code(self, payment_reference_id: str, payload: dict):
|
|
67
|
+
"""
|
|
68
|
+
Updates the facilitator code on the Payment Service for a given payment_reference_id.
|
|
69
|
+
Args:
|
|
70
|
+
payment_reference_id (str): The payment reference ID (subscription ID)
|
|
71
|
+
payload (dict): Body containing facilitatorCode and userId
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
dict: Response data
|
|
75
|
+
"""
|
|
76
|
+
logger.info(f"In - update_subscription_facilitator_code payment_reference_id={payment_reference_id}, "
|
|
77
|
+
f"payload={payload}")
|
|
78
|
+
response = self._put(
|
|
79
|
+
url=f"{self.base_url}",
|
|
80
|
+
endpoint=f"{self.urls.get('update_subscription_facilitator_code')}/{payment_reference_id}",
|
|
81
|
+
json=payload
|
|
82
|
+
)
|
|
83
|
+
logger.info(f"Out - update_subscription_facilitator_code response={response}")
|
|
84
|
+
return response
|
|
@@ -48,7 +48,7 @@ Example
|
|
|
48
48
|
import logging
|
|
49
49
|
from typing import Optional, Dict, Any
|
|
50
50
|
|
|
51
|
-
from bw_essentials.constants.services import Services
|
|
51
|
+
from bw_essentials.constants.services import Services, PortfolioStatus
|
|
52
52
|
from bw_essentials.services.api_client import ApiClient
|
|
53
53
|
|
|
54
54
|
logger = logging.getLogger(__name__)
|
|
@@ -78,6 +78,10 @@ class PortfolioCatalogue(ApiClient):
|
|
|
78
78
|
self.name = Services.PORTFOLIO_CATALOGUE.value
|
|
79
79
|
self.urls = {
|
|
80
80
|
"rebalance": "rebalance",
|
|
81
|
+
"get_rebalance": "rebalance",
|
|
82
|
+
"subscriptions_by_ids": "subscription-plan/list",
|
|
83
|
+
"otp_subscriptions_by_ids": "one-time-plans/list",
|
|
84
|
+
"option_recommendations": "options/recommendations"
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
def create_rebalance(self, json: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
@@ -102,3 +106,167 @@ class PortfolioCatalogue(ApiClient):
|
|
|
102
106
|
)
|
|
103
107
|
logger.info("%s", data)
|
|
104
108
|
return data
|
|
109
|
+
|
|
110
|
+
def rebalance(self, portfolio_id, status):
|
|
111
|
+
"""Fetch the latest rebalance record for a portfolio.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
portfolio_id : str
|
|
116
|
+
Portfolio identifier.
|
|
117
|
+
status : str
|
|
118
|
+
Portfolio status filter.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
dict
|
|
123
|
+
The first record from the service response's 'data' list.
|
|
124
|
+
"""
|
|
125
|
+
logger.info("In - rebalance portfolio_id=%s status=%s", portfolio_id, status)
|
|
126
|
+
data = self._get(
|
|
127
|
+
url=self.base_url,
|
|
128
|
+
endpoint=self.urls["get_rebalance"],
|
|
129
|
+
params={'portfolioId': portfolio_id, 'status': status}
|
|
130
|
+
)
|
|
131
|
+
logger.info("rebalance response: %s", data)
|
|
132
|
+
return data.get('data')[0]
|
|
133
|
+
|
|
134
|
+
def get_rebalance_due_date(self, portfolio_id, status):
|
|
135
|
+
"""Return the next rebalance date for an active portfolio, else None.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
portfolio_id : str
|
|
140
|
+
Portfolio identifier.
|
|
141
|
+
status : str
|
|
142
|
+
Portfolio status.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
str | None
|
|
147
|
+
The next rebalance date string if available; otherwise None.
|
|
148
|
+
"""
|
|
149
|
+
logger.info(
|
|
150
|
+
"In - get_rebalance_due_date portfolio_id=%s status=%s",
|
|
151
|
+
portfolio_id,
|
|
152
|
+
status,
|
|
153
|
+
)
|
|
154
|
+
if status in PortfolioStatus.ACTIVE_FOR_SUBSCRIBED_USER.value:
|
|
155
|
+
rebalance_history = self.rebalance(portfolio_id, status)
|
|
156
|
+
next_rebalance_date = (
|
|
157
|
+
rebalance_history.get('nextRebalanceDate') if rebalance_history else None
|
|
158
|
+
)
|
|
159
|
+
logger.info("nextRebalanceDate=%s", next_rebalance_date)
|
|
160
|
+
return next_rebalance_date
|
|
161
|
+
logger.info("get_rebalance_due_date not applicable for status=%s", status)
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def get_subscriptions_by_ids(self, plans_ids: list):
|
|
165
|
+
"""
|
|
166
|
+
Fetch multiple subscriptions by their plan IDs.
|
|
167
|
+
|
|
168
|
+
This method constructs the request URL using the configured base URL
|
|
169
|
+
and the `subscriptions_by_ids` endpoint. It sends a POST request with
|
|
170
|
+
the given list of `plans_ids` in the request body, logs the process,
|
|
171
|
+
and returns the subscription data.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
plans_ids (list[str] | list[int]): A list of subscription plan IDs
|
|
175
|
+
to fetch details for.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
list[dict]: A list of subscription data dictionaries if available,
|
|
179
|
+
otherwise an empty list.
|
|
180
|
+
"""
|
|
181
|
+
logger.info(f"In get_subscriptions_by_ids {plans_ids =}")
|
|
182
|
+
url = f"{self.base_url}"
|
|
183
|
+
payload = {
|
|
184
|
+
"planId": plans_ids
|
|
185
|
+
}
|
|
186
|
+
response = self._post(url=url, endpoint=self.urls.get('subscriptions_by_ids'), json=payload)
|
|
187
|
+
logger.info(f"Successfully fetched subscriptions by ids data. {response =}")
|
|
188
|
+
return response.get('data', [])
|
|
189
|
+
|
|
190
|
+
def get_otp_subscriptions_by_ids(self, plans_ids: list):
|
|
191
|
+
"""
|
|
192
|
+
Fetch OTP subscription details for multiple plan IDs.
|
|
193
|
+
|
|
194
|
+
This method sends a POST request to the `otp_subscriptions_by_ids`
|
|
195
|
+
endpoint with the given list of plan IDs in the request body.
|
|
196
|
+
It logs the request and response flow and returns the extracted
|
|
197
|
+
subscription data.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
plans_ids (list[str] | list[int]): A list of OTP subscription plan IDs
|
|
201
|
+
to retrieve details for.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
list[dict]: A list of OTP subscription data dictionaries if available,
|
|
205
|
+
otherwise an empty list.
|
|
206
|
+
"""
|
|
207
|
+
logger.info(f"In get_otp_subscriptions_by_ids {plans_ids =}")
|
|
208
|
+
url = f"{self.base_url}"
|
|
209
|
+
payload = {
|
|
210
|
+
"id": plans_ids
|
|
211
|
+
}
|
|
212
|
+
response = self._post(url=url, endpoint=self.urls.get('otp_subscriptions_by_ids'), json=payload)
|
|
213
|
+
logger.info(f"Successfully fetched otp subscriptions by ids data. {response =}")
|
|
214
|
+
return response.get('data', [])
|
|
215
|
+
|
|
216
|
+
def get_options_recommendations(self,
|
|
217
|
+
strike_price_gt: float = None,
|
|
218
|
+
segment_in: str = None,
|
|
219
|
+
date_gte: str = None,
|
|
220
|
+
limit: int = None,
|
|
221
|
+
broker: str = None,
|
|
222
|
+
underlying_symbol: str = None,
|
|
223
|
+
status: str = None,
|
|
224
|
+
isin_code: str = None):
|
|
225
|
+
"""
|
|
226
|
+
Fetch options trading recommendations based on filtering parameters.
|
|
227
|
+
|
|
228
|
+
This method sends a GET request to the Portfolio-Catalogue service's
|
|
229
|
+
`/options/recommendations` endpoint with optional query parameters
|
|
230
|
+
for filtering by strike price, trading segment, date, broker, and limit.
|
|
231
|
+
|
|
232
|
+
Endpoint:
|
|
233
|
+
GET /options/recommendations
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
strike_price_gt (float, optional): Fetch recommendations where strike price > given value.
|
|
237
|
+
segment_in (str, optional): Trading segment to filter by (e.g., 'fno').
|
|
238
|
+
date_gte (str, optional): Minimum date (ISO format, e.g., '2025-10-28').
|
|
239
|
+
limit (int, optional): Limit the number of records returned.
|
|
240
|
+
broker (str, optional): Broker name to filter recommendations (e.g., 'zerodha').
|
|
241
|
+
underlying_symbol (str, optional): Trading symbol to filter recommendations (e.g., 'RELIANCE').
|
|
242
|
+
status (str, optional): Status of the recommendation. (e.g., 'active', 'inactive')
|
|
243
|
+
isin_code (str, optional): ISIN code to filter recommendations).
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
list[dict]: A list of recommendation records if available, otherwise an empty list.
|
|
247
|
+
"""
|
|
248
|
+
logger.info(
|
|
249
|
+
"In - get_options_recommendations strike_price_gt=%s, segment_in=%s, date_gte=%s, limit=%s, "
|
|
250
|
+
"broker=%s, underlying_symbol=%s, status=%s, isin_code=%s",
|
|
251
|
+
strike_price_gt, segment_in, date_gte, limit, broker, underlying_symbol, status, isin_code
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
params = {k: v for k, v in {
|
|
255
|
+
"strike_price__gt": strike_price_gt,
|
|
256
|
+
"segment__in": segment_in,
|
|
257
|
+
"date__gte": date_gte,
|
|
258
|
+
"limit": limit,
|
|
259
|
+
"broker": broker,
|
|
260
|
+
"underlying_symbol": underlying_symbol,
|
|
261
|
+
"status": status,
|
|
262
|
+
"isin_code": isin_code
|
|
263
|
+
}.items() if v is not None}
|
|
264
|
+
|
|
265
|
+
response = self._get(
|
|
266
|
+
url=self.base_url,
|
|
267
|
+
endpoint=self.urls.get('option_recommendations'),
|
|
268
|
+
params=params
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
logger.info(f"Successfully fetched options recommendations. {response =}")
|
|
272
|
+
return response.get("data", [])
|
|
@@ -43,6 +43,7 @@ class PortfolioContent(ApiClient):
|
|
|
43
43
|
self.name = Services.PORTFOLIO_CONTENT.value
|
|
44
44
|
self.urls = {
|
|
45
45
|
"portfolio": "portfolio",
|
|
46
|
+
"portfolio-details": "portfolio-details"
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
def get_portfolio_details(self, portfolio_id: str) -> Optional[Dict[str, Any]]:
|
|
@@ -67,3 +68,26 @@ class PortfolioContent(ApiClient):
|
|
|
67
68
|
)
|
|
68
69
|
logger.info("%s", data)
|
|
69
70
|
return data
|
|
71
|
+
|
|
72
|
+
def get_portfolio_details_by_ids(self, portfolio_ids: list):
|
|
73
|
+
"""
|
|
74
|
+
Fetch portfolio details from the external service for the given portfolio IDs.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
portfolio_ids (list): A list of portfolio IDs to fetch details for.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
dict: The response data containing portfolio details from the external service.
|
|
81
|
+
"""
|
|
82
|
+
logger.info("Fetching portfolio details for %d portfolio IDs: %s", len(portfolio_ids), portfolio_ids)
|
|
83
|
+
|
|
84
|
+
payload = {
|
|
85
|
+
"portfolioIds": portfolio_ids
|
|
86
|
+
}
|
|
87
|
+
response = self._post(
|
|
88
|
+
url=self.base_url,
|
|
89
|
+
endpoint=self.urls['portfolio-details'],
|
|
90
|
+
json=payload
|
|
91
|
+
)
|
|
92
|
+
logger.info(f"Successfully fetched portfolio details by ids. {response=}")
|
|
93
|
+
return response.get('data', [])
|