finbourne-sdk-utils 0.0.24__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 (52) hide show
  1. features/__init__.py +0 -0
  2. features/main.py +11 -0
  3. finbourne_sdk_utils/__init__.py +8 -0
  4. finbourne_sdk_utils/cocoon/__init__.py +34 -0
  5. finbourne_sdk_utils/cocoon/async_tools.py +94 -0
  6. finbourne_sdk_utils/cocoon/cocoon.py +1862 -0
  7. finbourne_sdk_utils/cocoon/cocoon_printer.py +455 -0
  8. finbourne_sdk_utils/cocoon/config/domain_settings.json +125 -0
  9. finbourne_sdk_utils/cocoon/config/seed_sample_data.json +36 -0
  10. finbourne_sdk_utils/cocoon/dateorcutlabel.py +198 -0
  11. finbourne_sdk_utils/cocoon/instruments.py +482 -0
  12. finbourne_sdk_utils/cocoon/properties.py +442 -0
  13. finbourne_sdk_utils/cocoon/seed_sample_data.py +137 -0
  14. finbourne_sdk_utils/cocoon/systemConfiguration.py +92 -0
  15. finbourne_sdk_utils/cocoon/transaction_type_upload.py +136 -0
  16. finbourne_sdk_utils/cocoon/utilities.py +1877 -0
  17. finbourne_sdk_utils/cocoon/validator.py +243 -0
  18. finbourne_sdk_utils/extract/__init__.py +1 -0
  19. finbourne_sdk_utils/extract/group_holdings.py +400 -0
  20. finbourne_sdk_utils/iam/__init__.py +1 -0
  21. finbourne_sdk_utils/iam/roles.py +74 -0
  22. finbourne_sdk_utils/jupyter_tools/__init__.py +2 -0
  23. finbourne_sdk_utils/jupyter_tools/hide_code_button.py +23 -0
  24. finbourne_sdk_utils/jupyter_tools/stop_execution.py +14 -0
  25. finbourne_sdk_utils/logger/LusidLogger.py +41 -0
  26. finbourne_sdk_utils/logger/__init__.py +1 -0
  27. finbourne_sdk_utils/lpt/__init__.py +0 -0
  28. finbourne_sdk_utils/lpt/back_compat.py +20 -0
  29. finbourne_sdk_utils/lpt/cash_ladder.py +191 -0
  30. finbourne_sdk_utils/lpt/connect_lusid.py +64 -0
  31. finbourne_sdk_utils/lpt/connect_none.py +5 -0
  32. finbourne_sdk_utils/lpt/connect_token.py +9 -0
  33. finbourne_sdk_utils/lpt/dfq.py +321 -0
  34. finbourne_sdk_utils/lpt/either.py +65 -0
  35. finbourne_sdk_utils/lpt/get_instruments.py +101 -0
  36. finbourne_sdk_utils/lpt/lpt.py +374 -0
  37. finbourne_sdk_utils/lpt/lse.py +188 -0
  38. finbourne_sdk_utils/lpt/map_instruments.py +164 -0
  39. finbourne_sdk_utils/lpt/pager.py +32 -0
  40. finbourne_sdk_utils/lpt/record.py +13 -0
  41. finbourne_sdk_utils/lpt/refreshing_token.py +43 -0
  42. finbourne_sdk_utils/lpt/search_instruments.py +48 -0
  43. finbourne_sdk_utils/lpt/stdargs.py +154 -0
  44. finbourne_sdk_utils/lpt/txn_config.py +128 -0
  45. finbourne_sdk_utils/lpt/txn_config_yaml.py +493 -0
  46. finbourne_sdk_utils/pandas_utils/__init__.py +0 -0
  47. finbourne_sdk_utils/pandas_utils/lusid_pandas.py +128 -0
  48. finbourne_sdk_utils-0.0.24.dist-info/LICENSE +21 -0
  49. finbourne_sdk_utils-0.0.24.dist-info/METADATA +25 -0
  50. finbourne_sdk_utils-0.0.24.dist-info/RECORD +52 -0
  51. finbourne_sdk_utils-0.0.24.dist-info/WHEEL +5 -0
  52. finbourne_sdk_utils-0.0.24.dist-info/top_level.txt +2 -0
