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.
- features/__init__.py +0 -0
- features/main.py +11 -0
- finbourne_sdk_utils/__init__.py +8 -0
- finbourne_sdk_utils/cocoon/__init__.py +34 -0
- finbourne_sdk_utils/cocoon/async_tools.py +94 -0
- finbourne_sdk_utils/cocoon/cocoon.py +1862 -0
- finbourne_sdk_utils/cocoon/cocoon_printer.py +455 -0
- finbourne_sdk_utils/cocoon/config/domain_settings.json +125 -0
- finbourne_sdk_utils/cocoon/config/seed_sample_data.json +36 -0
- finbourne_sdk_utils/cocoon/dateorcutlabel.py +198 -0
- finbourne_sdk_utils/cocoon/instruments.py +482 -0
- finbourne_sdk_utils/cocoon/properties.py +442 -0
- finbourne_sdk_utils/cocoon/seed_sample_data.py +137 -0
- finbourne_sdk_utils/cocoon/systemConfiguration.py +92 -0
- finbourne_sdk_utils/cocoon/transaction_type_upload.py +136 -0
- finbourne_sdk_utils/cocoon/utilities.py +1877 -0
- finbourne_sdk_utils/cocoon/validator.py +243 -0
- finbourne_sdk_utils/extract/__init__.py +1 -0
- finbourne_sdk_utils/extract/group_holdings.py +400 -0
- finbourne_sdk_utils/iam/__init__.py +1 -0
- finbourne_sdk_utils/iam/roles.py +74 -0
- finbourne_sdk_utils/jupyter_tools/__init__.py +2 -0
- finbourne_sdk_utils/jupyter_tools/hide_code_button.py +23 -0
- finbourne_sdk_utils/jupyter_tools/stop_execution.py +14 -0
- finbourne_sdk_utils/logger/LusidLogger.py +41 -0
- finbourne_sdk_utils/logger/__init__.py +1 -0
- finbourne_sdk_utils/lpt/__init__.py +0 -0
- finbourne_sdk_utils/lpt/back_compat.py +20 -0
- finbourne_sdk_utils/lpt/cash_ladder.py +191 -0
- finbourne_sdk_utils/lpt/connect_lusid.py +64 -0
- finbourne_sdk_utils/lpt/connect_none.py +5 -0
- finbourne_sdk_utils/lpt/connect_token.py +9 -0
- finbourne_sdk_utils/lpt/dfq.py +321 -0
- finbourne_sdk_utils/lpt/either.py +65 -0
- finbourne_sdk_utils/lpt/get_instruments.py +101 -0
- finbourne_sdk_utils/lpt/lpt.py +374 -0
- finbourne_sdk_utils/lpt/lse.py +188 -0
- finbourne_sdk_utils/lpt/map_instruments.py +164 -0
- finbourne_sdk_utils/lpt/pager.py +32 -0
- finbourne_sdk_utils/lpt/record.py +13 -0
- finbourne_sdk_utils/lpt/refreshing_token.py +43 -0
- finbourne_sdk_utils/lpt/search_instruments.py +48 -0
- finbourne_sdk_utils/lpt/stdargs.py +154 -0
- finbourne_sdk_utils/lpt/txn_config.py +128 -0
- finbourne_sdk_utils/lpt/txn_config_yaml.py +493 -0
- finbourne_sdk_utils/pandas_utils/__init__.py +0 -0
- finbourne_sdk_utils/pandas_utils/lusid_pandas.py +128 -0
- finbourne_sdk_utils-0.0.24.dist-info/LICENSE +21 -0
- finbourne_sdk_utils-0.0.24.dist-info/METADATA +25 -0
- finbourne_sdk_utils-0.0.24.dist-info/RECORD +52 -0
- finbourne_sdk_utils-0.0.24.dist-info/WHEEL +5 -0
- finbourne_sdk_utils-0.0.24.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1862 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import concurrent
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import lusid
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from typing import List, Tuple
|
|
10
|
+
|
|
11
|
+
from finbourne_sdk_utils import cocoon
|
|
12
|
+
from finbourne_sdk_utils.cocoon.async_tools import run_in_executor, ThreadPool
|
|
13
|
+
from finbourne_sdk_utils.cocoon.dateorcutlabel import DateOrCutLabel
|
|
14
|
+
from finbourne_sdk_utils.cocoon.utilities import (
|
|
15
|
+
checkargs,
|
|
16
|
+
strip_whitespace,
|
|
17
|
+
group_request_into_one,
|
|
18
|
+
extract_unique_portfolio_codes,
|
|
19
|
+
extract_unique_portfolio_codes_effective_at_tuples,
|
|
20
|
+
get_attributes_and_types
|
|
21
|
+
)
|
|
22
|
+
from finbourne_sdk_utils.cocoon.validator import Validator
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
import pytz
|
|
25
|
+
import logging
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BatchLoader:
|
|
29
|
+
"""
|
|
30
|
+
This class contains all the methods used for loading data in batches. The @run_in_executor decorator makes the
|
|
31
|
+
synchronous functions awaitable
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
@run_in_executor
|
|
36
|
+
def load_instrument_batch(
|
|
37
|
+
api_factory: lusid.SyncApiClientFactory, instrument_batch: list, **kwargs
|
|
38
|
+
) -> lusid.models.UpsertInstrumentsResponse:
|
|
39
|
+
"""
|
|
40
|
+
Upserts a batch of instruments to LUSID
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
api_factory : lusid.SyncApiClientFactory
|
|
45
|
+
The api factory to use
|
|
46
|
+
instrument_batch : list[lusid.models.InstrumentDefinition]
|
|
47
|
+
The batch of instruments to upsert
|
|
48
|
+
**kwargs
|
|
49
|
+
arguments specific to each call e.g. effective_at for holdings, unique_identifiers
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
lusid.models.UpsertInstrumentsResponse
|
|
54
|
+
The response from LUSID
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Ensure that the list of allowed unique identifiers exists
|
|
58
|
+
if "unique_identifiers" not in list(kwargs.keys()):
|
|
59
|
+
unique_identifiers = cocoon.instruments.get_unique_identifiers(
|
|
60
|
+
api_factory=api_factory
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
unique_identifiers = kwargs["unique_identifiers"]
|
|
64
|
+
|
|
65
|
+
@checkargs
|
|
66
|
+
def get_alphabetically_first_identifier_key(
|
|
67
|
+
instrument: lusid.models.InstrumentDefinition, unique_identifiers: list
|
|
68
|
+
):
|
|
69
|
+
"""
|
|
70
|
+
Gets the alphabetically first occurring unique identifier on an instrument and use it as the correlation
|
|
71
|
+
id on the request
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
instrument : lusid.models.InstrumentDefinition
|
|
76
|
+
The instrument to create a correlation id for
|
|
77
|
+
unique_identifiers : list[str] unique_identifiers
|
|
78
|
+
The list of allowed unique identifiers
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
str
|
|
83
|
+
The correlation id to use on the request
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
unique_identifiers_populated = list(
|
|
87
|
+
set(unique_identifiers).intersection(
|
|
88
|
+
set(list(instrument.identifiers.keys()))
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
unique_identifiers_populated.sort()
|
|
92
|
+
first_unique_identifier_alphabetically = unique_identifiers_populated[0]
|
|
93
|
+
return f"{first_unique_identifier_alphabetically}: {instrument.identifiers[first_unique_identifier_alphabetically].value}"
|
|
94
|
+
|
|
95
|
+
# If scope is not defined set to default scope
|
|
96
|
+
return api_factory.build(lusid.api.InstrumentsApi).upsert_instruments(
|
|
97
|
+
scope=kwargs["instrument_scope"],
|
|
98
|
+
request_body={
|
|
99
|
+
get_alphabetically_first_identifier_key(
|
|
100
|
+
instrument, unique_identifiers
|
|
101
|
+
): instrument
|
|
102
|
+
for instrument in instrument_batch
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
@run_in_executor
|
|
108
|
+
def load_quote_batch(
|
|
109
|
+
api_factory: lusid.SyncApiClientFactory, quote_batch: list, **kwargs
|
|
110
|
+
) -> lusid.models.UpsertQuotesResponse:
|
|
111
|
+
"""
|
|
112
|
+
Upserts a batch of quotes into LUSID
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
api_factory : lusid.SyncApiClientFactory
|
|
117
|
+
The api factory to use
|
|
118
|
+
quote_batch : list[lusid.models.UpsertQuoteRequest]
|
|
119
|
+
The batch of quotes to upsert
|
|
120
|
+
kwargs
|
|
121
|
+
scope
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
lusid.models.UpsertQuotesResponse
|
|
126
|
+
The response from LUSID
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
if "scope" not in list(kwargs.keys()):
|
|
131
|
+
raise KeyError(
|
|
132
|
+
"You are trying to load quotes without a scope, please ensure that a scope is provided."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return api_factory.build(lusid.api.QuotesApi).upsert_quotes(
|
|
136
|
+
scope=kwargs["scope"],
|
|
137
|
+
request_body={
|
|
138
|
+
"_".join(
|
|
139
|
+
[
|
|
140
|
+
quote.quote_id.quote_series_id.instrument_id,
|
|
141
|
+
quote.quote_id.quote_series_id.instrument_id_type,
|
|
142
|
+
str(quote.quote_id.effective_at),
|
|
143
|
+
]
|
|
144
|
+
): quote
|
|
145
|
+
for quote in quote_batch
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
@run_in_executor
|
|
151
|
+
def load_transaction_batch(
|
|
152
|
+
api_factory: lusid.SyncApiClientFactory, transaction_batch: list, **kwargs
|
|
153
|
+
) -> lusid.models.UpsertPortfolioTransactionsResponse:
|
|
154
|
+
"""
|
|
155
|
+
Upserts a batch of transactions into LUSID
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
api_factory : lusid.SyncApiClientFactory
|
|
160
|
+
The api factory to use
|
|
161
|
+
code : str
|
|
162
|
+
The code of the TransactionPortfolio to upsert the transactions into
|
|
163
|
+
transaction_batch : list[lusid.models.TransactionRequest]
|
|
164
|
+
The batch of transactions to upsert
|
|
165
|
+
kwargs
|
|
166
|
+
code -The code of the TransactionPortfolio to upsert the transactions into
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
lusid.models.UpsertPortfolioTransactionsResponse
|
|
171
|
+
The response from LUSID
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
if "scope" not in list(kwargs.keys()):
|
|
175
|
+
raise KeyError(
|
|
176
|
+
"You are trying to load transactions without a scope, please ensure that a scope is provided."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if "code" not in list(kwargs.keys()):
|
|
180
|
+
raise KeyError(
|
|
181
|
+
"You are trying to load transactions without a portfolio code, please ensure that a code is provided."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return api_factory.build(
|
|
185
|
+
lusid.api.TransactionPortfoliosApi
|
|
186
|
+
).upsert_transactions(
|
|
187
|
+
scope=kwargs["scope"],
|
|
188
|
+
code=kwargs["code"],
|
|
189
|
+
transaction_request=transaction_batch,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
@run_in_executor
|
|
194
|
+
def load_transactions_with_commit_mode_batch(
|
|
195
|
+
api_factory: lusid.SyncApiClientFactory, transaction_batch: List, **kwargs
|
|
196
|
+
) -> lusid.models.UpsertPortfolioTransactionsResponse:
|
|
197
|
+
"""
|
|
198
|
+
Upserts a batch of transactions into LUSID with specified type of upsert.
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
api_factory : lusid.SyncApiClientFactory
|
|
203
|
+
The api factory to use
|
|
204
|
+
code : str
|
|
205
|
+
The code of the TransactionPortfolio to upsert the transactions into
|
|
206
|
+
transaction_batch : List[lusid.models.TransactionRequest]
|
|
207
|
+
The batch of transactions to upsert
|
|
208
|
+
kwargs
|
|
209
|
+
code -The code of the TransactionPortfolio to upsert the transactions into
|
|
210
|
+
transactions_commit_mode - The type of upsert method to use (Atomic or Partial)
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
lusid.models.BatchUpsertPortfolioTransactionsResponse
|
|
215
|
+
The response from LUSID
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
if "scope" not in list(kwargs.keys()):
|
|
219
|
+
raise KeyError(
|
|
220
|
+
"You are trying to load transactions without a scope, please ensure that a scope is provided."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if "code" not in list(kwargs.keys()):
|
|
224
|
+
raise KeyError(
|
|
225
|
+
"You are trying to load transactions without a portfolio code, please ensure that a code is provided."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if kwargs["transactions_commit_mode"] is None:
|
|
229
|
+
raise KeyError(
|
|
230
|
+
"You are trying to load transactions without a commit mode (transaction_commit_mode), please provide a commit mode (Atomic or Partial)."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if kwargs["transactions_commit_mode"].lower() not in ("atomic", "partial"):
|
|
234
|
+
raise KeyError(
|
|
235
|
+
"You are trying to load transactions without an accepted type of commit mode, please provide either Atomic or Partial to the commit mode."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
request_body = {
|
|
239
|
+
f"transaction_{idx}": transaction
|
|
240
|
+
for idx, transaction in enumerate(transaction_batch)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
return api_factory.build(
|
|
245
|
+
lusid.api.TransactionPortfoliosApi
|
|
246
|
+
).batch_upsert_transactions(
|
|
247
|
+
scope=kwargs["scope"],
|
|
248
|
+
code=kwargs["code"],
|
|
249
|
+
success_mode=kwargs["transactions_commit_mode"],
|
|
250
|
+
request_body=request_body
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
@run_in_executor
|
|
255
|
+
def load_holding_batch(
|
|
256
|
+
api_factory: lusid.SyncApiClientFactory, holding_batch: list, **kwargs
|
|
257
|
+
) -> lusid.models.HoldingsAdjustment:
|
|
258
|
+
"""
|
|
259
|
+
Upserts a batch of holdings into LUSID
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
api_factory : lusid.SyncApiClientFactory
|
|
264
|
+
The api factory to use
|
|
265
|
+
holding_batch : list[lusid.models.AdjustHoldingRequest]
|
|
266
|
+
The batch of holdings
|
|
267
|
+
scope : str
|
|
268
|
+
The scope to upsert holdings into
|
|
269
|
+
code : str
|
|
270
|
+
The code of the portfolio to upsert holdings into
|
|
271
|
+
effective_at : str/Datetime/np.datetime64/np.ndarray/pd.Timestamp
|
|
272
|
+
The effective date of the holdings batch
|
|
273
|
+
kwargs
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
lusid.models.HoldingsAdjustment
|
|
278
|
+
The response from LUSID
|
|
279
|
+
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
if "scope" not in list(kwargs.keys()):
|
|
283
|
+
raise KeyError(
|
|
284
|
+
"You are trying to load transactions without a scope, please ensure that a scope is provided."
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if "code" not in list(kwargs.keys()):
|
|
288
|
+
raise KeyError(
|
|
289
|
+
"You are trying to load transactions without a portfolio code, please ensure that a code is provided."
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if "effective_at" not in list(kwargs.keys()):
|
|
293
|
+
raise KeyError(
|
|
294
|
+
"""There is no mapping for effective_at in the required mapping, please add it"""
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# If only an adjustment has been specified
|
|
298
|
+
if (
|
|
299
|
+
"holdings_adjustment_only" in list(kwargs.keys())
|
|
300
|
+
and kwargs["holdings_adjustment_only"]
|
|
301
|
+
):
|
|
302
|
+
return api_factory.build(
|
|
303
|
+
lusid.api.TransactionPortfoliosApi
|
|
304
|
+
).adjust_holdings(
|
|
305
|
+
scope=kwargs["scope"],
|
|
306
|
+
code=kwargs["code"],
|
|
307
|
+
effective_at=str(DateOrCutLabel(kwargs["effective_at"])),
|
|
308
|
+
adjust_holding_request=holding_batch,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
return api_factory.build(lusid.api.TransactionPortfoliosApi).set_holdings(
|
|
312
|
+
scope=kwargs["scope"],
|
|
313
|
+
code=kwargs["code"],
|
|
314
|
+
effective_at=str(DateOrCutLabel(kwargs["effective_at"])),
|
|
315
|
+
adjust_holding_request=holding_batch,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
@run_in_executor
|
|
320
|
+
def load_portfolio_batch(
|
|
321
|
+
api_factory: lusid.SyncApiClientFactory, portfolio_batch: list, **kwargs
|
|
322
|
+
) -> lusid.models.Portfolio:
|
|
323
|
+
"""
|
|
324
|
+
Upserts a batch of portfolios to LUSID
|
|
325
|
+
|
|
326
|
+
Parameters
|
|
327
|
+
----------
|
|
328
|
+
api_factory : lusid.SyncApiClientFactory
|
|
329
|
+
the api factory to use
|
|
330
|
+
portfolio_batch : list[lusid.models.CreateTransactionPortfolioRequest]
|
|
331
|
+
The batch of portfolios to create
|
|
332
|
+
scope : str
|
|
333
|
+
The scope to create the portfolios in
|
|
334
|
+
code : str
|
|
335
|
+
The code of the portfolio to create
|
|
336
|
+
kwargs
|
|
337
|
+
|
|
338
|
+
Returns
|
|
339
|
+
-------
|
|
340
|
+
lusid.models.Portfolio
|
|
341
|
+
the response from LUSID
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
if "scope" not in list(kwargs.keys()):
|
|
345
|
+
raise KeyError(
|
|
346
|
+
"You are trying to load transactions without a scope, please ensure that a scope is provided."
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if "code" not in list(kwargs.keys()):
|
|
350
|
+
raise KeyError(
|
|
351
|
+
"You are trying to load transactions without a portfolio code, please ensure that a code is provided."
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
return api_factory.build(lusid.api.PortfoliosApi).get_portfolio(
|
|
356
|
+
scope=kwargs["scope"], code=kwargs["code"]
|
|
357
|
+
)
|
|
358
|
+
# Add in here upsert portfolio properties if it does exist
|
|
359
|
+
except lusid.exceptions.ApiException as e:
|
|
360
|
+
if e.status == 404:
|
|
361
|
+
return api_factory.build(
|
|
362
|
+
lusid.api.TransactionPortfoliosApi
|
|
363
|
+
).create_portfolio(
|
|
364
|
+
scope=kwargs["scope"],
|
|
365
|
+
create_transaction_portfolio_request=portfolio_batch[0],
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
return e
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
@run_in_executor
|
|
372
|
+
def load_reference_portfolio_batch(
|
|
373
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
374
|
+
reference_portfolio_batch: list,
|
|
375
|
+
**kwargs,
|
|
376
|
+
) -> lusid.models.Portfolio:
|
|
377
|
+
"""
|
|
378
|
+
Upserts a batch of reference portfolios to LUSID
|
|
379
|
+
|
|
380
|
+
Parameters
|
|
381
|
+
----------
|
|
382
|
+
api_factory : lusid.SyncApiClientFactory
|
|
383
|
+
the api factory to use
|
|
384
|
+
portfolio_batch : list[lusid.models.CreateReferencePortfolioRequest]
|
|
385
|
+
The batch of reference portfolios to create
|
|
386
|
+
scope : str
|
|
387
|
+
The scope to create the reference portfolios in
|
|
388
|
+
code : str
|
|
389
|
+
The code of the reference portfolio to create
|
|
390
|
+
kwargs
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
lusid.models.ReferencePortfolio
|
|
395
|
+
the response from LUSID
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
if "scope" not in kwargs.keys():
|
|
399
|
+
raise KeyError(
|
|
400
|
+
"You are trying to load a reference portfolio without a scope, please ensure that a scope is provided."
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if "code" not in kwargs.keys():
|
|
404
|
+
raise KeyError(
|
|
405
|
+
"You are trying to load a reference portfolio without a portfolio code, please ensure that a code is provided."
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
return api_factory.build(lusid.api.PortfoliosApi).get_portfolio(
|
|
410
|
+
scope=kwargs["scope"], code=kwargs["code"]
|
|
411
|
+
)
|
|
412
|
+
# TODO: Add in here upsert portfolio properties if it does exist
|
|
413
|
+
|
|
414
|
+
except lusid.exceptions.ApiException as e:
|
|
415
|
+
if e.status == 404:
|
|
416
|
+
return api_factory.build(
|
|
417
|
+
lusid.api.ReferencePortfolioApi
|
|
418
|
+
).create_reference_portfolio(
|
|
419
|
+
scope=kwargs["scope"],
|
|
420
|
+
create_reference_portfolio_request=reference_portfolio_batch[0],
|
|
421
|
+
)
|
|
422
|
+
else:
|
|
423
|
+
return e
|
|
424
|
+
|
|
425
|
+
@staticmethod
|
|
426
|
+
@run_in_executor
|
|
427
|
+
def load_instrument_property_batch(
|
|
428
|
+
api_factory: lusid.SyncApiClientFactory, property_batch: list, **kwargs
|
|
429
|
+
) -> List[lusid.models.UpsertInstrumentPropertiesResponse]:
|
|
430
|
+
"""
|
|
431
|
+
Add properties to the set instruments
|
|
432
|
+
|
|
433
|
+
Parameters
|
|
434
|
+
----------
|
|
435
|
+
api_factory : lusid.SyncApiClientFactory
|
|
436
|
+
The api factory to use
|
|
437
|
+
property_batch : list[lusid.models.UpsertInstrumentPropertyRequest]
|
|
438
|
+
Properties to add,
|
|
439
|
+
identifiers will be resolved to a LusidInstrumentId, where an identifier resolves to more
|
|
440
|
+
than one LusidInstrumentId the property will be added to all matching instruments
|
|
441
|
+
kwargs
|
|
442
|
+
|
|
443
|
+
Returns
|
|
444
|
+
-------
|
|
445
|
+
list[lusid.models.UpsertInstrumentPropertiesResponse]
|
|
446
|
+
the response from LUSID
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
results = []
|
|
450
|
+
for request in property_batch:
|
|
451
|
+
search_request = lusid.models.InstrumentSearchProperty(
|
|
452
|
+
key=f"instrument/default/{request.identifier_type}",
|
|
453
|
+
value=request.identifier,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# find the matching instruments
|
|
457
|
+
mastered_instruments = api_factory.build(
|
|
458
|
+
lusid.api.SearchApi
|
|
459
|
+
).instruments_search(
|
|
460
|
+
instrument_search_property=[search_request], mastered_only=True
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# flat map the results to a list of luids
|
|
464
|
+
luids = [
|
|
465
|
+
luid
|
|
466
|
+
for luids in [
|
|
467
|
+
list(
|
|
468
|
+
map(
|
|
469
|
+
lambda m: m.identifiers["LusidInstrumentId"].value,
|
|
470
|
+
mastered.mastered_instruments,
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
for mastered in [matches for matches in mastered_instruments]
|
|
474
|
+
]
|
|
475
|
+
for luid in luids
|
|
476
|
+
]
|
|
477
|
+
|
|
478
|
+
if len(luids) == 0:
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
properties_request = [
|
|
482
|
+
lusid.models.UpsertInstrumentPropertyRequest(
|
|
483
|
+
identifier_type="LusidInstrumentId",
|
|
484
|
+
identifier=luid,
|
|
485
|
+
properties=request.properties,
|
|
486
|
+
)
|
|
487
|
+
for luid in luids
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
results.append(
|
|
491
|
+
api_factory.build(
|
|
492
|
+
lusid.api.InstrumentsApi
|
|
493
|
+
).upsert_instruments_properties(properties_request)
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
return results
|
|
497
|
+
|
|
498
|
+
@staticmethod
|
|
499
|
+
@run_in_executor
|
|
500
|
+
def load_portfolio_group_batch(
|
|
501
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
502
|
+
portfolio_group_batch: list,
|
|
503
|
+
**kwargs,
|
|
504
|
+
) -> lusid.models.PortfolioGroup:
|
|
505
|
+
"""
|
|
506
|
+
Upserts a batch of portfolios to LUSID
|
|
507
|
+
|
|
508
|
+
Parameters
|
|
509
|
+
----------
|
|
510
|
+
api_factory : lusid.SyncApiClientFactory
|
|
511
|
+
the api factory to use
|
|
512
|
+
portfolio_group_batch : list[lusid.models.CreateTransactionPortfolioRequest]
|
|
513
|
+
The batch of portfilios to create
|
|
514
|
+
scope : str
|
|
515
|
+
The scope to create the portfolio group in
|
|
516
|
+
code : str
|
|
517
|
+
The code of the portfolio group to create
|
|
518
|
+
kwargs
|
|
519
|
+
|
|
520
|
+
Returns
|
|
521
|
+
-------
|
|
522
|
+
lusid.models.PortfolioGroup
|
|
523
|
+
The response from LUSID
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
updated_request = group_request_into_one(
|
|
527
|
+
portfolio_group_batch[0].__class__.__name__,
|
|
528
|
+
portfolio_group_batch,
|
|
529
|
+
["values"],
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
if "scope" not in list(kwargs.keys()):
|
|
533
|
+
raise KeyError(
|
|
534
|
+
"You are trying to load a portfolio group without a scope, please ensure that a scope is provided."
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
if "code" not in list(kwargs.keys()):
|
|
538
|
+
raise KeyError(
|
|
539
|
+
"You are trying to load a portfolio group without a portfolio code, please ensure that a code is provided."
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
|
|
544
|
+
current_portfolio_group = api_factory.build(
|
|
545
|
+
lusid.api.PortfolioGroupsApi
|
|
546
|
+
).get_portfolio_group(scope=kwargs["scope"], code=kwargs["code"])
|
|
547
|
+
|
|
548
|
+
# Capture all portfolios - the ones currently in group + the new ones to be added
|
|
549
|
+
all_portfolios_to_add = (
|
|
550
|
+
updated_request.values + current_portfolio_group.portfolios
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
current_portfolios_in_group = [
|
|
554
|
+
code
|
|
555
|
+
for code in updated_request.values
|
|
556
|
+
if code in current_portfolio_group.portfolios
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
if len(current_portfolios_in_group) > 0:
|
|
560
|
+
for code in current_portfolios_in_group:
|
|
561
|
+
logging.info(
|
|
562
|
+
f"The portfolio {code.code} with scope {code.scope} is already in group {current_portfolio_group.id.code}"
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# Parse out new portfolios only
|
|
566
|
+
new_portfolios = [
|
|
567
|
+
code
|
|
568
|
+
for code in all_portfolios_to_add
|
|
569
|
+
if code not in current_portfolio_group.portfolios
|
|
570
|
+
]
|
|
571
|
+
|
|
572
|
+
for code, scope in set(
|
|
573
|
+
[(resource.code, resource.scope) for resource in new_portfolios]
|
|
574
|
+
):
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
|
|
578
|
+
current_portfolio_group = api_factory.build(
|
|
579
|
+
lusid.api.PortfolioGroupsApi
|
|
580
|
+
).add_portfolio_to_group(
|
|
581
|
+
scope=kwargs["scope"],
|
|
582
|
+
code=kwargs["code"],
|
|
583
|
+
effective_at=datetime.now(tz=pytz.UTC).isoformat(),
|
|
584
|
+
resource_id=lusid.models.ResourceId(scope=scope, code=code),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
except lusid.exceptions.ApiException as e:
|
|
588
|
+
logging.error(json.loads(e.body)["title"])
|
|
589
|
+
|
|
590
|
+
return current_portfolio_group
|
|
591
|
+
|
|
592
|
+
# Add in here upsert portfolio properties if it does exist
|
|
593
|
+
except lusid.exceptions.ApiException as e:
|
|
594
|
+
if e.status == 404:
|
|
595
|
+
return api_factory.build(
|
|
596
|
+
lusid.api.PortfolioGroupsApi
|
|
597
|
+
).create_portfolio_group(
|
|
598
|
+
scope=kwargs["scope"],
|
|
599
|
+
create_portfolio_group_request=updated_request,
|
|
600
|
+
)
|
|
601
|
+
else:
|
|
602
|
+
return e
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
async def _load_data(
|
|
606
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
607
|
+
single_requests: list,
|
|
608
|
+
file_type: str,
|
|
609
|
+
**kwargs,
|
|
610
|
+
):
|
|
611
|
+
"""
|
|
612
|
+
This function calls the appropriate batch loader
|
|
613
|
+
|
|
614
|
+
Parameters
|
|
615
|
+
----------
|
|
616
|
+
api_factory : lusid.SyncApiClientFactory
|
|
617
|
+
The api factory to use
|
|
618
|
+
single_requests
|
|
619
|
+
The list of single requests for LUSID
|
|
620
|
+
file_type : str
|
|
621
|
+
The file type e.g. instruments, portfolios etc.
|
|
622
|
+
kwargs
|
|
623
|
+
arguments specific to each call e.g. effective_at for holdings
|
|
624
|
+
|
|
625
|
+
Returns
|
|
626
|
+
-------
|
|
627
|
+
BatchLoader : StaticMethod
|
|
628
|
+
A static method on batchloader
|
|
629
|
+
"""
|
|
630
|
+
|
|
631
|
+
# Dynamically call the correct async function to use based on the file type
|
|
632
|
+
identifier = uuid.uuid4()
|
|
633
|
+
logging.debug(f"Running load_{file_type}_batch({identifier})")
|
|
634
|
+
from time import time
|
|
635
|
+
|
|
636
|
+
start = time()
|
|
637
|
+
response = await getattr(BatchLoader, f"load_{file_type}_batch")(
|
|
638
|
+
api_factory,
|
|
639
|
+
single_requests,
|
|
640
|
+
# Any specific arguments e.g. 'code' for transactions, 'effective_at' for holdings is passed in via **kwargs
|
|
641
|
+
**kwargs,
|
|
642
|
+
)
|
|
643
|
+
logging.debug(f"Batch completed ({identifier}) - duration: {time() - start}")
|
|
644
|
+
return response
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _convert_batch_to_models(
|
|
648
|
+
data_frame: pd.DataFrame,
|
|
649
|
+
mapping_required: dict,
|
|
650
|
+
mapping_optional: dict,
|
|
651
|
+
property_columns: list,
|
|
652
|
+
properties_scope: str,
|
|
653
|
+
instrument_identifier_mapping: dict,
|
|
654
|
+
file_type: str,
|
|
655
|
+
domain_lookup: dict,
|
|
656
|
+
sub_holding_keys: list,
|
|
657
|
+
sub_holding_keys_scope: str,
|
|
658
|
+
**kwargs,
|
|
659
|
+
):
|
|
660
|
+
"""
|
|
661
|
+
This function populates the required models from a DataFrame and loads the data into LUSID
|
|
662
|
+
|
|
663
|
+
Parameters
|
|
664
|
+
----------
|
|
665
|
+
data_frame : pd.DataFrame
|
|
666
|
+
The DataFrame containing the data to load
|
|
667
|
+
mapping_required : dict
|
|
668
|
+
The required mapping
|
|
669
|
+
mapping_optional : dict
|
|
670
|
+
The optional mapping
|
|
671
|
+
property_columns : list
|
|
672
|
+
The property columns to add as property values
|
|
673
|
+
properties_scope : str
|
|
674
|
+
The scope to add the property values in
|
|
675
|
+
instrument_identifier_mapping : dict
|
|
676
|
+
The mapping for the identifiers
|
|
677
|
+
file_type : str
|
|
678
|
+
The file type to load
|
|
679
|
+
domain_lookup : dict
|
|
680
|
+
The domain lookup
|
|
681
|
+
sub_holding_keys : list
|
|
682
|
+
The sub holding keys to use
|
|
683
|
+
sub_holding_keys_scope : str
|
|
684
|
+
The scope to use for the sub holding keys
|
|
685
|
+
kwargs
|
|
686
|
+
Arguments specific to each call e.g. effective_at for holdings
|
|
687
|
+
|
|
688
|
+
Returns
|
|
689
|
+
-------
|
|
690
|
+
single_requests : list
|
|
691
|
+
A list of populated LUSID request models
|
|
692
|
+
"""
|
|
693
|
+
|
|
694
|
+
source_columns = [
|
|
695
|
+
column.get("target", column.get("source")) for column in property_columns
|
|
696
|
+
]
|
|
697
|
+
|
|
698
|
+
# Get the data types of the columns to be added as properties
|
|
699
|
+
property_dtypes = data_frame.loc[:, source_columns].dtypes
|
|
700
|
+
|
|
701
|
+
# Get the types of the attributes on the top level model for this request
|
|
702
|
+
open_api_types = get_attributes_and_types(getattr(
|
|
703
|
+
lusid.models, domain_lookup[file_type]["top_level_model"]
|
|
704
|
+
))
|
|
705
|
+
|
|
706
|
+
# If there is a sub_holding_keys attribute and it has a dict type this means the sub_holding_keys
|
|
707
|
+
# need to be populated with property values
|
|
708
|
+
if (
|
|
709
|
+
"sub_holding_keys" in open_api_types.keys()
|
|
710
|
+
and "Mapping" in open_api_types["sub_holding_keys"]
|
|
711
|
+
):
|
|
712
|
+
sub_holding_key_dtypes = data_frame.loc[:, sub_holding_keys].dtypes
|
|
713
|
+
# If not and they are provided as full keys
|
|
714
|
+
elif len(sub_holding_keys) > 0:
|
|
715
|
+
sub_holding_keys_row = cocoon.properties._infer_full_property_keys(
|
|
716
|
+
partial_keys=sub_holding_keys,
|
|
717
|
+
properties_scope=sub_holding_keys_scope,
|
|
718
|
+
domain="Transaction",
|
|
719
|
+
)
|
|
720
|
+
# If no keys
|
|
721
|
+
else:
|
|
722
|
+
sub_holding_keys_row = None
|
|
723
|
+
|
|
724
|
+
unique_identifiers = kwargs["unique_identifiers"]
|
|
725
|
+
|
|
726
|
+
# Iterate over the DataFrame creating the single requests
|
|
727
|
+
single_requests = []
|
|
728
|
+
for index, row in data_frame.iterrows():
|
|
729
|
+
|
|
730
|
+
# Create the property values for this row
|
|
731
|
+
if domain_lookup[file_type]["domain"] is None:
|
|
732
|
+
properties = None
|
|
733
|
+
else:
|
|
734
|
+
column_to_scope = {
|
|
735
|
+
column.get("target", column.get("source")): column.get(
|
|
736
|
+
"scope", properties_scope
|
|
737
|
+
)
|
|
738
|
+
for column in property_columns
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
properties = cocoon.properties.create_property_values(
|
|
742
|
+
row=row,
|
|
743
|
+
column_to_scope=column_to_scope,
|
|
744
|
+
scope=properties_scope,
|
|
745
|
+
domain=domain_lookup[file_type]["domain"],
|
|
746
|
+
dtypes=property_dtypes,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# Create the sub-holding-keys for this row
|
|
750
|
+
if (
|
|
751
|
+
"sub_holding_keys" in open_api_types.keys()
|
|
752
|
+
and "Mapping" in open_api_types["sub_holding_keys"]
|
|
753
|
+
):
|
|
754
|
+
sub_holding_keys_row = cocoon.properties.create_property_values(
|
|
755
|
+
row=row,
|
|
756
|
+
column_to_scope={},
|
|
757
|
+
scope=sub_holding_keys_scope,
|
|
758
|
+
domain="Transaction",
|
|
759
|
+
dtypes=sub_holding_key_dtypes,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
# Create identifiers for this row if applicable
|
|
763
|
+
if instrument_identifier_mapping is None or not bool(
|
|
764
|
+
instrument_identifier_mapping
|
|
765
|
+
):
|
|
766
|
+
identifiers = None
|
|
767
|
+
else:
|
|
768
|
+
identifiers = cocoon.instruments.create_identifiers(
|
|
769
|
+
index=index,
|
|
770
|
+
row=row,
|
|
771
|
+
file_type=file_type,
|
|
772
|
+
instrument_identifier_mapping=instrument_identifier_mapping,
|
|
773
|
+
unique_identifiers=unique_identifiers,
|
|
774
|
+
full_key_format=kwargs["full_key_format"],
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
# Construct the from the mapping, properties and identifiers the single request object and add it to the list
|
|
778
|
+
single_requests.append(
|
|
779
|
+
cocoon.utilities.populate_model(
|
|
780
|
+
model_object_name=domain_lookup[file_type]["top_level_model"],
|
|
781
|
+
required_mapping=mapping_required,
|
|
782
|
+
optional_mapping=mapping_optional,
|
|
783
|
+
row=row,
|
|
784
|
+
properties=properties,
|
|
785
|
+
identifiers=identifiers,
|
|
786
|
+
sub_holding_keys=sub_holding_keys_row,
|
|
787
|
+
)
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
return single_requests
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
async def _construct_batches(
|
|
794
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
795
|
+
data_frame: pd.DataFrame,
|
|
796
|
+
mapping_required: dict,
|
|
797
|
+
mapping_optional: dict,
|
|
798
|
+
property_columns: list,
|
|
799
|
+
properties_scope: str,
|
|
800
|
+
instrument_identifier_mapping: dict,
|
|
801
|
+
batch_size: int,
|
|
802
|
+
file_type: str,
|
|
803
|
+
domain_lookup: dict,
|
|
804
|
+
sub_holding_keys: list,
|
|
805
|
+
sub_holding_keys_scope: str,
|
|
806
|
+
return_unmatched_items: bool,
|
|
807
|
+
**kwargs,
|
|
808
|
+
):
|
|
809
|
+
"""
|
|
810
|
+
This constructs the batches and asynchronously sends them to be loaded into LUSID
|
|
811
|
+
|
|
812
|
+
Parameters
|
|
813
|
+
----------
|
|
814
|
+
api_factory : lusid.SyncApiClientFactory
|
|
815
|
+
The api factory to use
|
|
816
|
+
data_frame : pd.DataFrame
|
|
817
|
+
The DataFrame containing the data to load
|
|
818
|
+
mapping_required : dict
|
|
819
|
+
The required mapping
|
|
820
|
+
mapping_optional : dict
|
|
821
|
+
The optional mapping
|
|
822
|
+
property_columns : list
|
|
823
|
+
The property columns to add as property values
|
|
824
|
+
properties_scope : str
|
|
825
|
+
The scope to add the property values in
|
|
826
|
+
instrument_identifier_mapping : dict
|
|
827
|
+
The mapping for the identifiers
|
|
828
|
+
batch_size : int
|
|
829
|
+
The batch size to use
|
|
830
|
+
file_type : str
|
|
831
|
+
The file type to load
|
|
832
|
+
domain_lookup : dict
|
|
833
|
+
The domain lookup
|
|
834
|
+
sub_holding_keys : list
|
|
835
|
+
The sub holding keys to use
|
|
836
|
+
sub_holding_keys_scope : str
|
|
837
|
+
The scope to use for the sub-holding keys
|
|
838
|
+
return_unmatched_items : bool
|
|
839
|
+
Whether items with unmatched identifiers should be returned for transaction or holding upserts
|
|
840
|
+
kwargs
|
|
841
|
+
Arguments specific to each call e.g. effective_at for holdings
|
|
842
|
+
|
|
843
|
+
Returns
|
|
844
|
+
-------
|
|
845
|
+
dict
|
|
846
|
+
Contains the success responses and the errors (where an API exception has been raised)
|
|
847
|
+
"""
|
|
848
|
+
|
|
849
|
+
# Get the different behaviours required for different entities e.g quotes can be batched without worrying about portfolios
|
|
850
|
+
batching_no_portfolios = [
|
|
851
|
+
file_type
|
|
852
|
+
for file_type, settings in domain_lookup.items()
|
|
853
|
+
if not settings["portfolio_specific"]
|
|
854
|
+
]
|
|
855
|
+
batching_with_portfolios = [
|
|
856
|
+
file_type
|
|
857
|
+
for file_type, settings in domain_lookup.items()
|
|
858
|
+
if settings["portfolio_specific"]
|
|
859
|
+
]
|
|
860
|
+
|
|
861
|
+
if file_type in batching_no_portfolios:
|
|
862
|
+
|
|
863
|
+
# Everything can be sent up asynchronously, prepare batches based on batch size alone
|
|
864
|
+
async_batches = [
|
|
865
|
+
data_frame.iloc[i: i + batch_size]
|
|
866
|
+
for i in range(0, len(data_frame), batch_size)
|
|
867
|
+
]
|
|
868
|
+
|
|
869
|
+
# Nest the async batches inside a single synchronous batch
|
|
870
|
+
sync_batches = [
|
|
871
|
+
{
|
|
872
|
+
"async_batches": async_batches,
|
|
873
|
+
"codes": [None] * len(async_batches),
|
|
874
|
+
"effective_at": [None] * len(async_batches),
|
|
875
|
+
}
|
|
876
|
+
]
|
|
877
|
+
|
|
878
|
+
elif file_type in batching_with_portfolios:
|
|
879
|
+
|
|
880
|
+
if "effective_at" in domain_lookup[file_type]["required_call_attributes"]:
|
|
881
|
+
|
|
882
|
+
# Get unique effective dates
|
|
883
|
+
unique_effective_dates = list(
|
|
884
|
+
data_frame[mapping_required["effective_at"]].unique()
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
# Create a group for each effective date as they can not be batched asynchronously
|
|
888
|
+
effective_at_groups = [
|
|
889
|
+
data_frame.loc[
|
|
890
|
+
data_frame[mapping_required["effective_at"]] == effective_at
|
|
891
|
+
]
|
|
892
|
+
for effective_at in unique_effective_dates
|
|
893
|
+
]
|
|
894
|
+
|
|
895
|
+
# Create a synchronous batch for each effective date
|
|
896
|
+
sync_batches = [
|
|
897
|
+
{
|
|
898
|
+
# Different portfolio codes can be batched asynchronously inside the synchronous batch
|
|
899
|
+
"async_batches": [
|
|
900
|
+
effective_at_group.loc[
|
|
901
|
+
data_frame[mapping_required["code"]] == code
|
|
902
|
+
]
|
|
903
|
+
for code in list(
|
|
904
|
+
effective_at_group[mapping_required["code"]].unique()
|
|
905
|
+
)
|
|
906
|
+
],
|
|
907
|
+
"codes": list(
|
|
908
|
+
effective_at_group[mapping_required["code"]].unique()
|
|
909
|
+
),
|
|
910
|
+
"effective_at": [
|
|
911
|
+
list(
|
|
912
|
+
effective_at_group[
|
|
913
|
+
mapping_required["effective_at"]
|
|
914
|
+
].unique()
|
|
915
|
+
)[0]
|
|
916
|
+
]
|
|
917
|
+
* len(list(effective_at_group[mapping_required["code"]].unique())),
|
|
918
|
+
}
|
|
919
|
+
for effective_at_group in effective_at_groups
|
|
920
|
+
]
|
|
921
|
+
|
|
922
|
+
else:
|
|
923
|
+
|
|
924
|
+
unique_portfolios = list(data_frame[mapping_required["code"]].unique())
|
|
925
|
+
|
|
926
|
+
# Different portfolio codes can be batched asynchronously
|
|
927
|
+
async_batches = [
|
|
928
|
+
data_frame.loc[data_frame[mapping_required["code"]] == code]
|
|
929
|
+
for code in unique_portfolios
|
|
930
|
+
]
|
|
931
|
+
|
|
932
|
+
# Inside the synchronous batch split the values for each portfolio into appropriate batch sizes
|
|
933
|
+
sync_batches = [
|
|
934
|
+
{
|
|
935
|
+
"async_batches": [
|
|
936
|
+
async_batch.iloc[i: i + batch_size]
|
|
937
|
+
for async_batch in async_batches
|
|
938
|
+
],
|
|
939
|
+
"codes": [str(code) for code in unique_portfolios],
|
|
940
|
+
"effective_at": [None] * len(async_batches),
|
|
941
|
+
}
|
|
942
|
+
for i in range(
|
|
943
|
+
0,
|
|
944
|
+
max([len(async_batch) for async_batch in async_batches]),
|
|
945
|
+
batch_size,
|
|
946
|
+
)
|
|
947
|
+
]
|
|
948
|
+
|
|
949
|
+
logging.debug("Created sync batches: ")
|
|
950
|
+
logging.debug(
|
|
951
|
+
f"Number of batches: {len(sync_batches)}, "
|
|
952
|
+
+ f"Number of items in batches: {sum([len(sync_batch['async_batches']) for sync_batch in sync_batches])}"
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
# Asynchronously load the data into LUSID
|
|
956
|
+
responses = [
|
|
957
|
+
await asyncio.gather(
|
|
958
|
+
*[
|
|
959
|
+
_load_data(
|
|
960
|
+
api_factory=api_factory,
|
|
961
|
+
single_requests=_convert_batch_to_models(
|
|
962
|
+
data_frame=async_batch,
|
|
963
|
+
mapping_required=mapping_required,
|
|
964
|
+
mapping_optional=mapping_optional,
|
|
965
|
+
property_columns=property_columns,
|
|
966
|
+
properties_scope=properties_scope,
|
|
967
|
+
instrument_identifier_mapping=instrument_identifier_mapping,
|
|
968
|
+
file_type=file_type,
|
|
969
|
+
domain_lookup=domain_lookup,
|
|
970
|
+
sub_holding_keys=sub_holding_keys,
|
|
971
|
+
sub_holding_keys_scope=sub_holding_keys_scope,
|
|
972
|
+
**kwargs,
|
|
973
|
+
),
|
|
974
|
+
file_type=file_type,
|
|
975
|
+
code=code,
|
|
976
|
+
effective_at=effective_at,
|
|
977
|
+
**kwargs,
|
|
978
|
+
)
|
|
979
|
+
for async_batch, code, effective_at in zip(
|
|
980
|
+
sync_batch["async_batches"],
|
|
981
|
+
sync_batch["codes"],
|
|
982
|
+
sync_batch["effective_at"],
|
|
983
|
+
)
|
|
984
|
+
if not async_batch.empty
|
|
985
|
+
],
|
|
986
|
+
return_exceptions=True,
|
|
987
|
+
)
|
|
988
|
+
for sync_batch in sync_batches
|
|
989
|
+
]
|
|
990
|
+
logging.debug("Flattening responses")
|
|
991
|
+
responses_flattened = [
|
|
992
|
+
response for responses_sub in responses for response in responses_sub
|
|
993
|
+
]
|
|
994
|
+
|
|
995
|
+
# Raise any internal exceptions rather than propagating them to the response
|
|
996
|
+
for response in responses_flattened:
|
|
997
|
+
if isinstance(response, Exception) and not isinstance(
|
|
998
|
+
response, lusid.exceptions.ApiException
|
|
999
|
+
):
|
|
1000
|
+
raise response
|
|
1001
|
+
|
|
1002
|
+
# Collects the exceptions as failures and successful calls as values
|
|
1003
|
+
returned_response = {
|
|
1004
|
+
"errors": [r for r in responses_flattened if isinstance(r, Exception)],
|
|
1005
|
+
"success": [r for r in responses_flattened if not isinstance(r, Exception)],
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
# For successful transactions or holdings file types, optionally return unmatched identifiers with the responses
|
|
1009
|
+
if check_for_unmatched_items(
|
|
1010
|
+
flag=return_unmatched_items,
|
|
1011
|
+
file_type=file_type,
|
|
1012
|
+
):
|
|
1013
|
+
logging.debug("returning unmatched identifiers with the responses")
|
|
1014
|
+
returned_response["unmatched_items"] = unmatched_items(
|
|
1015
|
+
api_factory=api_factory,
|
|
1016
|
+
scope=kwargs.get("scope", None),
|
|
1017
|
+
data_frame=data_frame,
|
|
1018
|
+
mapping_required=mapping_required,
|
|
1019
|
+
file_type=file_type,
|
|
1020
|
+
returned_response=returned_response,
|
|
1021
|
+
sync_batches=sync_batches,
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
return returned_response
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def check_for_unmatched_items(flag, file_type):
|
|
1028
|
+
"""
|
|
1029
|
+
This method contains the conditional logic to determine whether the unmatched_items validation should be run.
|
|
1030
|
+
It should not be run if:
|
|
1031
|
+
a) it was not requested
|
|
1032
|
+
b) the upload was not for transactions or holdings
|
|
1033
|
+
|
|
1034
|
+
Parameters
|
|
1035
|
+
----------
|
|
1036
|
+
flag : bool
|
|
1037
|
+
The bool flag that indicates whether any unmatched identifiers should be checked
|
|
1038
|
+
file_type : str
|
|
1039
|
+
The file type of the upload
|
|
1040
|
+
Returns
|
|
1041
|
+
-------
|
|
1042
|
+
|
|
1043
|
+
A boolean value indicating whether the unmatched_items validation should be completed
|
|
1044
|
+
"""
|
|
1045
|
+
condition_1 = flag is True
|
|
1046
|
+
condition_2 = file_type in ["transaction", "holding"]
|
|
1047
|
+
|
|
1048
|
+
return condition_1 and condition_2
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
@checkargs
|
|
1052
|
+
def unmatched_items(
|
|
1053
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
1054
|
+
scope: str,
|
|
1055
|
+
data_frame: pd.DataFrame,
|
|
1056
|
+
mapping_required: dict,
|
|
1057
|
+
file_type: str,
|
|
1058
|
+
returned_response: dict,
|
|
1059
|
+
sync_batches: list = None,
|
|
1060
|
+
):
|
|
1061
|
+
"""
|
|
1062
|
+
This method orchestrates the identification of holdings or transactions objects that were successfully uploaded
|
|
1063
|
+
but did not resolve to known identifiers (i.e. where the instrument_uid is LUID_ZZZZZZ).
|
|
1064
|
+
|
|
1065
|
+
If there were any errors in the attempted upload, the check will not complete and an error message will be returned
|
|
1066
|
+
to let the user know that errors will need to be resolved first.
|
|
1067
|
+
|
|
1068
|
+
Parameters
|
|
1069
|
+
----------
|
|
1070
|
+
api_factory : lusid.SyncApiClientFactory api_factory
|
|
1071
|
+
The api factory to use
|
|
1072
|
+
scope : str
|
|
1073
|
+
The scope of the resource to load the data into
|
|
1074
|
+
data_frame : pd.DataFrame
|
|
1075
|
+
The DataFrame containing the data
|
|
1076
|
+
mapping_required : dict
|
|
1077
|
+
The required mapping
|
|
1078
|
+
file_type : str
|
|
1079
|
+
The type of file e.g. transactions, instruments, holdings, quotes, portfolios
|
|
1080
|
+
returned_response : dict
|
|
1081
|
+
The response from laod_from_data_frame
|
|
1082
|
+
sync_batches : list
|
|
1083
|
+
A list of the batches used to upload the data into LUSID.
|
|
1084
|
+
|
|
1085
|
+
Returns
|
|
1086
|
+
-------
|
|
1087
|
+
responses: list
|
|
1088
|
+
A list of objects to be appended to the ultimate response for load_from_data_frame.
|
|
1089
|
+
"""
|
|
1090
|
+
|
|
1091
|
+
if len(returned_response["errors"]) > 0:
|
|
1092
|
+
return ["Please resolve all upload errors to check for unmatched items."]
|
|
1093
|
+
|
|
1094
|
+
if file_type == "transaction":
|
|
1095
|
+
return _unmatched_transactions(
|
|
1096
|
+
api_factory=api_factory,
|
|
1097
|
+
scope=scope,
|
|
1098
|
+
data_frame=data_frame,
|
|
1099
|
+
mapping_required=mapping_required,
|
|
1100
|
+
sync_batches=sync_batches,
|
|
1101
|
+
)
|
|
1102
|
+
elif file_type == "holding":
|
|
1103
|
+
return _unmatched_holdings(
|
|
1104
|
+
api_factory=api_factory,
|
|
1105
|
+
scope=scope,
|
|
1106
|
+
sync_batches=sync_batches,
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def _unmatched_transactions(
|
|
1111
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
1112
|
+
scope: str,
|
|
1113
|
+
data_frame: pd.DataFrame,
|
|
1114
|
+
mapping_required: dict,
|
|
1115
|
+
sync_batches: list = None,
|
|
1116
|
+
):
|
|
1117
|
+
"""
|
|
1118
|
+
This method identifies which instruments were not resolved with a transaction upload using load_from_data_frame.
|
|
1119
|
+
|
|
1120
|
+
Parameters
|
|
1121
|
+
----------
|
|
1122
|
+
api_factory : lusid.SyncApiClientFactory api_factory
|
|
1123
|
+
The api factory to use
|
|
1124
|
+
scope : str
|
|
1125
|
+
The scope of the resource to load the data into
|
|
1126
|
+
data_frame : pd.DataFrame
|
|
1127
|
+
The DataFrame containing the data
|
|
1128
|
+
sync_batches : list
|
|
1129
|
+
A list of the batches used to upload the data into LUSID.
|
|
1130
|
+
|
|
1131
|
+
Returns
|
|
1132
|
+
-------
|
|
1133
|
+
responses: list
|
|
1134
|
+
A list of transaction objects to be appended to the ultimate response for load_from_data_frame.
|
|
1135
|
+
"""
|
|
1136
|
+
# Extract a list of portfolio codes from the sync_batches
|
|
1137
|
+
portfolio_codes = extract_unique_portfolio_codes(sync_batches)
|
|
1138
|
+
|
|
1139
|
+
# Create empty list to hold transaction ids and instruments
|
|
1140
|
+
unmatched_transactions = []
|
|
1141
|
+
|
|
1142
|
+
# For each portfolio, request the unmatched transactions from LUSID and append to the instantiated list
|
|
1143
|
+
for portfolio_code in portfolio_codes:
|
|
1144
|
+
portfolio_transactions = data_frame.loc[
|
|
1145
|
+
data_frame[mapping_required["code"]] == portfolio_code
|
|
1146
|
+
]
|
|
1147
|
+
from_transaction_date = min(
|
|
1148
|
+
portfolio_transactions[mapping_required["transaction_date"]].apply(
|
|
1149
|
+
lambda x: str(DateOrCutLabel(x))
|
|
1150
|
+
)
|
|
1151
|
+
)
|
|
1152
|
+
to_transactions_date = max(
|
|
1153
|
+
portfolio_transactions[mapping_required["transaction_date"]].apply(
|
|
1154
|
+
lambda x: str(DateOrCutLabel(x))
|
|
1155
|
+
)
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
unmatched_transactions.extend(
|
|
1159
|
+
return_unmatched_transactions(
|
|
1160
|
+
api_factory=api_factory,
|
|
1161
|
+
scope=scope,
|
|
1162
|
+
code=portfolio_code,
|
|
1163
|
+
from_transaction_date=from_transaction_date,
|
|
1164
|
+
to_transaction_date=to_transactions_date,
|
|
1165
|
+
)
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
# With the upload dataframe, filter out any transactions from the list that were not part of this upload and return
|
|
1169
|
+
return filter_unmatched_transactions(
|
|
1170
|
+
data_frame=data_frame,
|
|
1171
|
+
mapping_required=mapping_required,
|
|
1172
|
+
unmatched_transactions=unmatched_transactions,
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def return_unmatched_transactions(
|
|
1177
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
1178
|
+
scope: str,
|
|
1179
|
+
code: str,
|
|
1180
|
+
from_transaction_date: str,
|
|
1181
|
+
to_transaction_date: str,
|
|
1182
|
+
):
|
|
1183
|
+
"""
|
|
1184
|
+
Call the get transactions api and only return those transactions with unresolved identifiers.
|
|
1185
|
+
|
|
1186
|
+
Multiple pages of responses for the get_transactions api call are handled.
|
|
1187
|
+
|
|
1188
|
+
Parameters
|
|
1189
|
+
----------
|
|
1190
|
+
api_factory : lusid.SyncApiClientFactory api_factory
|
|
1191
|
+
The api factory to use
|
|
1192
|
+
scope : str
|
|
1193
|
+
The scope of the resource to load the data into
|
|
1194
|
+
code : str
|
|
1195
|
+
The code of the portfolio containing the transactions we want to return
|
|
1196
|
+
|
|
1197
|
+
Returns
|
|
1198
|
+
-------
|
|
1199
|
+
A list of transaction objects with the structure.
|
|
1200
|
+
"""
|
|
1201
|
+
transactions_api = api_factory.build(lusid.api.TransactionPortfoliosApi)
|
|
1202
|
+
done = False
|
|
1203
|
+
next_page = None
|
|
1204
|
+
unmatched_transactions = []
|
|
1205
|
+
|
|
1206
|
+
# We need to handle the possibility of paginated results
|
|
1207
|
+
while not done:
|
|
1208
|
+
|
|
1209
|
+
# There must be a filter included so only transactions with unmatched instruments are returned
|
|
1210
|
+
kwargs = {
|
|
1211
|
+
"scope": scope,
|
|
1212
|
+
"code": code,
|
|
1213
|
+
"from_transaction_date": from_transaction_date,
|
|
1214
|
+
"to_transaction_date": to_transaction_date,
|
|
1215
|
+
"filter": "instrumentUid eq'LUID_ZZZZZZZZ'",
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if next_page is not None:
|
|
1219
|
+
kwargs["page"] = next_page
|
|
1220
|
+
|
|
1221
|
+
response = transactions_api.get_transactions(**kwargs)
|
|
1222
|
+
|
|
1223
|
+
unmatched_transactions.extend([response for response in response.values])
|
|
1224
|
+
|
|
1225
|
+
next_page = response.next_page
|
|
1226
|
+
done = response.next_page is None
|
|
1227
|
+
|
|
1228
|
+
return unmatched_transactions
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def filter_unmatched_transactions(
|
|
1232
|
+
data_frame: pd.DataFrame,
|
|
1233
|
+
mapping_required: dict,
|
|
1234
|
+
unmatched_transactions: list,
|
|
1235
|
+
):
|
|
1236
|
+
"""
|
|
1237
|
+
This method will take the full list of unmatched transactions and remove any transactions that were not
|
|
1238
|
+
part of the current upload with load_from_data_frame.
|
|
1239
|
+
|
|
1240
|
+
Parameters
|
|
1241
|
+
----------
|
|
1242
|
+
data_frame : pd.DataFrame
|
|
1243
|
+
The DataFrame containing the data
|
|
1244
|
+
mapping_required : dict
|
|
1245
|
+
The required mapping from load_from_data_frame that helps identify the transaction_id column in the DataFrame
|
|
1246
|
+
unmatched_transactions : list
|
|
1247
|
+
A list of transaction objects.
|
|
1248
|
+
|
|
1249
|
+
Returns
|
|
1250
|
+
-------
|
|
1251
|
+
A filtered list of transaction objects.
|
|
1252
|
+
"""
|
|
1253
|
+
# Create a unique list of transaction_ids from the dataframe
|
|
1254
|
+
valid_txids = set(data_frame[mapping_required.get("transaction_id")])
|
|
1255
|
+
|
|
1256
|
+
# Iterate through the transactions and if the transaction_id is in the set, add it to the list to be returned
|
|
1257
|
+
filtered_unmatched_transactions = [
|
|
1258
|
+
unmatched_transaction
|
|
1259
|
+
for unmatched_transaction in unmatched_transactions
|
|
1260
|
+
if unmatched_transaction.transaction_id in valid_txids
|
|
1261
|
+
]
|
|
1262
|
+
|
|
1263
|
+
return filtered_unmatched_transactions
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
def _unmatched_holdings(
|
|
1267
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
1268
|
+
scope: str,
|
|
1269
|
+
sync_batches: list = None,
|
|
1270
|
+
):
|
|
1271
|
+
"""
|
|
1272
|
+
This method identifies which instruments were not resolved with a holdings upload using load_from_data_frame.
|
|
1273
|
+
|
|
1274
|
+
Parameters
|
|
1275
|
+
----------
|
|
1276
|
+
api_factory : lusid.SyncApiClientFactory api_factory
|
|
1277
|
+
The api factory to use
|
|
1278
|
+
scope : str
|
|
1279
|
+
The scope of the resource to load the data into
|
|
1280
|
+
sync_batches : list
|
|
1281
|
+
A list of the batches used to upload the data into LUSID
|
|
1282
|
+
|
|
1283
|
+
Returns
|
|
1284
|
+
-------
|
|
1285
|
+
responses: list
|
|
1286
|
+
A list of holding objects to be appended to the ultimate response for load_from_data_frame.
|
|
1287
|
+
"""
|
|
1288
|
+
# Extract a list of tuples of portfolio codes and effective at times from sync_batches
|
|
1289
|
+
code_tuples = extract_unique_portfolio_codes_effective_at_tuples(sync_batches)
|
|
1290
|
+
|
|
1291
|
+
# Create empty list to hold holdings that have not been resolved
|
|
1292
|
+
unmatched_holdings = []
|
|
1293
|
+
|
|
1294
|
+
# For each holding adjustment in the upload, check whether any contained unresolved instruments and append to list
|
|
1295
|
+
for code_tuple in code_tuples:
|
|
1296
|
+
unmatched_holdings.extend(
|
|
1297
|
+
return_unmatched_holdings(
|
|
1298
|
+
api_factory=api_factory,
|
|
1299
|
+
scope=scope,
|
|
1300
|
+
code_tuple=code_tuple,
|
|
1301
|
+
)
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
return unmatched_holdings
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def return_unmatched_holdings(
|
|
1308
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
1309
|
+
scope: str,
|
|
1310
|
+
code_tuple: Tuple[str, str],
|
|
1311
|
+
):
|
|
1312
|
+
"""
|
|
1313
|
+
Call the get holdings adjustments api and return a list of holding objects that have unresolved identifiers.
|
|
1314
|
+
|
|
1315
|
+
Parameters
|
|
1316
|
+
----------
|
|
1317
|
+
api_factory : lusid.SyncApiClientFactory api_factory
|
|
1318
|
+
The api factory to use
|
|
1319
|
+
scope : str
|
|
1320
|
+
The scope of the resource to load the data into
|
|
1321
|
+
code_tuple : (str, str)
|
|
1322
|
+
A tuple of format (portfolio_code, effective_at) which represents the combination of values
|
|
1323
|
+
used when upserting holdings during load_from_data_frame for file_type 'holding'
|
|
1324
|
+
|
|
1325
|
+
Returns
|
|
1326
|
+
-------
|
|
1327
|
+
A list of holding objects.
|
|
1328
|
+
|
|
1329
|
+
"""
|
|
1330
|
+
transactions_api = api_factory.build(lusid.api.TransactionPortfoliosApi)
|
|
1331
|
+
|
|
1332
|
+
# In case a holdings check occurs for a portfolio code and effective at combination that contain no holdings,
|
|
1333
|
+
# make sure to gracefully handle the exception so that any LUSID error from the main upload is not suppressed.
|
|
1334
|
+
try:
|
|
1335
|
+
response = transactions_api.get_holdings_adjustment(
|
|
1336
|
+
scope=scope,
|
|
1337
|
+
code=code_tuple[0],
|
|
1338
|
+
effective_at=str(DateOrCutLabel(code_tuple[1])),
|
|
1339
|
+
)
|
|
1340
|
+
except lusid.ApiException as e:
|
|
1341
|
+
if "HoldingsAdjustmentDoesNotExist" in str(e.body):
|
|
1342
|
+
logging.info(
|
|
1343
|
+
f"While validating holding upload, LUSID was unable to find any holdings within portfolio "
|
|
1344
|
+
f"{code_tuple[0]} for effectiveAt {str(DateOrCutLabel(code_tuple[1]))}."
|
|
1345
|
+
)
|
|
1346
|
+
return []
|
|
1347
|
+
|
|
1348
|
+
# Create a list of holding objects with unmatched identifiers (e.g. with LUID_ZZZZZZZZ) from the response
|
|
1349
|
+
return [
|
|
1350
|
+
adjustment
|
|
1351
|
+
for adjustment in response.adjustments
|
|
1352
|
+
if adjustment.instrument_uid == "LUID_ZZZZZZZZ"
|
|
1353
|
+
]
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
@checkargs
|
|
1357
|
+
def load_from_data_frame(
|
|
1358
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
1359
|
+
scope: str,
|
|
1360
|
+
data_frame: pd.DataFrame,
|
|
1361
|
+
mapping_required: dict,
|
|
1362
|
+
mapping_optional: dict,
|
|
1363
|
+
file_type: str,
|
|
1364
|
+
identifier_mapping: dict = None,
|
|
1365
|
+
property_columns: list = None,
|
|
1366
|
+
properties_scope: str = None,
|
|
1367
|
+
batch_size: int = None,
|
|
1368
|
+
remove_white_space: bool = True,
|
|
1369
|
+
instrument_name_enrichment: bool = False,
|
|
1370
|
+
transactions_commit_mode: str = None,
|
|
1371
|
+
sub_holding_keys: list = None,
|
|
1372
|
+
holdings_adjustment_only: bool = False,
|
|
1373
|
+
thread_pool_max_workers: int = 5,
|
|
1374
|
+
sub_holding_keys_scope: str = None,
|
|
1375
|
+
return_unmatched_items: bool = False,
|
|
1376
|
+
instrument_scope: str = None,
|
|
1377
|
+
):
|
|
1378
|
+
"""
|
|
1379
|
+
|
|
1380
|
+
Parameters
|
|
1381
|
+
----------
|
|
1382
|
+
api_factory : lusid.SyncApiClientFactory api_factory
|
|
1383
|
+
The api factory to use
|
|
1384
|
+
scope : str
|
|
1385
|
+
The scope of the resource to load the data into
|
|
1386
|
+
If file_type="instrument" scope will only be used for instrument properties
|
|
1387
|
+
data_frame : pd.DataFrame
|
|
1388
|
+
The DataFrame containing the data
|
|
1389
|
+
mapping_required : dict{str, str}
|
|
1390
|
+
The dictionary mapping the DataFrame columns to LUSID's required attributes
|
|
1391
|
+
mapping_optional : dict{str, str}
|
|
1392
|
+
The dictionary mapping the DataFrame columns to LUSID's optional attributes
|
|
1393
|
+
file_type : str
|
|
1394
|
+
The type of file e.g. transactions, instruments, holdings, quotes, portfolios
|
|
1395
|
+
identifier_mapping : dict{str, str}
|
|
1396
|
+
The dictionary mapping of LUSID instrument identifiers to identifiers in the DataFrame
|
|
1397
|
+
property_columns : list
|
|
1398
|
+
The columns to create properties for
|
|
1399
|
+
properties_scope : str
|
|
1400
|
+
The scope to add the properties to
|
|
1401
|
+
batch_size : int
|
|
1402
|
+
The size of the batch to use when using upsert calls e.g. upsert instruments, upsert quotes etc.
|
|
1403
|
+
remove_white_space : bool
|
|
1404
|
+
remove whitespace either side of each value in the dataframe
|
|
1405
|
+
instrument_name_enrichment : bool
|
|
1406
|
+
request additional identifier information from open-figi
|
|
1407
|
+
sub_holding_keys : list
|
|
1408
|
+
The sub holding keys to use for this request. Can be a list of property keys or a list of
|
|
1409
|
+
columns in the dataframe to use to create sub holdings
|
|
1410
|
+
holdings_adjustment_only : bool
|
|
1411
|
+
Whether to use the adjust_holdings api call rather than set_holdings when working with holdings
|
|
1412
|
+
thread_pool_max_workers : int
|
|
1413
|
+
The maximum number of workers to use in the thread pool used by the function
|
|
1414
|
+
sub_holding_keys_scope : str
|
|
1415
|
+
The scope to add the sub-holding keys to
|
|
1416
|
+
return_unmatched_items : bool
|
|
1417
|
+
When loading transactions or holdings, a 'True' flag will return a list of the transaction or holding
|
|
1418
|
+
objects where their instruments were unmatched at the time of the upsert
|
|
1419
|
+
(i.e. where instrument_uid is LUID_ZZZZZZ). This parameter will be ignored for file types other than
|
|
1420
|
+
transactions or holdings
|
|
1421
|
+
instrument_scope : str
|
|
1422
|
+
The scope to upsert to when upseting instrument
|
|
1423
|
+
|
|
1424
|
+
Returns
|
|
1425
|
+
-------
|
|
1426
|
+
responses: dict
|
|
1427
|
+
The responses from loading the data into LUSID
|
|
1428
|
+
|
|
1429
|
+
Examples
|
|
1430
|
+
--------
|
|
1431
|
+
|
|
1432
|
+
* Loading Instruments
|
|
1433
|
+
|
|
1434
|
+
.. code-block:: none
|
|
1435
|
+
|
|
1436
|
+
result = finbourne_sdk_utils.cocoon.load_from_data_frame(
|
|
1437
|
+
api_factory=api_factory,
|
|
1438
|
+
scope=scope,
|
|
1439
|
+
data_frame=instr_df,
|
|
1440
|
+
mapping_required=mapping["instruments"]["required"],
|
|
1441
|
+
mapping_optional={},
|
|
1442
|
+
file_type="instruments",
|
|
1443
|
+
identifier_mapping=mapping["instruments"]["identifier_mapping"],
|
|
1444
|
+
property_columns=mapping["instruments"]["properties"],
|
|
1445
|
+
properties_scope=scope,
|
|
1446
|
+
instrument_scope=scope
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
* Loading Instrument Properties
|
|
1450
|
+
|
|
1451
|
+
.. code-block:: none
|
|
1452
|
+
|
|
1453
|
+
result = finbourne_sdk_utils.cocoon.load_from_data_frame(
|
|
1454
|
+
api_factory=api_factory,
|
|
1455
|
+
scope=scope,
|
|
1456
|
+
data_frame=strat_properties,
|
|
1457
|
+
mapping_required=strat_mapping,
|
|
1458
|
+
mapping_optional={},
|
|
1459
|
+
file_type="instrument_property",
|
|
1460
|
+
property_columns=["block tag"],
|
|
1461
|
+
properties_scope=scope
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
* Loading Portfolios
|
|
1465
|
+
|
|
1466
|
+
.. code-block:: none
|
|
1467
|
+
|
|
1468
|
+
result = finbourne_sdk_utils.cocoon.load_from_data_frame(
|
|
1469
|
+
api_factory=api_factory,
|
|
1470
|
+
scope=scope,
|
|
1471
|
+
data_frame=portfolios,
|
|
1472
|
+
mapping_required=mapping["portfolios"]["required"],
|
|
1473
|
+
mapping_optional={},
|
|
1474
|
+
file_type="portfolios"
|
|
1475
|
+
)
|
|
1476
|
+
|
|
1477
|
+
* Loading Transactions
|
|
1478
|
+
|
|
1479
|
+
.. code-block:: none
|
|
1480
|
+
|
|
1481
|
+
result = finbourne_sdk_utils.cocoon.load_from_data_frame(
|
|
1482
|
+
api_factory=api_factory,
|
|
1483
|
+
scope=scope,
|
|
1484
|
+
data_frame=txn_df,
|
|
1485
|
+
mapping_required=mapping["transactions"]["required"],
|
|
1486
|
+
mapping_optional=mapping["transactions"]["optional"],
|
|
1487
|
+
file_type="transactions",
|
|
1488
|
+
identifier_mapping=mapping["transactions"]["identifier_mapping"],
|
|
1489
|
+
property_columns=mapping["transactions"]["properties"],
|
|
1490
|
+
properties_scope=scope
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
* Loading Transactions with Commit Mode
|
|
1494
|
+
|
|
1495
|
+
.. code-block:: none
|
|
1496
|
+
|
|
1497
|
+
result = finbourne_sdk_utils.cocoon.load_from_data_frame(
|
|
1498
|
+
api_factory=api_factory,
|
|
1499
|
+
scope=scope,
|
|
1500
|
+
data_frame=txn_df,
|
|
1501
|
+
mapping_required=mapping["transactions"]["required"],
|
|
1502
|
+
mapping_optional=mapping["transactions"]["optional"],
|
|
1503
|
+
file_type="transactions_with_commit",
|
|
1504
|
+
transactions_commit_mode="(Atomic|Partial)"
|
|
1505
|
+
identifier_mapping=mapping["transactions"]["identifier_mapping"],
|
|
1506
|
+
property_columns=mapping["transactions"]["properties"],
|
|
1507
|
+
properties_scope=scope
|
|
1508
|
+
)
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
* Loading Quotes
|
|
1512
|
+
|
|
1513
|
+
.. code-block:: none
|
|
1514
|
+
|
|
1515
|
+
result = lpt.load_from_data_frame(
|
|
1516
|
+
api_factory=api_factory,
|
|
1517
|
+
scope=scope,
|
|
1518
|
+
data_frame=df_adjusted_quotes,
|
|
1519
|
+
mapping_required=mapping["quotes"]["required"],
|
|
1520
|
+
mapping_optional={},
|
|
1521
|
+
file_type="quotes"
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1524
|
+
* loading Holdings
|
|
1525
|
+
|
|
1526
|
+
.. code-block:: none
|
|
1527
|
+
|
|
1528
|
+
result = lpt.load_from_data_frame(
|
|
1529
|
+
api_factory=api_factory,
|
|
1530
|
+
scope=holdings_scope,
|
|
1531
|
+
data_frame=seg_df,
|
|
1532
|
+
mapping_required=mapping["holdings"]["required"],
|
|
1533
|
+
mapping_optional=mapping["holdings"]["optional"],
|
|
1534
|
+
identifier_mapping=holdings_mapping["holdings"]["identifier_mapping"],
|
|
1535
|
+
file_type="holdings"
|
|
1536
|
+
)
|
|
1537
|
+
|
|
1538
|
+
"""
|
|
1539
|
+
|
|
1540
|
+
# A mapping between the file type and relevant attributes e.g. domain, top_level_model etc.
|
|
1541
|
+
domain_lookup = cocoon.utilities.load_json_file("config/domain_settings.json")
|
|
1542
|
+
|
|
1543
|
+
# Convert the file type to lower case & singular as well as checking it is of the allowed value
|
|
1544
|
+
file_type = (
|
|
1545
|
+
Validator(file_type, "file_type")
|
|
1546
|
+
.make_singular()
|
|
1547
|
+
.make_lower()
|
|
1548
|
+
.check_allowed_value(list(domain_lookup.keys()))
|
|
1549
|
+
.value
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
# Ensures that it is a single index dataframe
|
|
1553
|
+
Validator(data_frame.index, "data_frame_index").check_is_not_instance(pd.MultiIndex)
|
|
1554
|
+
|
|
1555
|
+
# Set defaults aligned with the data type of each argument, this allows for users to provide None
|
|
1556
|
+
identifier_mapping = (
|
|
1557
|
+
Validator(identifier_mapping, "identifier_mapping")
|
|
1558
|
+
.set_default_value_if_none(default={})
|
|
1559
|
+
.discard_dict_keys_none_value()
|
|
1560
|
+
.value
|
|
1561
|
+
)
|
|
1562
|
+
|
|
1563
|
+
properties_scope = (
|
|
1564
|
+
Validator(properties_scope, "properties_scope")
|
|
1565
|
+
.set_default_value_if_none(default=scope)
|
|
1566
|
+
.value
|
|
1567
|
+
)
|
|
1568
|
+
|
|
1569
|
+
sub_holding_keys_scope = (
|
|
1570
|
+
Validator(sub_holding_keys_scope, "sub_holding_keys_scope")
|
|
1571
|
+
.set_default_value_if_none(default=properties_scope)
|
|
1572
|
+
.value
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
instrument_scope = (
|
|
1576
|
+
Validator(instrument_scope, "instrument_scope")
|
|
1577
|
+
.set_default_value_if_none(default="default")
|
|
1578
|
+
.value
|
|
1579
|
+
)
|
|
1580
|
+
|
|
1581
|
+
property_columns = (
|
|
1582
|
+
Validator(property_columns, "property_columns")
|
|
1583
|
+
.set_default_value_if_none(default=[])
|
|
1584
|
+
.value
|
|
1585
|
+
)
|
|
1586
|
+
|
|
1587
|
+
property_columns = [
|
|
1588
|
+
{"source": column, "target": column} if isinstance(column, str) else column
|
|
1589
|
+
for column in property_columns
|
|
1590
|
+
]
|
|
1591
|
+
|
|
1592
|
+
Validator(
|
|
1593
|
+
property_columns, "property_columns"
|
|
1594
|
+
).check_entries_are_strings_or_dict_containing_key("source")
|
|
1595
|
+
|
|
1596
|
+
sub_holding_keys = (
|
|
1597
|
+
Validator(sub_holding_keys, "sub_holding_keys")
|
|
1598
|
+
.set_default_value_if_none(default=[])
|
|
1599
|
+
.value
|
|
1600
|
+
)
|
|
1601
|
+
|
|
1602
|
+
batch_size = (
|
|
1603
|
+
Validator(batch_size, "batch_size")
|
|
1604
|
+
.set_default_value_if_none(domain_lookup[file_type]["default_batch_size"])
|
|
1605
|
+
.override_value(
|
|
1606
|
+
not domain_lookup[file_type]["batch_allowed"],
|
|
1607
|
+
domain_lookup[file_type]["default_batch_size"],
|
|
1608
|
+
)
|
|
1609
|
+
.value
|
|
1610
|
+
)
|
|
1611
|
+
|
|
1612
|
+
# Discard mappings where the provided value is None
|
|
1613
|
+
mapping_required = (
|
|
1614
|
+
Validator(mapping_required, "mapping_required")
|
|
1615
|
+
.discard_dict_keys_none_value()
|
|
1616
|
+
.value
|
|
1617
|
+
)
|
|
1618
|
+
|
|
1619
|
+
mapping_optional = (
|
|
1620
|
+
Validator(mapping_optional, "mapping_optional")
|
|
1621
|
+
.discard_dict_keys_none_value()
|
|
1622
|
+
.value
|
|
1623
|
+
)
|
|
1624
|
+
|
|
1625
|
+
required_call_attributes = domain_lookup[file_type]["required_call_attributes"]
|
|
1626
|
+
if "scope" in required_call_attributes:
|
|
1627
|
+
required_call_attributes.remove("scope")
|
|
1628
|
+
|
|
1629
|
+
# Check that all required parameters exist
|
|
1630
|
+
Validator(
|
|
1631
|
+
required_call_attributes, "required_attributes_for_call"
|
|
1632
|
+
).check_subset_of_list(list(mapping_required.keys()), "required_mapping")
|
|
1633
|
+
|
|
1634
|
+
# Verify that all the required attributes for this top level model exist in the provided required mapping
|
|
1635
|
+
cocoon.utilities.verify_all_required_attributes_mapped(
|
|
1636
|
+
mapping=mapping_required,
|
|
1637
|
+
model_object_name=domain_lookup[file_type]["top_level_model"],
|
|
1638
|
+
exempt_attributes=["identifiers", "properties", "instrument_identifiers"],
|
|
1639
|
+
)
|
|
1640
|
+
|
|
1641
|
+
# Create the thread pool to use with the async_tools.run_in_executor decorator to make sync functions awaitable
|
|
1642
|
+
thread_pool = ThreadPool(thread_pool_max_workers).thread_pool
|
|
1643
|
+
|
|
1644
|
+
if instrument_name_enrichment:
|
|
1645
|
+
loop = cocoon.async_tools.start_event_loop_new_thread()
|
|
1646
|
+
|
|
1647
|
+
data_frame, mapping_required = asyncio.run_coroutine_threadsafe(
|
|
1648
|
+
cocoon.instruments.enrich_instruments(
|
|
1649
|
+
api_factory=api_factory,
|
|
1650
|
+
data_frame=data_frame,
|
|
1651
|
+
instrument_identifier_mapping=identifier_mapping,
|
|
1652
|
+
mapping_required=mapping_required,
|
|
1653
|
+
constant_prefix="$",
|
|
1654
|
+
**{"thread_pool": thread_pool},
|
|
1655
|
+
),
|
|
1656
|
+
loop,
|
|
1657
|
+
).result()
|
|
1658
|
+
|
|
1659
|
+
# Stop the additional event loop
|
|
1660
|
+
cocoon.async_tools.stop_event_loop_new_thread(loop)
|
|
1661
|
+
|
|
1662
|
+
"""
|
|
1663
|
+
Unnest and populate defaults where a mapping is provided with column and/or default fields in a nested dictionary
|
|
1664
|
+
|
|
1665
|
+
e.g.
|
|
1666
|
+
{'name': {
|
|
1667
|
+
'column': 'instrument_name',
|
|
1668
|
+
'default': 'unknown_name'
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
rather than simply
|
|
1673
|
+
{'name': 'instrument_name'}
|
|
1674
|
+
"""
|
|
1675
|
+
(
|
|
1676
|
+
data_frame,
|
|
1677
|
+
mapping_required,
|
|
1678
|
+
) = cocoon.utilities.handle_nested_default_and_column_mapping(
|
|
1679
|
+
data_frame=data_frame, mapping=mapping_required, constant_prefix="$"
|
|
1680
|
+
)
|
|
1681
|
+
(
|
|
1682
|
+
data_frame,
|
|
1683
|
+
mapping_optional,
|
|
1684
|
+
) = cocoon.utilities.handle_nested_default_and_column_mapping(
|
|
1685
|
+
data_frame=data_frame, mapping=mapping_optional, constant_prefix="$"
|
|
1686
|
+
)
|
|
1687
|
+
|
|
1688
|
+
# Get all the DataFrame columns as well as those that contain at least one null value
|
|
1689
|
+
data_frame_columns = list(data_frame.columns.values)
|
|
1690
|
+
nan_columns = [
|
|
1691
|
+
column for column in data_frame_columns if data_frame[column].isna().any()
|
|
1692
|
+
]
|
|
1693
|
+
|
|
1694
|
+
# Validate that none of the provided columns are missing or invalid
|
|
1695
|
+
Validator(
|
|
1696
|
+
mapping_required, "mapping_required"
|
|
1697
|
+
).get_dict_values().filter_list_using_first_character("$").check_subset_of_list(
|
|
1698
|
+
data_frame_columns, "DataFrame Columns"
|
|
1699
|
+
).check_no_intersection_with_list(
|
|
1700
|
+
nan_columns, "Columns with Missing Values"
|
|
1701
|
+
)
|
|
1702
|
+
|
|
1703
|
+
Validator(
|
|
1704
|
+
mapping_optional, "mapping_optional"
|
|
1705
|
+
).get_dict_values().filter_list_using_first_character("$").check_subset_of_list(
|
|
1706
|
+
data_frame_columns, "DataFrame Columns"
|
|
1707
|
+
)
|
|
1708
|
+
|
|
1709
|
+
Validator(
|
|
1710
|
+
identifier_mapping, "identifier_mapping"
|
|
1711
|
+
).get_dict_values().filter_list_using_first_character("$").check_subset_of_list(
|
|
1712
|
+
data_frame_columns, "DataFrame Columns"
|
|
1713
|
+
)
|
|
1714
|
+
|
|
1715
|
+
source_columns = [column["source"] for column in property_columns]
|
|
1716
|
+
Validator(source_columns, "property_columns").check_subset_of_list(
|
|
1717
|
+
data_frame_columns, "DataFrame Columns"
|
|
1718
|
+
)
|
|
1719
|
+
|
|
1720
|
+
# Converts higher level data types such as dictionaries and lists to strings
|
|
1721
|
+
data_frame = data_frame.applymap(cocoon.utilities.convert_cell_value_to_string)
|
|
1722
|
+
|
|
1723
|
+
if remove_white_space:
|
|
1724
|
+
column_list = [source_columns]
|
|
1725
|
+
for col in [mapping_optional, mapping_required, identifier_mapping]:
|
|
1726
|
+
column_list.append(col.values())
|
|
1727
|
+
|
|
1728
|
+
column_list = list(set([item for sublist in column_list for item in sublist]))
|
|
1729
|
+
data_frame = strip_whitespace(data_frame, column_list)
|
|
1730
|
+
|
|
1731
|
+
# Get the types of the attributes on the top level model for this request
|
|
1732
|
+
|
|
1733
|
+
open_api_types = get_attributes_and_types(getattr(
|
|
1734
|
+
lusid.models, domain_lookup[file_type]["top_level_model"]
|
|
1735
|
+
))
|
|
1736
|
+
|
|
1737
|
+
# If there is a sub_holding_keys attribute and it has a dict type this means the sub_holding_keys
|
|
1738
|
+
# need to have a property definition and be populated with values from the provided dataframe columns
|
|
1739
|
+
if (
|
|
1740
|
+
"sub_holding_keys" in open_api_types.keys()
|
|
1741
|
+
and "Mapping[" in open_api_types["sub_holding_keys"]
|
|
1742
|
+
):
|
|
1743
|
+
Validator(sub_holding_keys, "sub_holding_key_columns").check_subset_of_list(
|
|
1744
|
+
data_frame_columns, "DataFrame Columns"
|
|
1745
|
+
)
|
|
1746
|
+
|
|
1747
|
+
# Check for and create missing property definitions for the sub-holding-keys
|
|
1748
|
+
data_frame = cocoon.properties.create_missing_property_definitions_from_file(
|
|
1749
|
+
api_factory=api_factory,
|
|
1750
|
+
properties_scope=sub_holding_keys_scope,
|
|
1751
|
+
domain="Transaction",
|
|
1752
|
+
data_frame=data_frame,
|
|
1753
|
+
property_columns=[{"source": key} for key in sub_holding_keys],
|
|
1754
|
+
)
|
|
1755
|
+
|
|
1756
|
+
# Check for and create missing property definitions for the properties
|
|
1757
|
+
if domain_lookup[file_type]["domain"] is not None:
|
|
1758
|
+
data_frame = cocoon.properties.create_missing_property_definitions_from_file(
|
|
1759
|
+
api_factory=api_factory,
|
|
1760
|
+
properties_scope=properties_scope,
|
|
1761
|
+
domain=domain_lookup[file_type]["domain"],
|
|
1762
|
+
data_frame=data_frame,
|
|
1763
|
+
property_columns=property_columns,
|
|
1764
|
+
)
|
|
1765
|
+
|
|
1766
|
+
# If the transaction contains subholding keys which aren't defined in the portfolio. We first create the
|
|
1767
|
+
# properties that don't already exist, then we make the properties sub-holding keys in the portfolios.
|
|
1768
|
+
if (
|
|
1769
|
+
file_type in ("transaction", "transactions_commit_mode")
|
|
1770
|
+
and sub_holding_keys is not None
|
|
1771
|
+
and sub_holding_keys != []
|
|
1772
|
+
):
|
|
1773
|
+
# if the SHK key is written in {domain}/{scope}/{code} form we extract the code since when we add it to
|
|
1774
|
+
# the properties there will be issues due to different formats. Also, there are issues with the
|
|
1775
|
+
# create_missing_property_definitions_from_file function as it extracts data from columns using the
|
|
1776
|
+
# sub-holding keys.
|
|
1777
|
+
sub_holding_keys_codes = [
|
|
1778
|
+
key if "/" not in key else key.split("/")[2] for key in sub_holding_keys
|
|
1779
|
+
]
|
|
1780
|
+
|
|
1781
|
+
# Check for and create missing property definitions for the sub-holding-keys
|
|
1782
|
+
data_frame = cocoon.properties.create_missing_property_definitions_from_file(
|
|
1783
|
+
api_factory=api_factory,
|
|
1784
|
+
properties_scope=properties_scope,
|
|
1785
|
+
domain="Transaction",
|
|
1786
|
+
data_frame=data_frame,
|
|
1787
|
+
property_columns=[{"source": key} for key in sub_holding_keys_codes],
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1790
|
+
transaction_portfolio_api = api_factory.build(
|
|
1791
|
+
lusid.api.TransactionPortfoliosApi
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
# Add subholding keys to the portfolios we are going to apply the transactions to
|
|
1795
|
+
for code in set(data_frame[mapping_required["code"]]):
|
|
1796
|
+
transaction_portfolio_api.patch_portfolio_details(
|
|
1797
|
+
scope,
|
|
1798
|
+
code,
|
|
1799
|
+
[
|
|
1800
|
+
{
|
|
1801
|
+
"value": cocoon.properties._infer_full_property_keys(
|
|
1802
|
+
partial_keys=sub_holding_keys,
|
|
1803
|
+
properties_scope=properties_scope,
|
|
1804
|
+
domain="Transaction",
|
|
1805
|
+
),
|
|
1806
|
+
"path": "/subHoldingKeys",
|
|
1807
|
+
"op": "add",
|
|
1808
|
+
}
|
|
1809
|
+
],
|
|
1810
|
+
)
|
|
1811
|
+
|
|
1812
|
+
# Add sub-holding keys to the properties, so it is created for each transaction.
|
|
1813
|
+
property_columns += [
|
|
1814
|
+
{"source": sub_holding_key, "target": sub_holding_key}
|
|
1815
|
+
for sub_holding_key in sub_holding_keys_codes
|
|
1816
|
+
]
|
|
1817
|
+
|
|
1818
|
+
# Start a new event loop in a new thread, this is required to run inside a Jupyter notebook
|
|
1819
|
+
loop = cocoon.async_tools.start_event_loop_new_thread()
|
|
1820
|
+
|
|
1821
|
+
# Keyword arguments to be used in requests to the LUSID API
|
|
1822
|
+
keyword_arguments = {
|
|
1823
|
+
"scope": scope,
|
|
1824
|
+
# This handles that identifiers need to be specified differently based on the request type, allowing users
|
|
1825
|
+
# to provide either the entire key e.g. "Instrument/default/Figi" or just the code "Figi" for any request
|
|
1826
|
+
"full_key_format": domain_lookup[file_type]["full_key_format"],
|
|
1827
|
+
# Gets the allowed unique identifiers
|
|
1828
|
+
"unique_identifiers": cocoon.instruments.get_unique_identifiers(
|
|
1829
|
+
api_factory=api_factory
|
|
1830
|
+
),
|
|
1831
|
+
"transactions_commit_mode": transactions_commit_mode,
|
|
1832
|
+
"holdings_adjustment_only": holdings_adjustment_only,
|
|
1833
|
+
"thread_pool": thread_pool,
|
|
1834
|
+
"instrument_scope": instrument_scope,
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
# Get the responses from LUSID
|
|
1838
|
+
logging.debug("constructing batches...")
|
|
1839
|
+
responses = asyncio.run_coroutine_threadsafe(
|
|
1840
|
+
_construct_batches(
|
|
1841
|
+
api_factory=api_factory,
|
|
1842
|
+
data_frame=data_frame,
|
|
1843
|
+
mapping_required=mapping_required,
|
|
1844
|
+
mapping_optional=mapping_optional,
|
|
1845
|
+
property_columns=property_columns,
|
|
1846
|
+
properties_scope=properties_scope,
|
|
1847
|
+
instrument_identifier_mapping=identifier_mapping,
|
|
1848
|
+
batch_size=batch_size,
|
|
1849
|
+
file_type=file_type,
|
|
1850
|
+
domain_lookup=domain_lookup,
|
|
1851
|
+
sub_holding_keys=sub_holding_keys,
|
|
1852
|
+
sub_holding_keys_scope=sub_holding_keys_scope,
|
|
1853
|
+
return_unmatched_items=return_unmatched_items,
|
|
1854
|
+
**keyword_arguments,
|
|
1855
|
+
),
|
|
1856
|
+
loop,
|
|
1857
|
+
).result()
|
|
1858
|
+
|
|
1859
|
+
# Stop the additional event loop
|
|
1860
|
+
cocoon.async_tools.stop_event_loop_new_thread(loop)
|
|
1861
|
+
|
|
1862
|
+
return {file_type + "s": responses}
|