wbfdm 2.2.1__py2.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.

Potentially problematic release.


This version of wbfdm might be problematic. Click here for more details.

Files changed (337) hide show
  1. wbfdm/__init__.py +2 -0
  2. wbfdm/admin/__init__.py +42 -0
  3. wbfdm/admin/classifications.py +39 -0
  4. wbfdm/admin/esg.py +23 -0
  5. wbfdm/admin/exchanges.py +53 -0
  6. wbfdm/admin/instrument_lists.py +23 -0
  7. wbfdm/admin/instrument_prices.py +62 -0
  8. wbfdm/admin/instrument_requests.py +33 -0
  9. wbfdm/admin/instruments.py +117 -0
  10. wbfdm/admin/instruments_relationships.py +25 -0
  11. wbfdm/admin/options.py +101 -0
  12. wbfdm/analysis/__init__.py +2 -0
  13. wbfdm/analysis/esg/__init__.py +0 -0
  14. wbfdm/analysis/esg/enums.py +82 -0
  15. wbfdm/analysis/esg/esg_analysis.py +217 -0
  16. wbfdm/analysis/esg/utils.py +13 -0
  17. wbfdm/analysis/financial_analysis/__init__.py +1 -0
  18. wbfdm/analysis/financial_analysis/financial_metric_analysis.py +88 -0
  19. wbfdm/analysis/financial_analysis/financial_ratio_analysis.py +125 -0
  20. wbfdm/analysis/financial_analysis/financial_statistics_analysis.py +271 -0
  21. wbfdm/analysis/financial_analysis/statement_with_estimates.py +558 -0
  22. wbfdm/analysis/financial_analysis/utils.py +316 -0
  23. wbfdm/analysis/technical_analysis/__init__.py +1 -0
  24. wbfdm/analysis/technical_analysis/technical_analysis.py +138 -0
  25. wbfdm/analysis/technical_analysis/traces.py +165 -0
  26. wbfdm/analysis/utils.py +32 -0
  27. wbfdm/apps.py +14 -0
  28. wbfdm/contrib/__init__.py +0 -0
  29. wbfdm/contrib/dsws/__init__.py +0 -0
  30. wbfdm/contrib/dsws/client.py +285 -0
  31. wbfdm/contrib/internal/__init__.py +0 -0
  32. wbfdm/contrib/internal/dataloaders/__init__.py +0 -0
  33. wbfdm/contrib/internal/dataloaders/market_data.py +87 -0
  34. wbfdm/contrib/metric/__init__.py +0 -0
  35. wbfdm/contrib/metric/admin/__init__.py +2 -0
  36. wbfdm/contrib/metric/admin/instruments.py +12 -0
  37. wbfdm/contrib/metric/admin/metrics.py +43 -0
  38. wbfdm/contrib/metric/apps.py +10 -0
  39. wbfdm/contrib/metric/backends/__init__.py +2 -0
  40. wbfdm/contrib/metric/backends/base.py +159 -0
  41. wbfdm/contrib/metric/backends/performances.py +265 -0
  42. wbfdm/contrib/metric/backends/statistics.py +182 -0
  43. wbfdm/contrib/metric/decorators.py +14 -0
  44. wbfdm/contrib/metric/dispatch.py +23 -0
  45. wbfdm/contrib/metric/dto.py +88 -0
  46. wbfdm/contrib/metric/exceptions.py +6 -0
  47. wbfdm/contrib/metric/factories.py +33 -0
  48. wbfdm/contrib/metric/filters.py +28 -0
  49. wbfdm/contrib/metric/migrations/0001_initial.py +88 -0
  50. wbfdm/contrib/metric/migrations/0002_remove_instrumentmetric_unique_instrument_metric_and_more.py +26 -0
  51. wbfdm/contrib/metric/migrations/__init__.py +0 -0
  52. wbfdm/contrib/metric/models.py +180 -0
  53. wbfdm/contrib/metric/orchestrators.py +94 -0
  54. wbfdm/contrib/metric/registry.py +80 -0
  55. wbfdm/contrib/metric/serializers.py +44 -0
  56. wbfdm/contrib/metric/tasks.py +27 -0
  57. wbfdm/contrib/metric/tests/__init__.py +0 -0
  58. wbfdm/contrib/metric/tests/backends/__init__.py +0 -0
  59. wbfdm/contrib/metric/tests/backends/test_performances.py +152 -0
  60. wbfdm/contrib/metric/tests/backends/test_statistics.py +48 -0
  61. wbfdm/contrib/metric/tests/conftest.py +92 -0
  62. wbfdm/contrib/metric/tests/test_dto.py +73 -0
  63. wbfdm/contrib/metric/tests/test_models.py +72 -0
  64. wbfdm/contrib/metric/tests/test_tasks.py +24 -0
  65. wbfdm/contrib/metric/tests/test_viewsets.py +79 -0
  66. wbfdm/contrib/metric/urls.py +19 -0
  67. wbfdm/contrib/metric/viewsets/__init__.py +1 -0
  68. wbfdm/contrib/metric/viewsets/configs/__init__.py +1 -0
  69. wbfdm/contrib/metric/viewsets/configs/display.py +92 -0
  70. wbfdm/contrib/metric/viewsets/configs/menus.py +11 -0
  71. wbfdm/contrib/metric/viewsets/configs/utils.py +137 -0
  72. wbfdm/contrib/metric/viewsets/mixins.py +245 -0
  73. wbfdm/contrib/metric/viewsets/viewsets.py +40 -0
  74. wbfdm/contrib/msci/__init__.py +0 -0
  75. wbfdm/contrib/msci/client.py +92 -0
  76. wbfdm/contrib/msci/dataloaders/__init__.py +0 -0
  77. wbfdm/contrib/msci/dataloaders/esg.py +87 -0
  78. wbfdm/contrib/msci/dataloaders/esg_controversies.py +81 -0
  79. wbfdm/contrib/msci/sync.py +58 -0
  80. wbfdm/contrib/msci/tests/__init__.py +0 -0
  81. wbfdm/contrib/msci/tests/conftest.py +1 -0
  82. wbfdm/contrib/msci/tests/test_client.py +70 -0
  83. wbfdm/contrib/qa/__init__.py +0 -0
  84. wbfdm/contrib/qa/apps.py +22 -0
  85. wbfdm/contrib/qa/database_routers.py +25 -0
  86. wbfdm/contrib/qa/dataloaders/__init__.py +0 -0
  87. wbfdm/contrib/qa/dataloaders/adjustments.py +56 -0
  88. wbfdm/contrib/qa/dataloaders/corporate_actions.py +59 -0
  89. wbfdm/contrib/qa/dataloaders/financials.py +83 -0
  90. wbfdm/contrib/qa/dataloaders/market_data.py +117 -0
  91. wbfdm/contrib/qa/dataloaders/officers.py +59 -0
  92. wbfdm/contrib/qa/dataloaders/reporting_dates.py +67 -0
  93. wbfdm/contrib/qa/dataloaders/statements.py +267 -0
  94. wbfdm/contrib/qa/tasks.py +0 -0
  95. wbfdm/dataloaders/__init__.py +0 -0
  96. wbfdm/dataloaders/cache.py +129 -0
  97. wbfdm/dataloaders/protocols.py +112 -0
  98. wbfdm/dataloaders/proxies.py +201 -0
  99. wbfdm/dataloaders/types.py +209 -0
  100. wbfdm/dynamic_preferences_registry.py +45 -0
  101. wbfdm/enums.py +657 -0
  102. wbfdm/factories/__init__.py +13 -0
  103. wbfdm/factories/classifications.py +56 -0
  104. wbfdm/factories/controversies.py +27 -0
  105. wbfdm/factories/exchanges.py +21 -0
  106. wbfdm/factories/instrument_list.py +22 -0
  107. wbfdm/factories/instrument_prices.py +79 -0
  108. wbfdm/factories/instruments.py +63 -0
  109. wbfdm/factories/instruments_relationships.py +31 -0
  110. wbfdm/factories/options.py +66 -0
  111. wbfdm/figures/__init__.py +1 -0
  112. wbfdm/figures/financials/__init__.py +1 -0
  113. wbfdm/figures/financials/financial_analysis_charts.py +469 -0
  114. wbfdm/figures/financials/financials_charts.py +711 -0
  115. wbfdm/filters/__init__.py +31 -0
  116. wbfdm/filters/classifications.py +100 -0
  117. wbfdm/filters/exchanges.py +22 -0
  118. wbfdm/filters/financials.py +95 -0
  119. wbfdm/filters/financials_analysis.py +119 -0
  120. wbfdm/filters/instrument_prices.py +112 -0
  121. wbfdm/filters/instruments.py +198 -0
  122. wbfdm/filters/utils.py +44 -0
  123. wbfdm/import_export/__init__.py +0 -0
  124. wbfdm/import_export/backends/__init__.py +0 -0
  125. wbfdm/import_export/backends/cbinsights/__init__.py +2 -0
  126. wbfdm/import_export/backends/cbinsights/deals.py +44 -0
  127. wbfdm/import_export/backends/cbinsights/equities.py +41 -0
  128. wbfdm/import_export/backends/cbinsights/mixin.py +15 -0
  129. wbfdm/import_export/backends/cbinsights/utils/__init__.py +0 -0
  130. wbfdm/import_export/backends/cbinsights/utils/classifications.py +4150 -0
  131. wbfdm/import_export/backends/cbinsights/utils/client.py +217 -0
  132. wbfdm/import_export/backends/refinitiv/__init__.py +5 -0
  133. wbfdm/import_export/backends/refinitiv/daily_fundamental.py +36 -0
  134. wbfdm/import_export/backends/refinitiv/fiscal_period.py +63 -0
  135. wbfdm/import_export/backends/refinitiv/forecast.py +178 -0
  136. wbfdm/import_export/backends/refinitiv/fundamental.py +103 -0
  137. wbfdm/import_export/backends/refinitiv/geographic_segment.py +32 -0
  138. wbfdm/import_export/backends/refinitiv/instrument.py +55 -0
  139. wbfdm/import_export/backends/refinitiv/instrument_price.py +77 -0
  140. wbfdm/import_export/backends/refinitiv/mixin.py +29 -0
  141. wbfdm/import_export/backends/refinitiv/utils/__init__.py +1 -0
  142. wbfdm/import_export/backends/refinitiv/utils/controller.py +182 -0
  143. wbfdm/import_export/handlers/__init__.py +0 -0
  144. wbfdm/import_export/handlers/instrument.py +253 -0
  145. wbfdm/import_export/handlers/instrument_list.py +101 -0
  146. wbfdm/import_export/handlers/instrument_price.py +71 -0
  147. wbfdm/import_export/handlers/option.py +54 -0
  148. wbfdm/import_export/handlers/private_equities.py +49 -0
  149. wbfdm/import_export/parsers/__init__.py +0 -0
  150. wbfdm/import_export/parsers/cbinsights/__init__.py +0 -0
  151. wbfdm/import_export/parsers/cbinsights/deals.py +39 -0
  152. wbfdm/import_export/parsers/cbinsights/equities.py +56 -0
  153. wbfdm/import_export/parsers/cbinsights/fundamentals.py +45 -0
  154. wbfdm/import_export/parsers/refinitiv/__init__.py +0 -0
  155. wbfdm/import_export/parsers/refinitiv/daily_fundamental.py +7 -0
  156. wbfdm/import_export/parsers/refinitiv/forecast.py +7 -0
  157. wbfdm/import_export/parsers/refinitiv/fundamental.py +9 -0
  158. wbfdm/import_export/parsers/refinitiv/geographic_segment.py +7 -0
  159. wbfdm/import_export/parsers/refinitiv/instrument.py +75 -0
  160. wbfdm/import_export/parsers/refinitiv/instrument_price.py +26 -0
  161. wbfdm/import_export/parsers/refinitiv/utils.py +96 -0
  162. wbfdm/import_export/resources/__init__.py +0 -0
  163. wbfdm/import_export/resources/classification.py +23 -0
  164. wbfdm/import_export/resources/instrument_prices.py +33 -0
  165. wbfdm/import_export/resources/instruments.py +176 -0
  166. wbfdm/jinja2.py +7 -0
  167. wbfdm/management/__init__.py +30 -0
  168. wbfdm/menu.py +11 -0
  169. wbfdm/migrations/0001_initial.py +71 -0
  170. wbfdm/migrations/0002_rename_statements_instrumentlookup_financials_and_more.py +144 -0
  171. wbfdm/migrations/0003_instrument_estimate_backend_and_more.py +34 -0
  172. wbfdm/migrations/0004_rename_financials_instrumentlookup_statements_and_more.py +86 -0
  173. wbfdm/migrations/0005_instrument_corporate_action_backend.py +29 -0
  174. wbfdm/migrations/0006_instrument_officer_backend.py +29 -0
  175. wbfdm/migrations/0007_instrument_country_instrument_currency_and_more.py +117 -0
  176. wbfdm/migrations/0008_controversy.py +75 -0
  177. wbfdm/migrations/0009_alter_controversy_flag_alter_controversy_initiated_and_more.py +85 -0
  178. wbfdm/migrations/0010_classification_classificationgroup_deal_exchange_and_more.py +1299 -0
  179. wbfdm/migrations/0011_delete_instrumentlookup_instrument_corporate_actions_and_more.py +169 -0
  180. wbfdm/migrations/0012_instrumentprice_created_instrumentprice_modified.py +564 -0
  181. wbfdm/migrations/0013_instrument_is_investable_universe_and_more.py +199 -0
  182. wbfdm/migrations/0014_alter_controversy_instrument.py +22 -0
  183. wbfdm/migrations/0015_instrument_instrument_investible_index.py +16 -0
  184. wbfdm/migrations/0016_instrumenttype_name_repr.py +18 -0
  185. wbfdm/migrations/0017_instrument_instrument_security_index.py +16 -0
  186. wbfdm/migrations/0018_instrument_instrument_level_index.py +20 -0
  187. wbfdm/migrations/0019_alter_controversy_source.py +17 -0
  188. wbfdm/migrations/0020_optionaggregate_option_and_more.py +249 -0
  189. wbfdm/migrations/0021_delete_instrumentdailystatistics.py +15 -0
  190. wbfdm/migrations/0022_instrument_cusip_option_open_interest_20d_and_more.py +91 -0
  191. wbfdm/migrations/0023_instrument_unique_ric_instrument_unique_rmc_and_more.py +53 -0
  192. wbfdm/migrations/0024_option_open_interest_10d_option_volume_10d_and_more.py +36 -0
  193. wbfdm/migrations/0025_instrument_is_primary_and_more.py +29 -0
  194. wbfdm/migrations/0026_instrument_is_cash_equivalent.py +30 -0
  195. wbfdm/migrations/0027_remove_instrument_unique_ric_and_more.py +100 -0
  196. wbfdm/migrations/__init__.py +0 -0
  197. wbfdm/models/__init__.py +4 -0
  198. wbfdm/models/esg/__init__.py +1 -0
  199. wbfdm/models/esg/controversies.py +81 -0
  200. wbfdm/models/exchanges/__init__.py +1 -0
  201. wbfdm/models/exchanges/exchanges.py +223 -0
  202. wbfdm/models/fields.py +117 -0
  203. wbfdm/models/fk_fields.py +403 -0
  204. wbfdm/models/indicators.py +0 -0
  205. wbfdm/models/instruments/__init__.py +19 -0
  206. wbfdm/models/instruments/classifications.py +265 -0
  207. wbfdm/models/instruments/instrument_lists.py +120 -0
  208. wbfdm/models/instruments/instrument_prices.py +540 -0
  209. wbfdm/models/instruments/instrument_relationships.py +251 -0
  210. wbfdm/models/instruments/instrument_requests.py +196 -0
  211. wbfdm/models/instruments/instruments.py +991 -0
  212. wbfdm/models/instruments/llm/__init__.py +1 -0
  213. wbfdm/models/instruments/llm/create_instrument_news_relationships.py +78 -0
  214. wbfdm/models/instruments/mixin/__init__.py +0 -0
  215. wbfdm/models/instruments/mixin/financials_computed.py +804 -0
  216. wbfdm/models/instruments/mixin/financials_serializer_fields.py +1407 -0
  217. wbfdm/models/instruments/mixin/instruments.py +294 -0
  218. wbfdm/models/instruments/options.py +225 -0
  219. wbfdm/models/instruments/private_equities.py +59 -0
  220. wbfdm/models/instruments/querysets.py +73 -0
  221. wbfdm/models/instruments/utils.py +41 -0
  222. wbfdm/preferences.py +21 -0
  223. wbfdm/serializers/__init__.py +4 -0
  224. wbfdm/serializers/esg.py +36 -0
  225. wbfdm/serializers/exchanges.py +39 -0
  226. wbfdm/serializers/instruments/__init__.py +37 -0
  227. wbfdm/serializers/instruments/classifications.py +139 -0
  228. wbfdm/serializers/instruments/instrument_lists.py +61 -0
  229. wbfdm/serializers/instruments/instrument_prices.py +73 -0
  230. wbfdm/serializers/instruments/instrument_relationships.py +170 -0
  231. wbfdm/serializers/instruments/instrument_requests.py +61 -0
  232. wbfdm/serializers/instruments/instruments.py +274 -0
  233. wbfdm/serializers/instruments/mixins.py +104 -0
  234. wbfdm/serializers/officers.py +20 -0
  235. wbfdm/signals.py +7 -0
  236. wbfdm/sync/__init__.py +0 -0
  237. wbfdm/sync/abstract.py +31 -0
  238. wbfdm/sync/runner.py +22 -0
  239. wbfdm/tasks.py +69 -0
  240. wbfdm/tests/__init__.py +0 -0
  241. wbfdm/tests/analysis/__init__.py +0 -0
  242. wbfdm/tests/analysis/financial_analysis/__init__.py +0 -0
  243. wbfdm/tests/analysis/financial_analysis/test_statement_with_estimates.py +392 -0
  244. wbfdm/tests/analysis/financial_analysis/test_utils.py +322 -0
  245. wbfdm/tests/analysis/test_esg.py +159 -0
  246. wbfdm/tests/conftest.py +92 -0
  247. wbfdm/tests/dataloaders/__init__.py +0 -0
  248. wbfdm/tests/dataloaders/test_cache.py +73 -0
  249. wbfdm/tests/models/__init__.py +0 -0
  250. wbfdm/tests/models/test_classifications.py +99 -0
  251. wbfdm/tests/models/test_exchanges.py +7 -0
  252. wbfdm/tests/models/test_instrument_list.py +117 -0
  253. wbfdm/tests/models/test_instrument_prices.py +306 -0
  254. wbfdm/tests/models/test_instruments.py +202 -0
  255. wbfdm/tests/models/test_merge.py +99 -0
  256. wbfdm/tests/models/test_options.py +69 -0
  257. wbfdm/tests/test_tasks.py +6 -0
  258. wbfdm/tests/tests.py +10 -0
  259. wbfdm/urls.py +222 -0
  260. wbfdm/utils.py +54 -0
  261. wbfdm/viewsets/__init__.py +10 -0
  262. wbfdm/viewsets/configs/__init__.py +5 -0
  263. wbfdm/viewsets/configs/buttons/__init__.py +8 -0
  264. wbfdm/viewsets/configs/buttons/classifications.py +23 -0
  265. wbfdm/viewsets/configs/buttons/exchanges.py +9 -0
  266. wbfdm/viewsets/configs/buttons/instrument_prices.py +49 -0
  267. wbfdm/viewsets/configs/buttons/instruments.py +283 -0
  268. wbfdm/viewsets/configs/display/__init__.py +22 -0
  269. wbfdm/viewsets/configs/display/classifications.py +138 -0
  270. wbfdm/viewsets/configs/display/esg.py +75 -0
  271. wbfdm/viewsets/configs/display/exchanges.py +42 -0
  272. wbfdm/viewsets/configs/display/instrument_lists.py +137 -0
  273. wbfdm/viewsets/configs/display/instrument_prices.py +199 -0
  274. wbfdm/viewsets/configs/display/instrument_requests.py +116 -0
  275. wbfdm/viewsets/configs/display/instruments.py +618 -0
  276. wbfdm/viewsets/configs/display/instruments_relationships.py +65 -0
  277. wbfdm/viewsets/configs/display/monthly_performances.py +72 -0
  278. wbfdm/viewsets/configs/display/officers.py +16 -0
  279. wbfdm/viewsets/configs/display/prices.py +21 -0
  280. wbfdm/viewsets/configs/display/statement_with_estimates.py +101 -0
  281. wbfdm/viewsets/configs/display/statements.py +48 -0
  282. wbfdm/viewsets/configs/endpoints/__init__.py +41 -0
  283. wbfdm/viewsets/configs/endpoints/classifications.py +87 -0
  284. wbfdm/viewsets/configs/endpoints/esg.py +20 -0
  285. wbfdm/viewsets/configs/endpoints/exchanges.py +6 -0
  286. wbfdm/viewsets/configs/endpoints/financials_analysis.py +65 -0
  287. wbfdm/viewsets/configs/endpoints/instrument_lists.py +38 -0
  288. wbfdm/viewsets/configs/endpoints/instrument_prices.py +51 -0
  289. wbfdm/viewsets/configs/endpoints/instrument_requests.py +20 -0
  290. wbfdm/viewsets/configs/endpoints/instruments.py +13 -0
  291. wbfdm/viewsets/configs/endpoints/instruments_relationships.py +31 -0
  292. wbfdm/viewsets/configs/endpoints/statements.py +6 -0
  293. wbfdm/viewsets/configs/menus/__init__.py +9 -0
  294. wbfdm/viewsets/configs/menus/classifications.py +19 -0
  295. wbfdm/viewsets/configs/menus/exchanges.py +10 -0
  296. wbfdm/viewsets/configs/menus/instrument_lists.py +10 -0
  297. wbfdm/viewsets/configs/menus/instruments.py +20 -0
  298. wbfdm/viewsets/configs/menus/instruments_relationships.py +33 -0
  299. wbfdm/viewsets/configs/titles/__init__.py +42 -0
  300. wbfdm/viewsets/configs/titles/classifications.py +79 -0
  301. wbfdm/viewsets/configs/titles/esg.py +11 -0
  302. wbfdm/viewsets/configs/titles/exchanges.py +12 -0
  303. wbfdm/viewsets/configs/titles/financial_ratio_analysis.py +6 -0
  304. wbfdm/viewsets/configs/titles/financials_analysis.py +50 -0
  305. wbfdm/viewsets/configs/titles/instrument_prices.py +50 -0
  306. wbfdm/viewsets/configs/titles/instrument_requests.py +16 -0
  307. wbfdm/viewsets/configs/titles/instruments.py +31 -0
  308. wbfdm/viewsets/configs/titles/instruments_relationships.py +21 -0
  309. wbfdm/viewsets/configs/titles/market_data.py +13 -0
  310. wbfdm/viewsets/configs/titles/prices.py +15 -0
  311. wbfdm/viewsets/configs/titles/statement_with_estimates.py +10 -0
  312. wbfdm/viewsets/esg.py +72 -0
  313. wbfdm/viewsets/exchanges.py +63 -0
  314. wbfdm/viewsets/financial_analysis/__init__.py +3 -0
  315. wbfdm/viewsets/financial_analysis/financial_metric_analysis.py +85 -0
  316. wbfdm/viewsets/financial_analysis/financial_ratio_analysis.py +85 -0
  317. wbfdm/viewsets/financial_analysis/statement_with_estimates.py +145 -0
  318. wbfdm/viewsets/instruments/__init__.py +80 -0
  319. wbfdm/viewsets/instruments/classifications.py +279 -0
  320. wbfdm/viewsets/instruments/financials_analysis.py +614 -0
  321. wbfdm/viewsets/instruments/instrument_lists.py +77 -0
  322. wbfdm/viewsets/instruments/instrument_prices.py +542 -0
  323. wbfdm/viewsets/instruments/instrument_requests.py +51 -0
  324. wbfdm/viewsets/instruments/instruments.py +106 -0
  325. wbfdm/viewsets/instruments/instruments_relationships.py +235 -0
  326. wbfdm/viewsets/instruments/utils.py +27 -0
  327. wbfdm/viewsets/market_data.py +172 -0
  328. wbfdm/viewsets/mixins.py +9 -0
  329. wbfdm/viewsets/officers.py +27 -0
  330. wbfdm/viewsets/prices.py +62 -0
  331. wbfdm/viewsets/statements/__init__.py +1 -0
  332. wbfdm/viewsets/statements/statements.py +100 -0
  333. wbfdm/viewsets/technical_analysis/__init__.py +1 -0
  334. wbfdm/viewsets/technical_analysis/monthly_performances.py +93 -0
  335. wbfdm-2.2.1.dist-info/METADATA +15 -0
  336. wbfdm-2.2.1.dist-info/RECORD +337 -0
  337. wbfdm-2.2.1.dist-info/WHEEL +5 -0
