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,403 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ import logging
4
+ from collections import OrderedDict
5
+ from functools import wraps
6
+
7
+ from django.core import checks
8
+ from django.core.exceptions import FieldDoesNotExist
9
+ from django.db.models import ForeignObjectRel
10
+ from django.db.models.fields.related import ForeignObject
11
+ from django.db.models.fields.related_descriptors import (
12
+ ForwardManyToOneDescriptor,
13
+ ReverseOneToOneDescriptor,
14
+ )
15
+ from django.db.models.sql.where import AND, WhereNode
16
+
17
+
18
+ def get_cached_value(instance, descriptor, default=None):
19
+ return descriptor.field.get_cached_value(instance, default=default)
20
+
21
+
22
+ def set_cached_value_by_descriptor(instance, descriptor, value):
23
+ descriptor.field.set_cached_value(instance, value)
24
+
25
+
26
+ def set_cached_value_by_field(instance, field, value):
27
+ field.set_cached_value(instance, value)
28
+
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ # TODO: Figure out if nullable fields is a proper error
34
+ class CompositeForwardManyToOneDescriptor(ForwardManyToOneDescriptor):
35
+ def __set__(self, instance, value):
36
+ if value is not None or not self.field.nullable_fields:
37
+ super().__set__(instance, value)
38
+ else:
39
+ # we set only the asked fields to None, not all field as the default ForwardManyToOneDescriptor will
40
+
41
+ # ### taken from original ForwardManyToOneDescriptor
42
+ # Look up the previously-related object, which may still be available
43
+ # since we've not yet cleared out the related field.
44
+ # Use the cache directly, instead of the accessor; if we haven't
45
+ # populated the cache, then we don't care - we're only accessing
46
+ # the object to invalidate the accessor cache, so there's no
47
+ # need to populate the cache just to expire it again.
48
+ related = get_cached_value(instance, self, None)
49
+
50
+ # If we've got an old related object, we need to clear out its
51
+ # cache. This cache also might not exist if the related object
52
+ # hasn't been accessed yet.
53
+ if related is not None:
54
+ related_field = self.field.remote_field
55
+ set_cached_value_by_field(related, related_field, None)
56
+
57
+ # ##### only original part
58
+ for lh_field_name, none_value in self.field.nullable_fields.items():
59
+ setattr(instance, lh_field_name, none_value)
60
+
61
+ # Set the related instance cache used by __get__ to avoid a SQL query
62
+ # when accessing the attribute we just set.
63
+ set_cached_value_by_descriptor(instance, self, None)
64
+
65
+
66
+ class CompositeForeignKey(ForeignObject):
67
+ requires_unique_target = False
68
+ auto_created = False
69
+ empty_strings_allowed = False
70
+
71
+ many_to_many = False
72
+ many_to_one = False
73
+ one_to_many = True
74
+ one_to_one = False
75
+
76
+ rel_class = ForeignObjectRel
77
+
78
+ def __init__(self, to, **kwargs):
79
+ """
80
+ create the ForeignObject, but use the to_fields as a dict which will later used as form_fields and to_fields
81
+ """
82
+ to_fields = kwargs["to_fields"]
83
+ self.null_if_equal = kwargs.pop("null_if_equal", [])
84
+ nullable_fields = kwargs.pop("nullable_fields", {})
85
+ if not isinstance(nullable_fields, dict):
86
+ nullable_fields = {v: None for v in nullable_fields}
87
+ self.nullable_fields = nullable_fields
88
+
89
+ # a list of tuple : (fieldnaem, value) . if fielname = value, then the field react as if fieldnaem_id = None
90
+ self._raw_fields = self.compute_to_fields(to_fields)
91
+ # hiro nakamura should have said «very bad guy. you are vilain»
92
+ if "on_delete" in kwargs:
93
+ kwargs["on_delete"] = self.override_on_delete(kwargs["on_delete"])
94
+
95
+ kwargs["to_fields"], kwargs["from_fields"] = zip(
96
+ *((k, v.value) for k, v in self._raw_fields.items() if v.is_local_field)
97
+ )
98
+ super().__init__(to, **kwargs)
99
+
100
+ def override_on_delete(self, original):
101
+ @wraps(original)
102
+ def wrapper(collector, field, sub_objs, using):
103
+ res = original(collector, field, sub_objs, using)
104
+ # we make something nasty : we update the collector to
105
+ # skip the local field which does not have a dbcolumn
106
+ try:
107
+ del collector.field_updates[self.model][(self, None)]
108
+ except KeyError:
109
+ pass
110
+ return res
111
+
112
+ wrapper._original_fn = original
113
+
114
+ return wrapper
115
+
116
+ def check(self, **kwargs):
117
+ errors = super().check(**kwargs)
118
+ errors.extend(self._check_null_with_nullifequal())
119
+ errors.extend(self._check_nullifequal_fields_exists())
120
+ errors.extend(self._check_to_fields_local_valide())
121
+ errors.extend(self._check_to_fields_remote_valide())
122
+ errors.extend(self._check_recursion_field_dependecy())
123
+ errors.extend(self._check_bad_order_fields())
124
+ return errors
125
+
126
+ def _check_bad_order_fields(self):
127
+ res = []
128
+ try:
129
+ dependents = list(self.local_related_fields)
130
+ except FieldDoesNotExist:
131
+ return [] # the errors shall be raised befor by _check_recursion_field_dependecy
132
+
133
+ for field in self.model._meta.get_fields():
134
+ try:
135
+ dependents.remove(field)
136
+ except ValueError:
137
+ pass
138
+ if field == self:
139
+ if dependents:
140
+ # we met the current fields, but all dependent fields is not
141
+ # passed befor : we will have a problem in the init of some objects
142
+ # where the rest of dependents fields will override the
143
+ # values set by the current one (see Model.__init__)
144
+ res.append(
145
+ checks.Error(
146
+ "the field %s depend on the fields %s which is defined after. define them befor %s"
147
+ % (self.name, ",".join(f.name for f in dependents), self.name),
148
+ hint=None,
149
+ obj=self,
150
+ id="compositefk.E006",
151
+ )
152
+ )
153
+ break
154
+ return res
155
+
156
+ def _check_recursion_field_dependecy(self):
157
+ res = []
158
+ for local_field in self._raw_fields.values():
159
+ try:
160
+ f = self.model._meta.get_field(local_field.value)
161
+ if isinstance(f, CompositeForeignKey):
162
+ res.append(
163
+ checks.Error(
164
+ "the field %s depend on the field %s which is another CompositeForeignKey"
165
+ % (self.name, local_field),
166
+ hint=None,
167
+ obj=self,
168
+ id="compositefk.E005",
169
+ )
170
+ )
171
+ except FieldDoesNotExist:
172
+ pass # _check_to_fields_local_valide already raise errors for this
173
+ return res
174
+
175
+ def _check_to_fields_local_valide(self):
176
+ res = []
177
+ for local_field in self._raw_fields.values():
178
+ if isinstance(local_field, LocalFieldValue):
179
+ try:
180
+ self.model._meta.get_field(local_field.value)
181
+ except FieldDoesNotExist:
182
+ res.append(
183
+ checks.Error(
184
+ "the field %s does not exists on the model %s" % (local_field, self.model),
185
+ hint=None,
186
+ obj=self,
187
+ id="compositefk.E003",
188
+ )
189
+ )
190
+ return res
191
+
192
+ def _check_to_fields_remote_valide(self):
193
+ res = []
194
+ for remote_field in self._raw_fields.keys():
195
+ try:
196
+ self.related_model._meta.get_field(remote_field)
197
+ except FieldDoesNotExist:
198
+ res.append(
199
+ checks.Error(
200
+ "the field %s does not exists on the model %s" % (remote_field, self.model),
201
+ hint=None,
202
+ obj=self,
203
+ id="compositefk.E004",
204
+ )
205
+ )
206
+ return res
207
+
208
+ def _check_null_with_nullifequal(self):
209
+ if self.null_if_equal and not self.null:
210
+ return [
211
+ checks.Error(
212
+ "you must set null=True to field %s.%s if null_if_equal is given"
213
+ % (self.model.__class__.__name__, self.name),
214
+ hint=None,
215
+ obj=self,
216
+ id="compositefk.E001",
217
+ )
218
+ ]
219
+ return []
220
+
221
+ def _check_nullifequal_fields_exists(self):
222
+ res = []
223
+ for field_name, value in self.null_if_equal:
224
+ try:
225
+ self.model._meta.get_field(field_name)
226
+ except FieldDoesNotExist:
227
+ res.append(
228
+ checks.Error(
229
+ "the field %s does not exists on the model %s" % (field_name, self.model),
230
+ hint=None,
231
+ obj=self,
232
+ id="compositefk.E002",
233
+ )
234
+ )
235
+ return res
236
+
237
+ def deconstruct(self):
238
+ name, path, args, kwargs = super(CompositeForeignKey, self).deconstruct()
239
+ del kwargs["from_fields"]
240
+ if "on_delete" in kwargs:
241
+ kwargs["on_delete"] = kwargs["on_delete"]._original_fn
242
+ kwargs["to_fields"] = self._raw_fields
243
+ kwargs["null_if_equal"] = self.null_if_equal
244
+ return name, path, args, kwargs
245
+
246
+ def get_extra_descriptor_filter(self, instance):
247
+ return {k: v.value for k, v in self._raw_fields.items() if isinstance(v, RawFieldValue)}
248
+
249
+ def get_extra_restriction(self, alias, related_alias):
250
+ constraint = WhereNode(connector=AND)
251
+ for remote, local in self._raw_fields.items():
252
+ lookup = local.get_lookup(self, self.related_model._meta.get_field(remote), alias)
253
+ if lookup:
254
+ constraint.add(lookup, AND)
255
+ if constraint.children:
256
+ return constraint
257
+ else:
258
+ return None
259
+
260
+ def compute_to_fields(self, to_fields):
261
+ """
262
+ compute the to_fields parameterse to make it uniformly a dict of CompositePart
263
+ :param set[unicode]|dict[unicode, unicode] to_fields: the list/dict of fields to match
264
+ :return: the well formated to_field containing only subclasses of CompositePart
265
+ :rtype: dict[str, CompositePart]
266
+ """
267
+ # for problem in trim_join, we must try to give the fields in a consistent order with others models...
268
+ # see #26515 at https://code.djangoproject.com/ticket/26515
269
+
270
+ return OrderedDict(
271
+ (k, (v if isinstance(v, CompositePart) else LocalFieldValue(v)))
272
+ for k, v in (to_fields.items() if isinstance(to_fields, dict) else zip(to_fields, to_fields))
273
+ )
274
+
275
+ def db_type(self, connection):
276
+ # A CompositeForeignKey don't have a column in the database
277
+ # so return None.
278
+ return None
279
+
280
+ def db_parameters(self, connection):
281
+ return {"type": None, "check": None}
282
+
283
+ def contribute_to_class(self, cls, name, **kwargs):
284
+ super().contribute_to_class(cls, name, **kwargs)
285
+ setattr(cls, self.name, CompositeForwardManyToOneDescriptor(self))
286
+
287
+ def get_instance_value_for_fields(self, instance, fields):
288
+ # we override this method to provide the feathur of converting
289
+ # some special values of teh composite local fields into a
290
+ # None pointing field.
291
+ # ie, if company is ' ' and it mean that the current field
292
+ # point to nothing (as if it was None) => we transform this
293
+ # ' ' into a true None to let django das as if it was None
294
+ res = super(CompositeForeignKey, self).get_instance_value_for_fields(instance, fields)
295
+ if self.null_if_equal:
296
+ for field_name, exception_value in self.null_if_equal:
297
+ val = getattr(instance, field_name)
298
+ if val == exception_value:
299
+ # we have field_name that is equal to the bad value
300
+ # currently, it is enouth since the django implementation check at first
301
+ # if there is a None in the result
302
+ return (None,)
303
+ return res
304
+
305
+
306
+ class CompositeOneToOneField(CompositeForeignKey):
307
+ # Field flags
308
+ many_to_many = False
309
+ many_to_one = False
310
+ one_to_many = False
311
+ one_to_one = True
312
+
313
+ related_accessor_class = ReverseOneToOneDescriptor
314
+
315
+ description = "One-to-one relationship"
316
+
317
+ def __init__(self, to, **kwargs):
318
+ kwargs["unique"] = True
319
+ super().__init__(to, **kwargs)
320
+ self.remote_field.multiple = False
321
+
322
+ def deconstruct(self):
323
+ name, path, args, kwargs = super().deconstruct()
324
+ if "unique" in kwargs:
325
+ del kwargs["unique"]
326
+ return name, path, args, kwargs
327
+
328
+
329
+ class CompositePart(object):
330
+ is_local_field = True
331
+
332
+ def __init__(self, value):
333
+ self.value = value
334
+
335
+ def deconstruct(self):
336
+ module_name = self.__module__
337
+ name = self.__class__.__name__
338
+ return ("%s.%s" % (module_name, name), (self.value,), {})
339
+
340
+ def __repr__(self):
341
+ return "%s(%r)" % (self.__class__.__name__, self.value)
342
+
343
+ def __eq__(self, other):
344
+ if self.__class__ != other.__class__:
345
+ return False
346
+ return self.value == other.value
347
+
348
+ def get_lookup(self, main_field, for_remote, alias):
349
+ """
350
+ create a fake field for the lookup capability
351
+ :param CompositeForeignKey main_field: the local fk
352
+ :param Field for_remote: the remote field to match
353
+ :return:
354
+ """
355
+
356
+
357
+ class RawFieldValue(CompositePart):
358
+ """
359
+ represent a raw value for a field.
360
+ """
361
+
362
+ is_local_field = False
363
+
364
+ def get_lookup(self, main_field, for_remote, alias):
365
+ """
366
+ create a fake field for the lookup capability
367
+ :param CompositeForeignKey main_field: the local fk
368
+ :param Field for_remote: the remote field to match
369
+ :return:
370
+ """
371
+ lookup_class = for_remote.get_lookup("exact")
372
+ return lookup_class(for_remote.get_col(alias), self.value)
373
+
374
+
375
+ class FunctionBasedFieldValue(RawFieldValue):
376
+ def __init__(self, func):
377
+ self._func = func
378
+
379
+ def deconstruct(self):
380
+ module_name = self.__module__
381
+ name = self.__class__.__name__
382
+ return ("%s.%s" % (module_name, name), (self._func,), {})
383
+
384
+ def __eq__(self, other):
385
+ if self.__class__ != other.__class__:
386
+ return False
387
+ return self._func == other._func
388
+
389
+ @property
390
+ def value(self):
391
+ return self._func()
392
+
393
+ @value.setter
394
+ def value(self):
395
+ pass
396
+
397
+
398
+ class LocalFieldValue(CompositePart):
399
+ """
400
+ implicitly used, represent the value of a local field
401
+ """
402
+
403
+ is_local_field = True
File without changes
@@ -0,0 +1,19 @@
1
+ from .llm import run_company_extraction_llm
2
+ from .instruments import Instrument, InstrumentType, Cash, Equity
3
+ from .instrument_prices import InstrumentPrice
4
+ from .instrument_relationships import (
5
+ InstrumentClassificationRelatedInstrument,
6
+ InstrumentClassificationThroughModel,
7
+ InstrumentFavoriteGroup,
8
+ RelatedInstrumentThroughModel,
9
+ )
10
+ from .classifications import (
11
+ Classification,
12
+ ClassificationGroup,
13
+ )
14
+ from .instrument_lists import InstrumentList, InstrumentListThroughModel
15
+
16
+ from .instrument_requests import InstrumentRequest
17
+
18
+ from .private_equities import Deal
19
+ from .options import Option, OptionAggregate
@@ -0,0 +1,265 @@
1
+ from typing import List, Optional
2
+
3
+ import roman
4
+ from django.contrib.postgres.expressions import ArraySubquery
5
+ from django.db import models
6
+ from django.db.models.functions import Cast
7
+ from django.db.models.signals import post_save
8
+ from django.dispatch import receiver
9
+ from mptt.models import MPTTModel, TreeForeignKey
10
+ from wbcore.models import WBModel
11
+ from wbcore.utils.models import ComplexToStringMixin
12
+
13
+ from .instrument_relationships import InstrumentClassificationThroughModel
14
+
15
+
16
+ class ClassificationGroup(WBModel):
17
+ name = models.CharField(max_length=128, verbose_name="Name")
18
+ is_primary = models.BooleanField(
19
+ default=False,
20
+ verbose_name="Primary",
21
+ help_text="Set to True if this " "classification must be used as " "default if not specified " "otherwise",
22
+ )
23
+ max_depth = models.IntegerField(default=0, verbose_name="Maximum Depth")
24
+ code_level_digits = models.IntegerField(default=2, verbose_name="The number of digits per code level")
25
+
26
+ def __str__(self) -> str:
27
+ return f'{self.name} ({"Primary" if self.is_primary else "Non Primary"})'
28
+
29
+ def save(self, *args, **kwargs):
30
+ qs = ClassificationGroup.objects.filter(is_primary=True).exclude(id=self.id)
31
+ if self.is_primary:
32
+ qs.update(is_primary=False)
33
+ elif not qs.exists():
34
+ self.is_primary = True
35
+ return super().save(*args, **kwargs)
36
+
37
+ def get_levels_representation(self) -> List[str]:
38
+ return [
39
+ self.classifications.filter(height=i).first().level_representation
40
+ for i in range(self.max_depth + 1)
41
+ if self.classifications.filter(height=i).exists()
42
+ ]
43
+
44
+ def get_fields_names(self, sep: str = "__") -> list[str]:
45
+ return [f"parent{f'{sep}parent' * height}" for height in range(self.max_depth)]
46
+
47
+ def annotate_queryset(
48
+ self,
49
+ queryset: models.QuerySet,
50
+ classification_height: int,
51
+ instrument_label_key: str,
52
+ unique: bool = False,
53
+ annotation_label: str = "classifications",
54
+ ):
55
+ ref_id = "classification__"
56
+ if classification_height:
57
+ ref_id += f"{'parent__' * classification_height}"
58
+ ref_id += "id"
59
+ if instrument_label_key:
60
+ instrument_label_key += "__"
61
+ base_subquery = InstrumentClassificationThroughModel.objects.filter(
62
+ classification__group=self,
63
+ instrument__tree_id=models.OuterRef(f"{instrument_label_key}tree_id"),
64
+ instrument__lft__lte=models.OuterRef(f"{instrument_label_key}lft"),
65
+ instrument__rght__gte=models.OuterRef(f"{instrument_label_key}rght"),
66
+ )
67
+ if unique:
68
+ expression = models.Subquery(base_subquery.values(ref_id)[:1])
69
+ else:
70
+ expression = ArraySubquery(
71
+ base_subquery.values(ref_id).distinct(ref_id)
72
+ ) # we use distinct in order to remove duplicated classification (e.g. classification added to the parent as well as the children)
73
+
74
+ return queryset.annotate(**{annotation_label: expression})
75
+
76
+ class Meta:
77
+ verbose_name = "Classification Group"
78
+ verbose_name_plural = "Classification Groups"
79
+
80
+ @classmethod
81
+ def get_representation_endpoint(cls) -> str:
82
+ return "wbfdm:classificationgrouprepresentation-list"
83
+
84
+ @classmethod
85
+ def get_representation_value_key(cls) -> str:
86
+ return "id"
87
+
88
+ @classmethod
89
+ def get_representation_label_key(cls) -> str:
90
+ return "{{name}}"
91
+
92
+ @classmethod
93
+ def get_endpoint_basename(cls) -> str:
94
+ return "wbfdm:classificationgroup"
95
+
96
+
97
+ class Classification(MPTTModel, ComplexToStringMixin):
98
+ parent = TreeForeignKey(
99
+ "self",
100
+ related_name="children",
101
+ null=True,
102
+ blank=True,
103
+ on_delete=models.CASCADE,
104
+ verbose_name="Parent Classification",
105
+ )
106
+ height = models.PositiveIntegerField(
107
+ default=0,
108
+ verbose_name="The height (leaf node have height 0)",
109
+ )
110
+ group = models.ForeignKey(
111
+ ClassificationGroup,
112
+ related_name="classifications",
113
+ on_delete=models.CASCADE,
114
+ verbose_name="Classification Group",
115
+ )
116
+
117
+ level_representation = models.CharField(max_length=256, verbose_name="Level Representation")
118
+
119
+ name = models.CharField(max_length=128, verbose_name="Name")
120
+ code_aggregated = models.CharField(max_length=64, verbose_name="Code Aggregated")
121
+
122
+ investable = models.BooleanField(default=True, help_text="Is this classification investable for us?")
123
+
124
+ description = models.TextField(
125
+ default="",
126
+ blank=True,
127
+ help_text="Give a basic definition and description",
128
+ verbose_name="Definition/Description",
129
+ )
130
+
131
+ @classmethod
132
+ def dict_to_model(cls, classification_data):
133
+ if isinstance(classification_data, int):
134
+ return cls.objects.filter(id=classification_data).first()
135
+ res = cls.objects.all()
136
+ if code_aggregated := classification_data.get("code_aggregated", None):
137
+ res = res.filter(code_aggregated=code_aggregated)
138
+ if group_id := classification_data.get("group", None):
139
+ res = res.filter(group=group_id)
140
+ if res.count() == 1:
141
+ return res.first()
142
+
143
+ def __str__(self):
144
+ if self.computed_str:
145
+ return self.computed_str
146
+ return f"{self.code_aggregated} {self.name}"
147
+
148
+ def get_classified_instruments(self, only_favorites: bool = False) -> models.QuerySet:
149
+ childs_classifications = self.get_descendants(include_self=True)
150
+ params = {"classifications__in": childs_classifications}
151
+ if only_favorites:
152
+ params["classifications_through__is_favorite"] = True
153
+
154
+ from wbfdm.models import Instrument
155
+
156
+ return Instrument.objects.filter(**params).distinct()
157
+
158
+ def save(self, *args, **kwargs):
159
+ if self.parent:
160
+ self.group = self.parent.group
161
+ if not self.code_aggregated:
162
+ self.code_aggregated = self.get_next_valid_code(self.group, self.parent)
163
+ if not self.level_representation:
164
+ self.level_representation = self.get_default_level_representation(self.group, self.parent)
165
+ return super().save(*args, **kwargs)
166
+
167
+ def compute_str(self) -> str:
168
+ if parent := self.parent:
169
+ tree_titles = parent.name
170
+ while parent and (parent := parent.parent):
171
+ tree_titles += f" - {parent.name}"
172
+ return f"{self.code_aggregated} {self.name} [{tree_titles}] ({self.group.name})"
173
+ return f"{self.code_aggregated} {self.name} ({self.group.name})"
174
+
175
+ class Meta:
176
+ verbose_name = "Classification"
177
+ verbose_name_plural = "Classifications"
178
+ constraints = [models.UniqueConstraint(fields=["group", "code_aggregated"], name="unique_classification")]
179
+
180
+ @classmethod
181
+ def get_next_valid_code(cls, group: "ClassificationGroup", parent: "Classification | None" = None) -> str:
182
+ """
183
+ Return the next valid classification code given the classification parent and its group parameters
184
+ Args:
185
+ group: The classification group the estimated code belongs to
186
+ parent: The classification parent (if any)
187
+
188
+ Returns:
189
+ The next valid and unused aggregated classification code
190
+ """
191
+ if not group:
192
+ raise ValueError("This method needs a group")
193
+ siblings_classifications = (
194
+ Classification.objects.filter(parent=parent, group=group)
195
+ .annotate(casted_code=Cast("code_aggregated", models.BigIntegerField()))
196
+ .order_by("-casted_code")
197
+ )
198
+ parent_level = parent.level + 1 if parent else 0
199
+ code_aggregated_digits = group.code_level_digits * (parent_level + 1)
200
+ if last_classification := siblings_classifications.first():
201
+ for i in range(0, 100 - last_classification.casted_code % 10**group.code_level_digits):
202
+ if not siblings_classifications.filter(casted_code=last_classification.casted_code + i).exists():
203
+ last_valid_code = last_classification.casted_code + i
204
+ return f"{last_valid_code:0{code_aggregated_digits}}"
205
+ if parent:
206
+ return parent.code_aggregated + f"{1:0{group.code_level_digits}}"
207
+ return f"{1:0{group.code_level_digits}}"
208
+
209
+ @classmethod
210
+ def get_default_level_representation(
211
+ cls, group: "ClassificationGroup", parent: Optional["Classification"] = None
212
+ ) -> str:
213
+ """
214
+ Return the default level representation, extracted from the classification siblings
215
+ Args:
216
+ group: The classification group the estimated code belongs to
217
+ parent: The classification parent (if any)
218
+
219
+ Returns:
220
+ A default level representation (e.g. Industry)
221
+ """
222
+ level = parent.level + 1 if parent else None
223
+ siblings_classifications = cls.objects.filter(level=level, group=group).order_by("id")
224
+ if siblings_classifications.exists():
225
+ return siblings_classifications.last().level_representation
226
+ level = roman.toRoman(level) if level else 0
227
+ return f"Level {level}"
228
+
229
+ @classmethod
230
+ def get_representation_endpoint(cls) -> str:
231
+ return "wbfdm:classificationrepresentation-list"
232
+
233
+ @classmethod
234
+ def get_representation_value_key(cls) -> str:
235
+ return "id"
236
+
237
+ @classmethod
238
+ def get_endpoint_basename(cls) -> str:
239
+ return "wbfdm:classification"
240
+
241
+
242
+ @receiver(post_save, sender="wbfdm.Classification")
243
+ def post_save_parent_classification(sender, instance, created, raw, **kwargs):
244
+ # Recursively call the parent save function to recompute its parameters
245
+ if not raw and instance.parent:
246
+ instance.parent.save()
247
+ if instance.level is not None:
248
+ update_fields = {"height": instance.group.max_depth - instance.level}
249
+ Classification.objects.filter(id=instance.id).update(**update_fields)
250
+ instance.refresh_from_db()
251
+ # # Ensure initial parent classifcation span the proper classification tree structure
252
+ if instance.group and not instance.get_descendants().exists() and instance.level < instance.group.max_depth:
253
+ Classification.objects.create(
254
+ parent=instance,
255
+ group=instance.group,
256
+ name=f"{instance.name} (Level {instance.level})",
257
+ )
258
+ """
259
+ If a parent classification is not investable, therefore its "children" become non investable too, by cascade.
260
+ If a child classification becomes investable and one of its parent is non investable, therefore he cannot be
261
+ investable as long as its parent is non investable. (-> in the serializer)
262
+ """
263
+ if not raw and not instance.investable and instance.get_descendants().exists():
264
+ descandants = instance.get_descendants()
265
+ descandants.update(investable=False)