@@ -0,0 +1,243 @@
1
+ import logging
2
+
3
+
4
+ class Validator:
5
+ def __init__(self, value, value_name):
6
+ self.value = value
7
+ self.value_name = value_name
8
+
9
+ def check_is_not_instance(self, instance_type):
10
+
11
+ if isinstance(self.value, instance_type):
12
+ raise TypeError(
13
+ f"The {self.value_name} must be of type {str(instance_type)}, you supplied '{str(type(self.value))}' instead."
14
+ )
15
+ return self
16
+
17
+ def check_allowed_value(self, allowed_values: list):
18
+ """
19
+ Checks that value exists in the provided list
20
+
21
+ Parameters
22
+ ----------
23
+ allowed_values : list
24
+ The list of allowed values
25
+
26
+ Returns
27
+ -------
28
+ Validator
29
+ """
30
+
31
+ if self.value not in allowed_values:
32
+ raise ValueError(
33
+ f"The {self.value_name} must be one of {str(allowed_values)}, you supplied '{self.value}' instead."
34
+ )
35
+ return self
36
+
37
+ def make_singular(self):
38
+ """
39
+ Makes a plural string singular
40
+
41
+ Returns
42
+ -------
43
+ Validator
44
+ """
45
+
46
+ if isinstance(self.value, str):
47
+ self.value = self.value.rstrip("s")
48
+ logging.debug(
49
+ f"The value of {self.value_name} has had any 's' characters stripped from the right to make it singular"
50
+ )
51
+ return self
52
+
53
+ def make_lower(self):
54
+ """
55
+ Makes a string lowercase
56
+
57
+ Returns
58
+ -------
59
+ Validator
60
+ """
61
+
62
+ if isinstance(self.value, str):
63
+ self.value = self.value.lower()
64
+ logging.debug(f"The value of {self.value_name} has been made lower case")
65
+ return self
66
+
67
+ def set_default_value_if_none(self, default):
68
+ """
69
+ Sets a default value if the current value is None
70
+
71
+ Parameters
72
+ ----------
73
+ default
74
+ The default value
75
+
76
+ Returns
77
+ -------
78
+ Validator
79
+ """
80
+
81
+ if self.value is None:
82
+ self.value = default
83
+ logging.debug(
84
+ f"The value of {self.value_name} has been updated from None to {default}"
85
+ )
86
+ return self
87
+
88
+ def override_value(self, override_flag: bool, override_value):
89
+ """
90
+ Overrides the current value of the ovverride_flag is True
91
+
92
+ Parameters
93
+ ----------
94
+ override_flag : bool
95
+ Whether or not to override the current value
96
+ override_value
97
+ The value to use to override the existing values
98
+
99
+ Returns
100
+ -------
101
+ Validator
102
+ """
103
+
104
+ if override_flag:
105
+ self.value = override_value
106
+ logging.debug(
107
+ f"The value of {self.value_name} has been overriden with {override_value}"
108
+ )
109
+ return self
110
+
111
+ def discard_dict_keys_none_value(self):
112
+ """
113
+ Discards dictionary key, value pairs where the value is None
114
+
115
+ Returns
116
+ -------
117
+ Validator
118
+ """
119
+
120
+ if isinstance(self.value, dict):
121
+ invalid_keys = [key for key, value in self.value.items() if value is None]
122
+ if len(invalid_keys) > 0:
123
+ logging.info(
124
+ f"The values for the keys {str(invalid_keys)} are None and have thus been removed from {self.value_name}"
125
+ )
126
+ for key in invalid_keys:
127
+ self.value.pop(key, None)
128
+ return self
129
+
130
+ def get_dict_values(self):
131
+ """
132
+ Gets a list of values from a dictionary
133
+
134
+ Returns
135
+ -------
136
+ Validator
137
+ """
138
+
139
+ if isinstance(self.value, dict):
140
+ self.value = list(self.value.values())
141
+ return self
142
+
143
+ def filter_list_using_first_character(self, first_character: str):
144
+ """
145
+ Filters a list of strings by looking to see if the first character matches the provided
146
+
147
+ Parameters
148
+ ----------
149
+ first_character : str
150
+ The character to look for in the first character of each element
151
+
152
+ Returns
153
+ -------
154
+ Validator
155
+ """
156
+
157
+ if isinstance(self.value, list):
158
+ to_remove = []
159
+ for value in self.value:
160
+ if isinstance(value, str) and (
161
+ len(value) > 0 and value[0] == first_character
162
+ ):
163
+ to_remove.append(value)
164
+ self.value = [value for value in self.value if value not in to_remove]
165
+ return self
166
+
167
+ def check_subset_of_list(self, superset: list, superset_name: str):
168
+ """
169
+ Checks if one list is a subset of another
170
+
171
+ Parameters
172
+ ----------
173
+ superset : list
174
+ The superset to check of the value is a subset of
175
+ superset_name : list
176
+ The name of the superset
177
+
178
+ Returns
179
+ -------
180
+ Validator
181
+ """
182
+
183
+ if isinstance(self.value, list):
184
+ if len(set(self.value) - set(superset)) > 0:
185
+ raise ValueError(
186
+ f"""The values {str(set(self.value) - set(superset))} exist in the {self.value_name}
187
+ but do not exist in the {superset_name}."""
188
+ )
189
+ return self
190
+
191
+ def check_no_intersection_with_list(self, other_list: list, list_name: str):
192
+ """
193
+ Checks that the value has no intersection with a provided list
194
+
195
+ Parameters
196
+ ----------
197
+ other_list : list
198
+ The list to check the value has no intersection with
199
+ list_name : list
200
+ The name of the list
201
+
202
+ Returns
203
+ -------
204
+ Validator
205
+
206
+ """
207
+
208
+ if isinstance(self.value, list):
209
+ if len(set(other_list).intersection(set(self.value))) > 0:
210
+ err = f"""The columns {str(set(other_list).intersection(set(self.value)))} are specified in {self.value_name}
211
+ yet they contain null (NaN) values for some rows in the provided data. Null values are not
212
+ allowed for required fields. Please ensure that required columns do not contain ANY null
213
+ values or specify a default value in your mapping by specifying a dictionary with the keys
214
+ "column" and "default"."""
215
+ logging.error(err)
216
+ raise ValueError(err)
217
+
218
+ def check_entries_are_strings_or_dict_containing_key(self, expected_key):
219
+ missing_items = []
220
+
221
+ for item in self.value:
222
+ if not (isinstance(item, dict) or isinstance(item, str)):
223
+ missing_items.append(f"{item} is not a string or dictionary")
224
+ continue
225
+
226
+ if isinstance(item, dict):
227
+ if expected_key not in item:
228
+ missing_items.append(
229
+ f"{item} does not contain the mandatory 'source' key"
230
+ )
231
+ continue
232
+ if not isinstance(item[expected_key], str):
233
+ missing_items.append(
234
+ f"{item[expected_key]} in {item} is not a string"
235
+ )
236
+ continue
237
+
238
+ if len(missing_items) > 0:
239
+ raise ValueError(
240
+ f"The value {self.value} provided in {self.value_name} is invalid. {', '.join(missing_items)}."
241
+ )
242
+
243
+ return self
@@ -0,0 +1 @@
1
+ from finbourne_sdk_utils.extract.group_holdings import get_holdings_for_group
@@ -0,0 +1,400 @@
1
+ from finbourne_sdk_utils.cocoon.async_tools import run_in_executor, ThreadPool
2
+ import lusid
3
+ import asyncio
4
+ from functools import reduce
5
+ from typing import Dict, List
6
+ from finbourne_sdk_utils.cocoon.async_tools import (
7
+ start_event_loop_new_thread,
8
+ stop_event_loop_new_thread,
9
+ )
10
+
11
+
12
+ def _join_holdings(
13
+ holdings_to_join: Dict[str, List[lusid.models.PortfolioHolding]],
14
+ group_by_portfolio: bool = False,
15
+ dict_key="GroupHoldings",
16
+ ) -> Dict[str, List[lusid.models.PortfolioHolding]]:
17
+ """
18
+ This function joins the holdings together from multiple Portfolios into a single list of PortfolioHolding
19
+
20
+ Currently the following constraints exist:
21
+
22
+ 1) Only support for instrument properties where the first one is taken
23
+ 2) Sub-holding-keys are ignored
24
+ 3) The portfolio currency is assumed to be the same across all Portfolios
25
+
26
+ Parameters
27
+ ----------
28
+ holdings_to_join : Dict[str, List[lusid.models.PortfolioHolding]
29
+ The dictionary of lists of PortfolioHolding keyed by the unique combination of Scope and Code
30
+ for the Portfolio Group or Portfolios. These will either be returned as is or joined to form a new dictionary
31
+ with a single key against a single list of PortfolioHolding.
32
+
33
+ group_by_portfolio : bool
34
+ Whether or not to group the holdings by portfolio
35
+
36
+ dict_key : str
37
+ The key to use for the merged holdings, usually the scope/code combination of the Portfolio Group
38
+
39
+ Returns
40
+ -------
41
+ holdings_joined : Dict[str, List[lusid.models.PortfolioHolding]
42
+ The joined dictionary of a list of PortfolioHolding which either has a single key for the Portfolio
43
+ Group or a key for each Portfolio in the group
44
+ """
45
+
46
+ # If each portfolio should remain separated
47
+ if group_by_portfolio:
48
+ return holdings_to_join
49
+
50
+ # Flatten the holdings into a single list
51
+ all_holdings = [
52
+ holding
53
+ for holding_list in holdings_to_join.values()
54
+ for holding in holding_list
55
+ ]
56
+ # Initialise a dictionary to hold the positions against a key
57
+ all_holdings_keyed = {}
58
+ # Construct the keyed dictionary using the LUSID instrument id, holding type and currency as the unique key for each holding
59
+ [
60
+ all_holdings_keyed.setdefault(
61
+ f"{holding.instrument_uid}:{holding.holding_type}:{holding.cost.currency}",
62
+ [],
63
+ ).append(holding)
64
+ for holding in all_holdings
65
+ ]
66
+
67
+ # Initialise a list to hold the joined holdings
68
+ joined_holdings = []
69
+
70
+ # Reduce the list of holdings against each key to a single holding
71
+ for key, value in all_holdings_keyed.items():
72
+ joined_holdings.append(
73
+ lusid.models.PortfolioHolding(
74
+ # Use the instrument_uid from the key
75
+ instrument_uid=key.split(":")[0],
76
+ # Use the holding type from the key
77
+ holding_type=key.split(":")[1],
78
+ # Add the metric fields together
79
+ units=reduce((lambda x, y: x + y), list(map(lambda x: x.units, value))),
80
+ settled_units=reduce(
81
+ (lambda x, y: x + y), list(map(lambda x: x.settled_units, value))
82
+ ),
83
+ cost=lusid.models.CurrencyAndAmount(
84
+ # Use the currency form the key
85
+ currency=key.split(":")[2],
86
+ amount=reduce(
87
+ (lambda x, y: x + y), list(map(lambda x: x.cost.amount, value))
88
+ ),
89
+ ),
90
+ cost_portfolio_ccy=lusid.models.CurrencyAndAmount(
91
+ # Use the first currency, these holdings could be from different portfolios, so the validity of this is questionable
92
+ currency=value[0].cost_portfolio_ccy.currency,
93
+ amount=reduce(
94
+ (lambda x, y: x + y),
95
+ list(map(lambda x: x.cost_portfolio_ccy.amount, value)),
96
+ ),
97
+ ),
98
+ # Takes the properties from the first value, only allows instrument properties
99
+ properties={
100
+ property_key: value
101
+ for property_key, value in value[0].properties.items()
102
+ if value.key.split("/")[0] == "Instrument"
103
+ },
104
+ )
105
+ )
106
+
107
+ return {dict_key: joined_holdings}
108
+
109
+
110
+ @run_in_executor
111
+ def _get_portfolio_group(
112
+ api_factory: lusid.SyncApiClientFactory, scope: str, code: str, **kwargs
113
+ ) -> lusid.models.PortfolioGroup:
114
+ """
115
+ This function gets a Portfolio Group from LUSID.
116
+
117
+ Parameters
118
+ ----------
119
+ api_factory : lusid.SyncApiClientFactory
120
+ The api factory to use
121
+ scope : str
122
+ The scope of the Portfolio Group
123
+ code : str
124
+ The code of the Portfolio Group, with the scope this uniquely identifiers the Portfolio Group
125
+
126
+ Returns
127
+ -------
128
+ response : lusid.models.PortfolioGroup
129
+ The Portfolio Group
130
+
131
+ Other Parameters
132
+ ------
133
+ effective_at : datetime
134
+ The effective datetime at which to get the Portfolio Group
135
+ as_at : datetime
136
+ The as at datetime at which to get the Portfolio Group
137
+ thread_pool
138
+ The thread pool to run this function in
139
+ """
140
+
141
+ # Filter out the relevant keyword arguments as the LUSID API will raise an exception if given extras
142
+ lusid_keyword_arguments = {
143
+ key: value for key, value in kwargs.items() if key in ["effective_at", "as_at"]
144
+ }
145
+
146
+ # Call LUSID to get the portfolio group
147
+ response = api_factory.build(lusid.api.PortfolioGroupsApi).get_portfolio_group(scope=scope, code=code, **lusid_keyword_arguments)
148
+
149
+ return response
150
+
151
+
152
+ @run_in_executor
153
+ def _get_portfolio_holdings(
154
+ api_factory: lusid.SyncApiClientFactory, scope: str, code: str, **kwargs
155
+ ) -> Dict[str, List[lusid.models.PortfolioHolding]]:
156
+ """
157
+ This function gets the holdings of a Portfolio from LUSID.
158
+
159
+ Parameters
160
+ ----------
161
+ api_factory : lusid.SyncApiClientFactory
162
+ The api factory to use
163
+ scope : str
164
+ The scope of the Portfolio
165
+ code : str
166
+ The code of the Portfolio, with the scope this uniquely identifiers the Portfolio
167
+ effective_at : datetime
168
+ The effective datetime at which to get the Portfolio Group
169
+ as_at : datetime
170
+ The as at datetime at which to get the Portfolio Group
171
+ filter : str
172
+ by_taxlots : bool
173
+ property_keys : list[str]
174
+
175
+ Returns
176
+ -------
177
+ response : Dict[str, List[lusid.models.PortfolioHolding]]
178
+ The list of PortfolioHolding keyed by the unique combination of the Portfolio's scope and code
179
+
180
+ Other Parameters
181
+ ------
182
+ effective_at : datetime
183
+ The effective datetime at which to get the Portfolio Group
184
+ as_at : datetime
185
+ The as at datetime at which to get the Portfolio Group
186
+ filter : str
187
+ The filter to use to filter the holdings
188
+ by_taxlots : bool
189
+ Whether or not to break the holdings down into individual tax lots
190
+ property_keys : list[str]
191
+ The list of property keys to decorate onto the holdings, must be from the Instrument domain
192
+ thread_pool
193
+ The thread pool to run this function in
194
+ """
195
+
196
+ # Filter out the relevant keyword arguments as the LUSID API will raise an exception if given extras
197
+ lusid_keyword_arguments = {
198
+ key: value
199
+ for key, value in kwargs.items()
200
+ if key in ["effective_at", "as_at", "filter", "by_taxlots", "property_keys"]
201
+ }
202
+
203
+ # Call LUSID to get the holdings for the Portfolio
204
+ response = api_factory.build(lusid.api.TransactionPortfoliosApi).get_holdings(scope=scope, code=code, **lusid_keyword_arguments)
205
+
206
+ # Key the response with the unique scope/code combination
207
+ return {f"{scope} : {code}": response.values}
208
+
209
+
210
+ async def _get_holdings_for_group_recursive(
211
+ api_factory: lusid.SyncApiClientFactory,
212
+ group_scope: str,
213
+ group_code: str,
214
+ group_by_portfolio=False,
215
+ **kwargs,
216
+ ) -> Dict[str, List[lusid.models.PortfolioHolding]]:
217
+ """
218
+ This function recursively gets the holdings for a Portfolio Group in LUSID by making a request to get the holdings for
219
+ each sub-group and portofolio and then joining the results together above the API.
220
+
221
+ Parameters
222
+ ----------
223
+ api_factory : lusid.SyncApiClientFactory
224
+ The api factory to use
225
+ group_scope : str
226
+ The scope of the Portfolio Group
227
+ group_code : str
228
+ The code of the Portfolio Group
229
+ group_by_portfolio : bool
230
+ Whether or not to group the holdings by Portfolio, if False will merge all Holdings together based on Instrument
231
+
232
+ Returns
233
+ -------
234
+ Dict[str, List[lusid.models.PortfolioHolding]]
235
+ The single set of holdings
236
+
237
+ Other Parameters
238
+ ------
239
+ effective_at : datetime
240
+ The effective datetime at which to get the Portfolio Group
241
+ as_at : datetime
242
+ The as at datetime at which to get the Portfolio Group
243
+ filter : str
244
+ The filter to use to filter the holdings
245
+ by_taxlots : bool
246
+ Whether or not to break the holdings down into individual tax lots
247
+ property_keys : list[str]
248
+ The list of property keys to decorate onto the holdings, must be from the Instrument domain
249
+ thread_pool
250
+ The thread pool to run this function in
251
+ """
252
+
253
+ # Get the details for the Portfolio Group including its sub-group and Portfolio members
254
+ response = await _get_portfolio_group(
255
+ api_factory, group_scope, group_code, **kwargs
256
+ )
257
+ portfolios = response.portfolios
258
+ sub_groups = response.sub_groups
259
+
260
+ # Get the holdings for each portfolio
261
+ portfolio_holdings = await asyncio.gather(
262
+ *[
263
+ _get_portfolio_holdings(
264
+ api_factory=api_factory,
265
+ scope=portfolio.scope,
266
+ code=portfolio.code,
267
+ **kwargs,
268
+ )
269
+ for portfolio in portfolios
270
+ ],
271
+ return_exceptions=False,
272
+ )
273
+
274
+ # Turn the list of dictionaries into a single dictionary where the holdings are keyed by the Portfolio scope : code
275
+ portfolio_holdings_keyed = {k: v for d in portfolio_holdings for k, v in d.items()}
276
+
277
+ # Join the holdings across all portfolios together
278
+ joined_portfolio_holdings = _join_holdings(
279
+ portfolio_holdings_keyed,
280
+ group_by_portfolio,
281
+ dict_key=f"{group_scope} : {group_code}",
282
+ )
283
+
284
+ # If there aren't any sub-groups return these joined holdings
285
+ if len(sub_groups) == 0:
286
+ return joined_portfolio_holdings
287
+ # Otherwise get the joined holdings for the sub-groups
288
+ else:
289
+ sub_group_holdings = await asyncio.gather(
290
+ *[
291
+ _get_holdings_for_group_recursive(
292
+ api_factory=api_factory,
293
+ group_scope=sub_group.scope,
294
+ group_code=sub_group.code,
295
+ group_by_portfolio=group_by_portfolio,
296
+ **kwargs,
297
+ )
298
+ for sub_group in sub_groups
299
+ ],
300
+ return_exceptions=False,
301
+ )
302
+
303
+ # Turn the list of dictionaries into a single dictionary where the holdings are keyed by the Portfolio scope : code
304
+ sub_group_holdings_keyed = {
305
+ k: v for d in sub_group_holdings for k, v in d.items()
306
+ }
307
+
308
+ # Join together the holdings across all the sub-groups
309
+ joined_sub_group_holdings = _join_holdings(
310
+ sub_group_holdings_keyed, group_by_portfolio, dict_key="SubGroupHoldings"
311
+ )
312
+
313
+ # There shouldn't be any overlap between the keys in these two sets
314
+ if (
315
+ len(
316
+ set(joined_sub_group_holdings.keys()).intersection(
317
+ set(joined_portfolio_holdings)
318
+ )
319
+ )
320
+ > 0
321
+ ):
322
+ raise ValueError(
323
+ "There are duplicate Portfolios in the Portfolio Group. This is not currently supported. "
324
+ "Please ensure that there are no duplicates in your Porfolio Group via sub-groups and try again"
325
+ )
326
+
327
+ # Join and return the joined sub-group holdings with the joined portfolio holdings
328
+ return _join_holdings(
329
+ {**joined_sub_group_holdings, **joined_portfolio_holdings},
330
+ group_by_portfolio,
331
+ dict_key=f"{group_scope} : {group_code}",
332
+ )
333
+
334
+
335
+ def get_holdings_for_group(
336
+ api_factory: lusid.SyncApiClientFactory,
337
+ group_scope: str,
338
+ group_code: str,
339
+ group_by_portfolio: bool = False,
340
+ num_threads=5,
341
+ **kwargs,
342
+ ) -> Dict[str, List[lusid.models.PortfolioHolding]]:
343
+ """
344
+ This function gets the holdings for a Portfolio Group in LUSID.
345
+
346
+ Parameters
347
+ ----------
348
+ api_factory : lusid.SyncApiClientFactory
349
+ The api factory to use
350
+ group_scope : str
351
+ The scope of the Portfolio Group
352
+ group_code : str
353
+ The code of the Portfolio Group
354
+ group_by_portfolio : bool
355
+ Whether or not to group the holdings by Portfolio, if False will merge all Holdings together based on Instrument
356
+ num_threads : int
357
+ The number of threads to use for asynchronous programming
358
+
359
+ Returns
360
+ -------
361
+ group_holdings : Dict[str, List[lusid.models.PortfolioHolding]]
362
+ The single set of holdings either keyed by the Portfolio scope/code or the Portfolio Group scope/code
363
+
364
+ Other Parameters
365
+ ------
366
+ effective_at : datetime
367
+ The effective datetime at which to get the Portfolio Group
368
+ as_at : datetime
369
+ The as at datetime at which to get the Portfolio Group
370
+ filter : str
371
+ The filter to use to filter the holdings
372
+ by_taxlots : bool
373
+ Whether or not to break the holdings down into individual tax lots
374
+ property_keys : list[str]
375
+ The list of property keys to decorate onto the holdings, must be from the Instrument domain
376
+ """
377
+
378
+ # Create a new thread pool to run the asynchronous tasks in
379
+ thread_pool = ThreadPool(num_threads).thread_pool
380
+ kwargs["thread_pool"] = thread_pool
381
+
382
+ # Start a new event loop in a new thread, this is required to run inside a Jupyter notebook
383
+ loop = start_event_loop_new_thread()
384
+
385
+ # Get the responses from LUSID
386
+ group_holdings = asyncio.run_coroutine_threadsafe(
387
+ _get_holdings_for_group_recursive(
388
+ api_factory=api_factory,
389
+ group_scope=group_scope,
390
+ group_code=group_code,
391
+ group_by_portfolio=group_by_portfolio,
392
+ **kwargs,
393
+ ),
394
+ loop,
395
+ ).result()
396
+
397
+ # Stop the additional event loop
398
+ stop_event_loop_new_thread(loop)
399
+
400
+ return group_holdings
@@ -0,0 +1 @@
1
+ import finbourne_sdk_utils.iam.roles