@@ -0,0 +1,77 @@
1
+ from datetime import datetime
2
+ from io import BytesIO
3
+ from typing import Optional
4
+
5
+ import pandas as pd
6
+ from django.db import models
7
+ from pandas.tseries.offsets import BDay
8
+ from wbcore.contrib.io.backends import AbstractDataBackend, register
9
+
10
+ from .mixin import DataBackendMixin
11
+ from .utils import Controller
12
+
13
+ DEFAULT_MAPPING = {
14
+ "MV": "market_capitalization",
15
+ "MVC": "market_capitalization_consolidated",
16
+ "VO": "volume",
17
+ "MAV#(X(VO),-50D,R)": "volume_50d",
18
+ "BETA": "beta",
19
+ "NOSH": "outstanding_shares",
20
+ "IBNOSH": "outstanding_shares_consolidated",
21
+ "P": "close",
22
+ "PI": "price_index",
23
+ "RY": "yield_redemption",
24
+ "NR": "net_return",
25
+ "IO": "offered_rate",
26
+ }
27
+
28
+ MUTUAL_FUND_TIMEDELTA_DAY_SHIFT = 7
29
+
30
+
31
+ @register("Instrument Price", provider_key="refinitiv", save_data_in_import_source=False, passive_only=False)
32
+ class DataBackend(DataBackendMixin, AbstractDataBackend):
33
+ def __init__(self, import_credential: Optional[models.Model] = None, **kwargs):
34
+ self.controller = Controller(import_credential.username, import_credential.password)
35
+
36
+ def get_files(
37
+ self,
38
+ execution_time: datetime,
39
+ obj_external_ids: list[str] = None,
40
+ **kwargs,
41
+ ) -> BytesIO:
42
+ execution_date = execution_time.date()
43
+
44
+ start = kwargs.get("start", (execution_date - BDay(2)).date())
45
+
46
+ delayed_instrument_ids = list(
47
+ filter(
48
+ lambda x: True,
49
+ obj_external_ids,
50
+ )
51
+ )
52
+ obj_external_ids = list(filter(lambda v: v not in delayed_instrument_ids, obj_external_ids))
53
+ # we get all active instruments even these were we are not suppose to fetch prices.
54
+ fields = list(DEFAULT_MAPPING.keys())
55
+ if obj_external_ids or delayed_instrument_ids:
56
+ df_list = []
57
+ if obj_external_ids:
58
+ df_list.append(
59
+ self.controller.get_data(obj_external_ids, fields, start, execution_date, ibes_fields=["IBNOSH"])
60
+ )
61
+ if delayed_instrument_ids:
62
+ # we need to get mutual fund price in a different batch because there is a price delay and the windows approach can't handle it otherwise
63
+ df_list.append(
64
+ self.controller.get_data(
65
+ delayed_instrument_ids,
66
+ fields,
67
+ start - BDay(MUTUAL_FUND_TIMEDELTA_DAY_SHIFT),
68
+ execution_date,
69
+ ibes_fields=["IBNOSH"],
70
+ )
71
+ )
72
+ df = pd.concat(df_list, axis=0, ignore_index=True)
73
+ if not df.empty:
74
+ content_file = BytesIO()
75
+ df.to_json(content_file, orient="records")
76
+ file_name = f"instrument_price_{start:%Y-%m-%d}-{execution_date:%Y-%m-%d}_{datetime.timestamp(execution_time)}.json"
77
+ yield file_name, content_file
@@ -0,0 +1,29 @@
1
+ from datetime import date
2
+
3
+ from django.db import models
4
+ from wbfdm.models.instruments import Instrument
5
+
6
+
7
+ class DataBackendMixin:
8
+ def is_object_valid(self, obj: models.Model) -> bool:
9
+ return (
10
+ super().is_object_valid(obj)
11
+ and obj.is_active_at_date(date.today())
12
+ and (obj.refinitiv_identifier_code or obj.isin or obj.refinitiv_mnemonic_code)
13
+ )
14
+
15
+ def get_default_queryset(self):
16
+ privates_equities = Instrument.objects.filter(instrument_type__key="private_equity")
17
+ return Instrument.objects.exclude(id__in=privates_equities.values("id"))
18
+
19
+ def get_provider_id(self, obj: models.Model) -> str:
20
+ if perm_id := self.controller.fetch_perm_id(
21
+ instrument_ric=obj.refinitiv_identifier_code,
22
+ instrument_isin=obj.isin,
23
+ instrument_mnemonic=obj.refinitiv_mnemonic_code,
24
+ ):
25
+ return perm_id
26
+ elif (
27
+ obj.refinitiv_identifier_code
28
+ ): # We default to the RIC in case permID can't be found because some instrument don't have any
29
+ return obj.refinitiv_identifier_code
@@ -0,0 +1 @@
1
+ from .controller import Controller
@@ -0,0 +1,182 @@
1
+ from datetime import date
2
+
3
+ import pandas as pd
4
+ from django.conf import settings
5
+ from django.contrib.contenttypes.models import ContentType
6
+ from pandas.tseries.offsets import QuarterEnd, YearEnd
7
+ from tqdm import tqdm
8
+ from wbcore.contrib.io.models import ImportedObjectProviderRelationship, Provider
9
+ from wbfdm.contrib.dsws.client import Client
10
+ from wbfdm.models.instruments.instruments import Instrument
11
+
12
+ REPORT_TYPE_MAP = {"QTR": "Q", "TRI": "4M", "SAN": "6M", "ANN": "Y"}
13
+
14
+
15
+ class Controller:
16
+ def __init__(self, client_username: str, client_password: str):
17
+ self.provider = Provider.objects.get(key="refinitiv")
18
+ self.client = Client(username=client_username, password=client_password)
19
+
20
+ @classmethod
21
+ def _wrap_identifier_in_bracklet(cls, perm_ids: list[str]) -> list[str]:
22
+ return [f"<{perm_id}>" for perm_id in perm_ids]
23
+
24
+ def get_start_and_end_date_for_interim(self, perm_id: str, end: date, start: date | None = None) -> date | None:
25
+ if not start:
26
+ start = end - QuarterEnd(1)
27
+ if rel := ImportedObjectProviderRelationship.objects.filter(
28
+ provider=self.provider,
29
+ provider_identifier=perm_id,
30
+ content_type=ContentType.objects.get_for_model(Instrument),
31
+ ).first():
32
+ instrument = rel.content_object
33
+ fundamentals = instrument.fundamentals.order_by("-period__period_year", "-period__period_index")
34
+ # Not an historical import, it's a bulk one, we need to get the next fiscal period to fetch
35
+ if fundamentals.exists() and (fiscal_period := fundamentals.first().period):
36
+ start = fiscal_period.calendar_period.end_time.date()
37
+
38
+ return (start + QuarterEnd(0)).date(), (
39
+ end + QuarterEnd(0) + QuarterEnd(1)
40
+ ).date() # We always refetch since the beginning of the quarter
41
+
42
+ @classmethod
43
+ def get_start_date_for_annual(cls, end: date, start: date | None = None) -> date:
44
+ if not start:
45
+ start = end
46
+ return (start - YearEnd(1)).date(), (
47
+ end + YearEnd(0)
48
+ ).date() # We always refetch since the beginning of the quarter
49
+
50
+ def get_frequency(self, instrument: str) -> str:
51
+ try:
52
+ res = self.client.get_static_df([instrument], ["IBEFPD"])
53
+ if not res.empty:
54
+ return REPORT_TYPE_MAP[res.set_index("Instrument").loc[instrument, "IBEFPD"]]
55
+ except Exception:
56
+ return "Q"
57
+
58
+ def fetch_perm_id(
59
+ self,
60
+ instrument_ric: str = None,
61
+ instrument_isin: str = None,
62
+ instrument_mnemonic: str = None,
63
+ perm_id_symbols: list[str] = ["QPID", "IPID"],
64
+ ) -> str | None:
65
+ def _process_ticker(ticker):
66
+ if not (df := self.client.get_static_df(tickers=[ticker], fields=perm_id_symbols)).empty:
67
+ for perm_id in perm_id_symbols:
68
+ if identifier := df[perm_id].iloc[0]:
69
+ return identifier
70
+
71
+ res = None
72
+ if instrument_ric:
73
+ res = _process_ticker(f"<{instrument_ric}>")
74
+ if instrument_mnemonic and not res:
75
+ res = _process_ticker(instrument_mnemonic)
76
+ if instrument_isin and not res:
77
+ res = _process_ticker(instrument_isin)
78
+ return res
79
+
80
+ def get_data(
81
+ self,
82
+ identifiers: list[str],
83
+ fields: list[str],
84
+ start: date | None = None,
85
+ end: date | None = None,
86
+ wrap_tickers_into_brackets: bool = True,
87
+ **extra_client_kwargs,
88
+ ) -> pd.DataFrame:
89
+ df_list = []
90
+ chunked_identifiers = self.client.get_chunked_list(identifiers, len(fields))
91
+
92
+ if settings.DEBUG:
93
+ chunked_identifiers = tqdm(chunked_identifiers, total=len(chunked_identifiers))
94
+
95
+ frequency = extra_client_kwargs.pop("freq", "D")
96
+ for perm_ids in chunked_identifiers:
97
+ if wrap_tickers_into_brackets:
98
+ perm_ids = self._wrap_identifier_in_bracklet(perm_ids)
99
+ if start or end:
100
+ dff = self.client.get_timeserie_df(
101
+ perm_ids, fields, start=start, end=end, freq=frequency, **extra_client_kwargs
102
+ )
103
+ df_list.append(dff)
104
+ else:
105
+ df_list.append(self.client.get_static_df(perm_ids, fields, **extra_client_kwargs))
106
+ if df_list:
107
+ return pd.concat(df_list, axis=0)
108
+ return pd.DataFrame()
109
+
110
+ def get_interim_fundamental_data(
111
+ self,
112
+ identifiers: list[str],
113
+ annual_fields: list[str],
114
+ initial_start: date | None = None,
115
+ initial_end: date | None = None,
116
+ wrap_tickers_into_brackets: bool = True,
117
+ **extra_client_kwargs,
118
+ ) -> pd.DataFrame:
119
+ df_list = []
120
+ interim_fields_map = {field + "A" if field[0:2] == "WC" else field: field for field in annual_fields}
121
+ if settings.DEBUG:
122
+ identifiers = tqdm(identifiers, total=len(identifiers))
123
+
124
+ for instrument_perm_id in identifiers:
125
+ start = initial_start # copy start argument
126
+ end = initial_end # copy end argument
127
+
128
+ start, end = self.get_start_and_end_date_for_interim(instrument_perm_id, end, start=start)
129
+ if start and end:
130
+ perm_ids = [instrument_perm_id]
131
+ if wrap_tickers_into_brackets:
132
+ perm_ids = self._wrap_identifier_in_bracklet(perm_ids)
133
+ # frequency = self.get_frequency(identifiers[0])
134
+ df = self.client.get_timeserie_df(
135
+ perm_ids,
136
+ list(interim_fields_map.keys()),
137
+ start=start,
138
+ end=end,
139
+ **extra_client_kwargs,
140
+ )
141
+ if not df.empty:
142
+ df_list.append(df)
143
+ if df_list:
144
+ df = pd.concat(df_list, axis=0).dropna(how="all")
145
+ df["period__period_interim"] = True
146
+ return df.rename(columns=interim_fields_map)
147
+ return pd.DataFrame()
148
+
149
+ def get_annual_fundamental_data(
150
+ self,
151
+ identifiers: list[str],
152
+ annual_fields: list[str],
153
+ initial_start: date | None = None,
154
+ initial_end: date | None = None,
155
+ wrap_tickers_into_brackets: bool = True,
156
+ **extra_client_kwargs,
157
+ ) -> pd.DataFrame:
158
+ df_list = []
159
+ start, end = self.get_start_date_for_annual(initial_end, start=initial_start)
160
+ chunked_identifiers = self.client.get_chunked_list(identifiers, len(annual_fields))
161
+ if settings.DEBUG:
162
+ chunked_identifiers = tqdm(chunked_identifiers, total=len(chunked_identifiers))
163
+ for perm_ids in chunked_identifiers:
164
+ if wrap_tickers_into_brackets:
165
+ perm_ids = self._wrap_identifier_in_bracklet(perm_ids)
166
+ df = self.client.get_timeserie_df(
167
+ perm_ids,
168
+ annual_fields,
169
+ start=start,
170
+ end=end,
171
+ freq="Y",
172
+ **extra_client_kwargs,
173
+ )
174
+ if not df.empty:
175
+ df_list.append(df)
176
+
177
+ if df_list:
178
+ df = pd.concat(df_list, axis=0).dropna(how="all")
179
+ df["WC05200"] = 4
180
+ df["period__period_interim"] = False
181
+ return df
182
+ return pd.DataFrame()
File without changes
@@ -0,0 +1,253 @@
1
+ import operator
2
+ from contextlib import suppress
3
+ from datetime import datetime
4
+ from functools import reduce
5
+ from typing import Any, Dict, Optional
6
+
7
+ from django.contrib.postgres.search import TrigramSimilarity
8
+ from django.core.exceptions import MultipleObjectsReturned
9
+ from django.db import IntegrityError, models
10
+ from django.db.models import Q
11
+ from wbcore.contrib.currency.import_export.handlers import CurrencyImportHandler
12
+ from wbcore.contrib.geography.models import Geography
13
+ from wbcore.contrib.io.exceptions import DeserializationError
14
+ from wbcore.contrib.io.imports import ImportExportHandler, ImportState
15
+ from wbfdm.models.exchanges import Exchange
16
+
17
+
18
+ class InstrumentLookup:
19
+ ORDERED_KEYS = [
20
+ "instrument_type",
21
+ "isin",
22
+ "refinitiv_identifier_code",
23
+ "refinitiv_mnemonic_code",
24
+ "identifier",
25
+ "ticker",
26
+ ]
27
+
28
+ def __init__(self, model, trigram_similarity_min_score: float = 0.8):
29
+ self.cache = {}
30
+ self.model = model
31
+ self.trigram_similarity_min_score = trigram_similarity_min_score
32
+
33
+ @classmethod
34
+ def _get_cache_key(cls, **data):
35
+ if data:
36
+ return "-".join([f"{k}:{data.get(k, None)}" for k in cls.ORDERED_KEYS if data.get(k, None) is not None])
37
+
38
+ def _lookup_instrument(
39
+ self,
40
+ instrument_type=None,
41
+ currency=None,
42
+ exchange=None,
43
+ name=None,
44
+ only_investable_universe: bool = False,
45
+ exact_lookup: bool = False,
46
+ **identifiers,
47
+ ):
48
+ identifiers = {k: v for k, v in identifiers.items() if v is not None}
49
+ # General lookup, we try to gracefully find the instrument based on all available identifier fields
50
+ cache_key = self._get_cache_key(**identifiers)
51
+ if cache_key and cache_key in self.cache:
52
+ return self.cache[cache_key]
53
+
54
+ instrument = None
55
+
56
+ # We need to lookup ticker because some provider gives us ticker with or without space in it
57
+ if only_investable_universe:
58
+ instruments = self.model.objects.filter(is_investable_universe=True)
59
+ else:
60
+ instruments = self.model.objects.filter(is_security=True)
61
+
62
+ # Try exact lookup on the filtered out universe
63
+ for identifier_key in [
64
+ "isin",
65
+ "refinitiv_identifier_code",
66
+ "refinitiv_mnemonic_code",
67
+ "sedol",
68
+ "cusip",
69
+ "ticker",
70
+ "identifier",
71
+ ]:
72
+ if identifier := identifiers.get(identifier_key, None):
73
+ with suppress(self.model.DoesNotExist, MultipleObjectsReturned):
74
+ identifier = str(identifier)
75
+ if (
76
+ identifier_key != "refinitiv_identifier_code"
77
+ ): # RIC cannot be uppercased because its symbology implies meaning for lowercase characters
78
+ identifier = identifier.upper()
79
+ instrument = instruments.get(**{identifier_key: identifier})
80
+ break
81
+
82
+ if not instrument and not exact_lookup:
83
+ if instrument_type:
84
+ if isinstance(instrument_type, str): # in case we receive a key as instrument type
85
+ instruments = instruments.filter(instrument_type__key=instrument_type)
86
+ else: # in case we receive a primary key as instrument type
87
+ instruments = instruments.filter(instrument_type=instrument_type)
88
+ lookup_fields = [
89
+ "isin",
90
+ "refinitiv_identifier_code",
91
+ "refinitiv_mnemonic_code",
92
+ "refinitiv_ticker",
93
+ "identifier",
94
+ "ticker",
95
+ ]
96
+
97
+ conditions = []
98
+ for field in lookup_fields:
99
+ if field_value := identifiers.get(field, None):
100
+ conditions.append(Q(**{f"{field}": field_value}))
101
+ if field == "isin":
102
+ conditions.append(Q(old_isins__contains=[field_value]))
103
+ if conditions:
104
+ instruments = instruments.filter(reduce(operator.or_, conditions))
105
+ if currency or exchange:
106
+ instruments_tmp = instruments
107
+ if exchange:
108
+ instruments_tmp = instruments_tmp.filter(exchange=exchange)
109
+ if currency:
110
+ instruments_tmp = instruments_tmp.filter(currency=currency)
111
+ if instruments_tmp.count() >= 1:
112
+ instruments = instruments_tmp
113
+ # last chance
114
+ if name and instruments.count() > 1:
115
+ instruments = instruments.annotate(similarity_score=TrigramSimilarity("name", name))
116
+ if instruments.filter(similarity_score__gt=self.trigram_similarity_min_score).count() == 1:
117
+ instruments = instruments.filter(similarity_score__gt=self.trigram_similarity_min_score)
118
+ if instruments.count() == 1:
119
+ instrument = instruments.first()
120
+ elif instrument_type and identifiers:
121
+ # if instrument type was provided but we still didn't find the security, we try without the instrument type in case it was mislabeled
122
+ instrument = self._lookup_instrument(
123
+ only_investable_universe=only_investable_universe,
124
+ exact_lookup=exact_lookup,
125
+ currency=currency,
126
+ exchange=exchange,
127
+ **identifiers,
128
+ )
129
+ if not instrument and name and identifiers:
130
+ # Sometime, identifier provided emptied the queryset of possible instruments. In a last chance approach, we try to only look for security with the given name
131
+ instrument = self._lookup_instrument(
132
+ only_investable_universe=only_investable_universe,
133
+ exact_lookup=exact_lookup,
134
+ instrument_type=instrument_type,
135
+ currency=currency,
136
+ exchange=exchange,
137
+ name=name,
138
+ )
139
+ if instrument:
140
+ self.cache[cache_key] = instrument
141
+ return instrument
142
+
143
+ def lookup(self, only_security: bool = False, exact_lookup: bool = False, **lookup_kwargs):
144
+ # To speed up lookup process, we try to get the quote from the investable universe first
145
+ security = self._lookup_instrument(only_investable_universe=True, exact_lookup=exact_lookup, **lookup_kwargs)
146
+ if not security:
147
+ security = self._lookup_instrument(
148
+ only_investable_universe=False, exact_lookup=exact_lookup, **lookup_kwargs
149
+ )
150
+ if not only_security and security:
151
+ quotes = security.children.all()
152
+ if quotes.exists():
153
+ # We try to find the quote for that security based on the given exchange (if provided). Otherwise, we default to the security primary exchange
154
+ exchange = lookup_kwargs.get("exchange")
155
+ if exchange and quotes.filter(exchange=exchange).exists():
156
+ return quotes.filter(exchange=exchange).first()
157
+ else:
158
+ return quotes.filter(is_primary=True).first()
159
+ return security
160
+
161
+
162
+ class InstrumentImportHandler(ImportExportHandler):
163
+ MODEL_APP_LABEL: str = "wbfdm.Instrument"
164
+ allow_update_save_failure = True
165
+ exclude_update_fields = ["name", "isin", "country"]
166
+
167
+ def __init__(self, *args, **kwargs):
168
+ super().__init__(*args, **kwargs)
169
+ self.instrument_lookup = InstrumentLookup(self.model)
170
+ self.currency_handler = CurrencyImportHandler(self.import_source)
171
+
172
+ def _deserialize(self, data: Dict[str, Any]):
173
+ from wbfdm.models import Classification, InstrumentType
174
+
175
+ if isinstance(data, int):
176
+ data = dict(id=data)
177
+ if data.get("currency", None):
178
+ data["currency"] = self.currency_handler.process_object(data["currency"], read_only=True)[0]
179
+ if instrument_type := data.get("instrument_type", None):
180
+ if isinstance(instrument_type, str):
181
+ data["instrument_type"] = InstrumentType.objects.get_or_create(
182
+ key=instrument_type,
183
+ defaults={"name": instrument_type.title(), "short_name": instrument_type.title()},
184
+ )[0]
185
+ elif isinstance(instrument_type, int):
186
+ data["instrument_type"] = InstrumentType.objects.get(id=instrument_type)
187
+ if data.get("country", None):
188
+ data["country"] = Geography.dict_to_model(data["country"])
189
+ if data.get("headquarter_city", None):
190
+ data["headquarter_city"] = Geography.dict_to_model(data["headquarter_city"], level=Geography.Level.CITY)
191
+ if inception_date := data.get("inception_date", None):
192
+ data["inception_date"] = datetime.strptime(inception_date, "%Y-%m-%d").date()
193
+ if classifications := data.pop("classifications", None):
194
+ data["classifications"] = [Classification.dict_to_model(c) for c in classifications]
195
+ if (exchange_data := data.pop("exchange", None)) and isinstance(exchange_data, dict):
196
+ sanitized_dict = {k: v for k, v in exchange_data.items() if v is not None}
197
+ if sanitized_dict:
198
+ data["exchange"] = Exchange.dict_to_model(sanitized_dict)
199
+
200
+ def _get_instance(
201
+ self,
202
+ data: Dict[str, Any],
203
+ history: Optional[models.QuerySet] = None,
204
+ only_security: bool = False,
205
+ **kwargs,
206
+ ) -> models.Model:
207
+ if isinstance(data, self.model):
208
+ return data
209
+ if instrument_id := data.pop("id", None):
210
+ try:
211
+ return self.model.objects.get(id=instrument_id)
212
+ except self.model.DoesNotExist:
213
+ raise DeserializationError("Instrument id does not match an existing instrument")
214
+ else:
215
+ return self.instrument_lookup.lookup(only_security=only_security, **data)
216
+
217
+ def _create_instance(self, data: Dict[str, Any], **kwargs) -> models.Model:
218
+ from wbfdm.models.instruments.instruments import Instrument
219
+
220
+ if not data.get("name", None):
221
+ raise DeserializationError("Can't create an instrument without at least a name")
222
+ classifications = data.pop("classifications", None)
223
+ try:
224
+ obj = Instrument.objects.create(
225
+ **data,
226
+ import_source=self.import_source,
227
+ )
228
+ if classifications:
229
+ obj.classifications.set([c for c in classifications if c])
230
+ return obj
231
+ except IntegrityError:
232
+ self.import_source.log += f"\nError while creating new instrument with data {data}"
233
+
234
+ return None
235
+
236
+ def process_object(self, underlying_instrument_data, **kwargs):
237
+ if underlying_instrument_data:
238
+ if isinstance(underlying_instrument_data, self.model):
239
+ return underlying_instrument_data, ImportState.UNMODIFIED
240
+ if isinstance(underlying_instrument_data, int):
241
+ underlying_instrument_data = dict(id=underlying_instrument_data)
242
+ return super().process_object(
243
+ underlying_instrument_data,
244
+ include_update_fields=[
245
+ "isin",
246
+ "ticker",
247
+ "refinitiv_mnemonic_code",
248
+ "refinitiv_identifier_code",
249
+ "country",
250
+ "exchange",
251
+ ],
252
+ **kwargs,
253
+ )
@@ -0,0 +1,101 @@
1
+ from collections import defaultdict
2
+ from datetime import datetime
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from django.contrib.auth.models import Permission
6
+ from django.db import models
7
+ from django.db.models import Q
8
+ from slugify import slugify
9
+ from wbcore.contrib.authentication.models import User
10
+ from wbcore.contrib.io.exceptions import DeserializationError
11
+ from wbcore.contrib.io.imports import ImportExportHandler
12
+ from wbcore.contrib.notifications.dispatch import send_notification
13
+
14
+ from .instrument import InstrumentImportHandler
15
+
16
+
17
+ class InstrumentListImportHandler(ImportExportHandler):
18
+ MODEL_APP_LABEL: str = "wbfdm.InstrumentListThroughModel"
19
+
20
+ def __init__(self, *args, **kwargs):
21
+ super().__init__(*args, **kwargs)
22
+ self.instrument_handler = InstrumentImportHandler(self.import_source)
23
+
24
+ def _deserialize(self, data: Dict[str, Any]):
25
+ if from_date := data.get("from_date", None):
26
+ data["from_date"] = datetime.strptime(from_date, "%Y-%m-%d").date()
27
+ if to_date := data.get("to_date", None):
28
+ data["to_date"] = datetime.strptime(to_date, "%Y-%m-%d").date()
29
+ if instrument_list_data := data.pop("instrument_list", None):
30
+ if isinstance(instrument_list_data, int):
31
+ data["instrument_list"] = self.model.instrument_list.get_queryset().get(id=instrument_list_data)
32
+ elif isinstance(instrument_list_data, dict) and "name" in instrument_list_data:
33
+ data["instrument_list"] = self.model.instrument_list.get_queryset().get_or_create(
34
+ identifier=instrument_list_data.pop("identifier", slugify(instrument_list_data["name"])),
35
+ **instrument_list_data,
36
+ )[0]
37
+ if instrument_data := data.pop("instrument", None):
38
+ data["instrument"] = self.instrument_handler.process_object(
39
+ instrument_data, only_security=True, read_only=True
40
+ )[0]
41
+ # we try to automatically match the instrument name against a already known matched row
42
+ if instrument_str := data.get("instrument_str"):
43
+ already_existing_rows = self.model.objects.filter(
44
+ instrument__isnull=False,
45
+ instrument_str=instrument_str,
46
+ instrument_list=data.get("instrument_list", None),
47
+ )
48
+ if already_existing_rows.count() == 1:
49
+ data["instrument"] = already_existing_rows.first().instrument
50
+ if "instrument_list" not in data:
51
+ raise DeserializationError("Instrument List not find in this row")
52
+
53
+ def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
54
+ if instrument := data.get("instrument", None):
55
+ return self.model.objects.filter(
56
+ instrument=instrument,
57
+ instrument_list=data["instrument_list"],
58
+ ).first()
59
+
60
+ def _post_processing_objects(
61
+ self,
62
+ created_objs: List[models.Model],
63
+ modified_objs: List[models.Model],
64
+ unmodified_objs: List[models.Model],
65
+ ):
66
+ objs = modified_objs + unmodified_objs + created_objs
67
+ lists = set(map(lambda x: x.instrument_list, objs))
68
+ leftovers_objs = self.model.objects.filter(instrument_list__in=lists)
69
+ for obj in objs:
70
+ leftovers_objs = leftovers_objs.exclude(id=obj.id)
71
+ leftovers_objs.delete()
72
+
73
+ instrument_dict = defaultdict(list)
74
+ for obj in modified_objs + created_objs:
75
+ instrument_dict[obj.instrument_list].append(obj)
76
+ for instrument_list, items in instrument_dict.items():
77
+ if items:
78
+ report = """
79
+ <p>List of instrument added or modified:</p>
80
+ <ul>
81
+ """
82
+ for item in items:
83
+ if item.instrument:
84
+ report += f"<li>{item.instrument_str} (automatically link to {item.instrument}</li>"
85
+ else:
86
+ report += f"<li>{item.instrument_str}</li>"
87
+ report += "</ul>"
88
+ perm = Permission.objects.get(codename="administrate_instrumentlist")
89
+ for user in (
90
+ User.objects.filter(is_active=True)
91
+ .filter(Q(groups__permissions=perm) | Q(user_permissions=perm))
92
+ .distinct()
93
+ ):
94
+ send_notification(
95
+ code="wbfdm.instrument_list_add",
96
+ name=f"Instruments have been added or modified into the instrument list {instrument_list.name}",
97
+ body=report,
98
+ user=user,
99
+ reverse_name="wbfdm:instrumentlist-detail",
100
+ reverse_args=[instrument_list.id],
101
+ )