CatPtain commited on
Commit
b8f2a01
·
verified ·
1 Parent(s): e5b541f

Upload 292 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. openbb_platform/providers/imf/README.md +35 -0
  2. openbb_platform/providers/imf/__init__.py +1 -0
  3. openbb_platform/providers/imf/openbb_imf/__init__.py +18 -0
  4. openbb_platform/providers/imf/openbb_imf/assets/__init__.py +1 -0
  5. openbb_platform/providers/imf/openbb_imf/assets/imf_country_map.json +260 -0
  6. openbb_platform/providers/imf/openbb_imf/assets/imf_symbols.json +0 -0
  7. openbb_platform/providers/imf/openbb_imf/models/__init__.py +1 -0
  8. openbb_platform/providers/imf/openbb_imf/models/available_indicators.py +126 -0
  9. openbb_platform/providers/imf/openbb_imf/models/direction_of_trade.py +274 -0
  10. openbb_platform/providers/imf/openbb_imf/models/economic_indicators.py +303 -0
  11. openbb_platform/providers/imf/openbb_imf/utils/__init__.py +1 -0
  12. openbb_platform/providers/imf/openbb_imf/utils/constants.py +125 -0
  13. openbb_platform/providers/imf/openbb_imf/utils/dot_helpers.py +87 -0
  14. openbb_platform/providers/imf/openbb_imf/utils/fsi_helpers.py +247 -0
  15. openbb_platform/providers/imf/openbb_imf/utils/helpers.py +34 -0
  16. openbb_platform/providers/imf/openbb_imf/utils/irfcl_helpers.py +283 -0
  17. openbb_platform/providers/imf/poetry.lock +0 -0
  18. openbb_platform/providers/imf/pyproject.toml +19 -0
  19. openbb_platform/providers/imf/tests/__init__.py +1 -0
  20. openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_direction_of_trade_fetcher_urllib3_v1.yaml +55 -0
  21. openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_direction_of_trade_fetcher_urllib3_v2.yaml +55 -0
  22. openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_economic_indicators_fetcher_urllib3_v1.yaml +54 -0
  23. openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_economic_indicators_fetcher_urllib3_v2.yaml +54 -0
  24. openbb_platform/providers/imf/tests/test_imf_fetchers.py +78 -0
  25. openbb_platform/providers/intrinio/README.md +13 -0
  26. openbb_platform/providers/intrinio/__init__.py +1 -0
  27. openbb_platform/providers/intrinio/openbb_intrinio/__init__.py +116 -0
  28. openbb_platform/providers/intrinio/openbb_intrinio/models/__init__.py +1 -0
  29. openbb_platform/providers/intrinio/openbb_intrinio/models/balance_sheet.py +506 -0
  30. openbb_platform/providers/intrinio/openbb_intrinio/models/calendar_ipo.py +193 -0
  31. openbb_platform/providers/intrinio/openbb_intrinio/models/cash_flow.py +338 -0
  32. openbb_platform/providers/intrinio/openbb_intrinio/models/company_filings.py +175 -0
  33. openbb_platform/providers/intrinio/openbb_intrinio/models/company_news.py +295 -0
  34. openbb_platform/providers/intrinio/openbb_intrinio/models/currency_pairs.py +90 -0
  35. openbb_platform/providers/intrinio/openbb_intrinio/models/equity_historical.py +274 -0
  36. openbb_platform/providers/intrinio/openbb_intrinio/models/equity_info.py +78 -0
  37. openbb_platform/providers/intrinio/openbb_intrinio/models/equity_quote.py +142 -0
  38. openbb_platform/providers/intrinio/openbb_intrinio/models/equity_search.py +89 -0
  39. openbb_platform/providers/intrinio/openbb_intrinio/models/etf_holdings.py +214 -0
  40. openbb_platform/providers/intrinio/openbb_intrinio/models/etf_info.py +655 -0
  41. openbb_platform/providers/intrinio/openbb_intrinio/models/etf_price_performance.py +227 -0
  42. openbb_platform/providers/intrinio/openbb_intrinio/models/etf_search.py +148 -0
  43. openbb_platform/providers/intrinio/openbb_intrinio/models/financial_attributes.py +78 -0
  44. openbb_platform/providers/intrinio/openbb_intrinio/models/financial_ratios.py +320 -0
  45. openbb_platform/providers/intrinio/openbb_intrinio/models/forward_ebitda_estimates.py +201 -0
  46. openbb_platform/providers/intrinio/openbb_intrinio/models/forward_eps_estimates.py +236 -0
  47. openbb_platform/providers/intrinio/openbb_intrinio/models/forward_pe_estimates.py +165 -0
  48. openbb_platform/providers/intrinio/openbb_intrinio/models/forward_sales_estimates.py +254 -0
  49. openbb_platform/providers/intrinio/openbb_intrinio/models/fred_series.py +114 -0
  50. openbb_platform/providers/intrinio/openbb_intrinio/models/historical_attributes.py +144 -0
openbb_platform/providers/imf/README.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenBB IMF Provider Extension
2
+
3
+ This package adds the `openbb-imf` provider extension to the OpenBB Platform.
4
+
5
+ ## Installation
6
+
7
+ Install from PyPI with:
8
+
9
+ ```sh
10
+ pip install openbb-imf
11
+ ```
12
+
13
+ ## Implementation
14
+
15
+ The extension utilizes the JSON RESTful Web Service ((https://datahelp.imf.org/knowledgebase/articles/630877-data-services)[https://datahelp.imf.org/knowledgebase/articles/630877-data-services])
16
+
17
+ No authorization is required to use, but IP addresses are bound by the limitations described in the link above.
18
+
19
+ ## Coverage
20
+
21
+ - Databases:
22
+ - International Reserves and Foreign Currency Liquidity
23
+ - Direction of Trade Statistics
24
+ - Financial Soundness Indicators
25
+
26
+ Coverage:
27
+ - All IRFCL tables.
28
+ - Individual, or multiple, time series from single or multiple countries.
29
+ - Core and Encouraged Set tables, plus all individual underlying series.
30
+
31
+ ### Endpoints
32
+
33
+ - `obb.economy.available_indicators`
34
+ - `obb.economy.indicators`
35
+ - `obb.economy.direction_of_trade`
openbb_platform/providers/imf/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """IMF Provoider Extension."""
openbb_platform/providers/imf/openbb_imf/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenBB IMF Provider Module."""
2
+
3
+ from openbb_core.provider.abstract.provider import Provider
4
+ from openbb_imf.models.available_indicators import ImfAvailableIndicatorsFetcher
5
+ from openbb_imf.models.direction_of_trade import ImfDirectionOfTradeFetcher
6
+ from openbb_imf.models.economic_indicators import ImfEconomicIndicatorsFetcher
7
+
8
+ imf_provider = Provider(
9
+ name="imf",
10
+ website="https://datahelp.imf.org/knowledgebase/articles/667681-using-json-restful-web-service",
11
+ description="This provider allows you to access International Monetary Fund data through the IMF Public Data API.",
12
+ fetcher_dict={
13
+ "AvailableIndicators": ImfAvailableIndicatorsFetcher,
14
+ "DirectionOfTrade": ImfDirectionOfTradeFetcher,
15
+ "EconomicIndicators": ImfEconomicIndicatorsFetcher,
16
+ },
17
+ repr_name="International Monetary Fund (IMF) Public Data API",
18
+ )
openbb_platform/providers/imf/openbb_imf/assets/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """IMF Static Assets."""
openbb_platform/providers/imf/openbb_imf/assets/imf_country_map.json ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "AF": "Afghanistan",
3
+ "AL": "Albania",
4
+ "DZ": "Algeria",
5
+ "AS": "American Samoa",
6
+ "AO": "Angola",
7
+ "AI": "Anguilla",
8
+ "AG": "Antigua and Barbuda",
9
+ "AR": "Argentina",
10
+ "AM": "Armenia",
11
+ "AW": "Aruba",
12
+ "S19": "Asia not allocated",
13
+ "AU": "Australia",
14
+ "AT": "Austria",
15
+ "AZ": "Azerbaijan",
16
+ "BS": "Bahamas",
17
+ "BH": "Bahrain",
18
+ "BD": "Bangladesh",
19
+ "BB": "Barbados",
20
+ "BY": "Belarus",
21
+ "BE": "Belgium",
22
+ "R1": "Belgo-Luxembourg Economic Union",
23
+ "BZ": "Belize",
24
+ "BJ": "Benin",
25
+ "BM": "Bermuda",
26
+ "BT": "Bhutan",
27
+ "BO": "Bolivia",
28
+ "BA": "Bosnia and Herzegovina",
29
+ "BW": "Botswana",
30
+ "BR": "Brazil",
31
+ "BN": "Brunei Darussalam",
32
+ "BG": "Bulgaria",
33
+ "BF": "Burkina Faso",
34
+ "BI": "Burundi",
35
+ "CV": "Cabo Verde",
36
+ "KH": "Cambodia",
37
+ "CM": "Cameroon",
38
+ "CA": "Canada",
39
+ "5Y": "Eastern Caribbean Currency Union",
40
+ "CF": "Central African Republic",
41
+ "TD": "Chad",
42
+ "CL": "Chile",
43
+ "HK": "Hong Kong",
44
+ "MO": "Macao",
45
+ "CN": "China",
46
+ "CO": "Colombia",
47
+ "KM": "Comoros",
48
+ "CD": "Democratic Republic of the Congo",
49
+ "CG": "Congo",
50
+ "CR": "Costa Rica",
51
+ "CI": "Ivory Coast",
52
+ "HR": "Croatia",
53
+ "CU": "Cuba",
54
+ "CW": "Curacao",
55
+ "CY": "Cyprus",
56
+ "CZ": "Czech Republic",
57
+ "CSH": "Former Czechoslovakia",
58
+ "DK": "Denmark",
59
+ "DJ": "Djibouti",
60
+ "DM": "Dominica",
61
+ "DO": "Dominican Republic",
62
+ "DE2": "East Germany",
63
+ "EC": "Ecuador",
64
+ "EG": "Egypt",
65
+ "SV": "El Salvador",
66
+ "GQ": "Equatorial Guinea",
67
+ "ER": "Eritrea",
68
+ "EE": "Estonia",
69
+ "SZ": "Eswatini",
70
+ "ET": "Ethiopia",
71
+ "E19": "Europe not allocated",
72
+ "FK": "Falkland Islands (Malvinas)",
73
+ "FO": "Faroe Islands",
74
+ "FJ": "Fiji",
75
+ "FI": "Finland",
76
+ "FR": "France",
77
+ "PF": "French Polynesia",
78
+ "GA": "Gabon",
79
+ "GM": "Gambia",
80
+ "GE": "Georgia",
81
+ "DE": "Germany",
82
+ "GH": "Ghana",
83
+ "GI": "Gibraltar",
84
+ "GR": "Greece",
85
+ "GL": "Greenland",
86
+ "GD": "Grenada",
87
+ "GU": "Guam",
88
+ "GT": "Guatemala",
89
+ "GN": "Guinea",
90
+ "GW": "Guinea-Bissau",
91
+ "GY": "Guyana",
92
+ "HT": "Haiti",
93
+ "VA": "Vatican City State",
94
+ "HN": "Honduras",
95
+ "HU": "Hungary",
96
+ "IS": "Iceland",
97
+ "IN": "India",
98
+ "ID": "Indonesia",
99
+ "IR": "Iran",
100
+ "IQ": "Iraq",
101
+ "IE": "Ireland",
102
+ "IL": "Israel",
103
+ "IT": "Italy",
104
+ "JM": "Jamaica",
105
+ "JP": "Japan",
106
+ "JO": "Jordan",
107
+ "KZ": "Kazakhstan",
108
+ "KE": "Kenya",
109
+ "KI": "Kiribati",
110
+ "KP": "North Korea",
111
+ "KR": "South Korea",
112
+ "XK": "Kosovo",
113
+ "KW": "Kuwait",
114
+ "KG": "Kyrgyzstan",
115
+ "LA": "Lao",
116
+ "LV": "Latvia",
117
+ "LB": "Lebanon",
118
+ "LS": "Lesotho",
119
+ "LR": "Liberia",
120
+ "LY": "Libya",
121
+ "LT": "Lithuania",
122
+ "LU": "Luxembourg",
123
+ "MG": "Madagascar",
124
+ "MW": "Malawi",
125
+ "MY": "Malaysia",
126
+ "1C_554": "West Malaysia",
127
+ "MV": "Maldives",
128
+ "ML": "Mali",
129
+ "MT": "Malta",
130
+ "MH": "Marshall islands",
131
+ "MR": "Mauritania",
132
+ "MU": "Mauritius",
133
+ "MX": "Mexico",
134
+ "FM": "Micronesia",
135
+ "F979": "Middle East and Central Asia not specified",
136
+ "MD": "Moldova",
137
+ "MN": "Mongolia",
138
+ "ME": "Montenegro",
139
+ "MS": "Montserrat",
140
+ "MA": "Morocco",
141
+ "MZ": "Mozambique",
142
+ "MM": "Myanmar",
143
+ "NA": "Namibia",
144
+ "NR": "Nauru",
145
+ "NP": "Nepal",
146
+ "AN": "Antilles",
147
+ "NL": "Netherlands",
148
+ "NC": "New Caledonia",
149
+ "NZ": "New Zealand",
150
+ "NI": "Nicaragua",
151
+ "NE": "Niger",
152
+ "NG": "Nigeria",
153
+ "MK": "North Macedonia",
154
+ "1C_958": "North Vietnam",
155
+ "NO": "Norway",
156
+ "OM": "Oman",
157
+ "PK": "Pakistan",
158
+ "PW": "Palau",
159
+ "PA": "Panama",
160
+ "PG": "Papua New Guinea",
161
+ "PY": "Paraguay",
162
+ "PE": "Peru",
163
+ "PH": "Philippines",
164
+ "PL": "Poland",
165
+ "PT": "Portugal",
166
+ "QA": "Qatar",
167
+ "RO": "Romania",
168
+ "RU": "Russia",
169
+ "RW": "Rwanda",
170
+ "WS": "Samoa",
171
+ "SM": "San Marino",
172
+ "ST": "Sao Tome and Principe",
173
+ "SA": "Saudi Arabia",
174
+ "SN": "Senegal",
175
+ "CS": "Serbia and Montenegro",
176
+ "RS": "Serbia",
177
+ "SC": "Seychelles",
178
+ "SL": "Sierra Leone",
179
+ "SG": "Singapore",
180
+ "SX": "Sint Maarten (Dutch part)",
181
+ "SK": "Slovakia",
182
+ "SI": "Slovenia",
183
+ "SB": "Solomon Islands",
184
+ "SO": "Somalia",
185
+ "ZA": "South Africa",
186
+ "1C_198": "South African Common Customs Area",
187
+ "SS": "South Sudan",
188
+ "ES": "Spain",
189
+ "LK": "Sri Lanka",
190
+ "KN": "Saint Kitts and Nevis",
191
+ "LC": "Saint Lucia",
192
+ "VC": "Saint Vincent and the Grenadines",
193
+ "SD": "Sudan",
194
+ "SR": "Suriname",
195
+ "SE": "Sweden",
196
+ "CH": "Switzerland",
197
+ "SY": "Syria",
198
+ "TW": "Taiwan",
199
+ "TJ": "Tajikistan",
200
+ "TZ": "Tanzania",
201
+ "TH": "Thailand",
202
+ "TL": "Timor-Leste",
203
+ "TG": "Togo",
204
+ "TO": "Tonga",
205
+ "TT": "Trinidad and Tobago",
206
+ "TN": "Tunisia",
207
+ "TR": "Turkey",
208
+ "TM": "Turkmenistan",
209
+ "TV": "Tuvalu",
210
+ "UG": "Uganda",
211
+ "UA": "Ukraine",
212
+ "AE": "United Arab Emirates",
213
+ "GB": "United Kingdom",
214
+ "US": "United States",
215
+ "UY": "Uruguay",
216
+ "SUH": "Former USSR",
217
+ "UZ": "Uzbekistan",
218
+ "VU": "Vanuatu",
219
+ "VE": "Venezuela",
220
+ "VN": "Viet Nam",
221
+ "PS": "Palestine",
222
+ "YE": "Yemen",
223
+ "YUC": "Former Yugoslavia",
224
+ "ZM": "Zambia",
225
+ "ZW": "Zimbabwe",
226
+ "R14": "Community of Independent States",
227
+ "U2": "Euro Area",
228
+ "E1": "Europe",
229
+ "B0": "European Union",
230
+ "F97": "Middle East",
231
+ "XS5": "Middle East and Central Asia",
232
+ "X88": "Other Countries n.i.e. (IMF)",
233
+ "F1": "Africa",
234
+ "F6": "Sub-Saharan Africa",
235
+ "F19": "Africa not allocated",
236
+ "A10": "Western Hemisphere",
237
+ "A109": "Western Hemisphere not allocated",
238
+ "W00": "World",
239
+ "XR99": "Special Categories and Economic Zones",
240
+ "XS25": "Developing Asia (IMF)",
241
+ "XR43": "Emerging and Developing Countries",
242
+ "XR29": "Advanced Economies (IMF)",
243
+ "1C_ALLC": "All Countries",
244
+ "1C_ALL": "All Countries and Country Groups",
245
+ "1C_ALLG": "All Country Groups",
246
+ "1C_903": "Emerging and Developing Europe",
247
+ "1C_080": "Export Earnings: Fuel",
248
+ "1C_092": "Export Earnings: Nonfuel",
249
+ "1C_440": "Middle East, North Africa, Afghanistan, and Pakistan",
250
+ "1C_473": "Yemen Arab Rep",
251
+ "1C_459": "Yemen P.D. Rep",
252
+ "1C_All_Reporting_Countries_Data": "All Reporting Countries, Data",
253
+ "1C_All_Reporting_Countries_Metadata": "All Reporting Countries, Metadata",
254
+ "1C_Daily_Reporters": "Daily Reporters",
255
+ "1C_Dual_Reporters": "Dual Reporters",
256
+ "1C_Monthly_Reporters": "Monthly Reporters",
257
+ "1C_Single_Reporters": "Single Reporters",
258
+ "1C_Single_Reporters_USA_and_Peru": "Single Reporters - USA and Peru",
259
+ "_X": "Unspecified"
260
+ }
openbb_platform/providers/imf/openbb_imf/assets/imf_symbols.json ADDED
The diff for this file is too large to render. See raw diff
 
openbb_platform/providers/imf/openbb_imf/models/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """OpenBB IMF Provider Models."""
openbb_platform/providers/imf/openbb_imf/models/available_indicators.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IMF Available Indicators."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from typing import Any, Optional, Union
6
+
7
+ from openbb_core.app.model.abstract.error import OpenBBError
8
+ from openbb_core.provider.abstract.fetcher import Fetcher
9
+ from openbb_core.provider.standard_models.available_indicators import (
10
+ AvailableIndicatorsData,
11
+ AvailableIndicesQueryParams,
12
+ )
13
+ from pydantic import Field
14
+
15
+
16
+ class ImfAvailableIndicatorsQueryParams(AvailableIndicesQueryParams):
17
+ """IMF Available Indicators Query Parameters."""
18
+
19
+ __json_schema_extra__ = {"query": {"multiple_items_allowed": True}}
20
+
21
+ query: Optional[str] = Field(
22
+ default=None,
23
+ description="The query string to search through the available indicators."
24
+ + " Use semicolons to separate multiple terms.",
25
+ )
26
+
27
+
28
+ class ImfAvailableIndicatorsData(AvailableIndicatorsData):
29
+ """IMF Available Indicators Data."""
30
+
31
+ __alias_dict__ = {
32
+ "symbol_root": "parent",
33
+ "description": "title",
34
+ }
35
+ dataset: Optional[str] = Field(
36
+ default=None,
37
+ description="The IMF dataset associated with the symbol.",
38
+ )
39
+ table: Optional[str] = Field(
40
+ default=None,
41
+ description="The name of the table associated with the symbol.",
42
+ )
43
+ level: Optional[int] = Field(
44
+ default=None,
45
+ description="The indentation level of the data, relative to the table and symbol_root",
46
+ )
47
+ order: Optional[Union[int, float]] = Field(
48
+ default=None,
49
+ description="Order of the data, relative to the table.",
50
+ )
51
+ children: Optional[str] = Field(
52
+ default=None,
53
+ description="The symbol of the child data, if any.",
54
+ )
55
+ unit: Optional[str] = Field(
56
+ default=None,
57
+ description="The unit of the data.",
58
+ )
59
+
60
+
61
+ class ImfAvailableIndicatorsFetcher(
62
+ Fetcher[ImfAvailableIndicatorsQueryParams, list[ImfAvailableIndicatorsData]]
63
+ ):
64
+ """IMF Available Indicators Fetcher."""
65
+
66
+ @staticmethod
67
+ def transform_query(params: dict[str, Any]) -> ImfAvailableIndicatorsQueryParams:
68
+ """Transform the query."""
69
+ return ImfAvailableIndicatorsQueryParams(**params)
70
+
71
+ @staticmethod
72
+ def extract_data(
73
+ query: ImfAvailableIndicatorsQueryParams,
74
+ credentials: Optional[dict[str, Any]] = None,
75
+ **kwargs: Any,
76
+ ) -> list[dict]:
77
+ """Fetch the data."""
78
+ # pylint: disable=import-outside-toplevel
79
+ from numpy import nan
80
+ from openbb_core.provider.utils.errors import EmptyDataError
81
+ from openbb_imf.utils.constants import load_symbols
82
+ from pandas import DataFrame, Series
83
+
84
+ try:
85
+ all_symbols = load_symbols("all")
86
+ except OpenBBError as e:
87
+ raise OpenBBError(f"Failed to load IMF symbols static file: {e}") from e
88
+
89
+ terms = [term.strip() for term in query.query.split(";")] if query.query else []
90
+
91
+ df = (
92
+ DataFrame(all_symbols)
93
+ .T.reset_index()
94
+ .rename(columns={"index": "symbol"})
95
+ .replace({nan: None})
96
+ )
97
+
98
+ if not terms:
99
+ records = df.to_dict(orient="records")
100
+ else:
101
+ combined_mask = Series([True] * len(df))
102
+ for term in terms:
103
+ mask = df.apply(
104
+ lambda row, term=term: row.astype(str).str.contains(
105
+ term, case=False, regex=True, na=False
106
+ )
107
+ ).any(axis=1)
108
+ combined_mask &= mask
109
+
110
+ matches = df[combined_mask]
111
+
112
+ if matches.empty:
113
+ raise EmptyDataError("No results found for the provided query.")
114
+
115
+ records = matches.to_dict(orient="records")
116
+
117
+ return records
118
+
119
+ @staticmethod
120
+ def transform_data(
121
+ query: ImfAvailableIndicatorsQueryParams,
122
+ data: list[dict],
123
+ **kwargs: Any,
124
+ ) -> list[ImfAvailableIndicatorsData]:
125
+ """Transform the data."""
126
+ return [ImfAvailableIndicatorsData.model_validate(d) for d in data]
openbb_platform/providers/imf/openbb_imf/models/direction_of_trade.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IMF Direction Of Trade Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from datetime import datetime
6
+ from typing import Any, Optional, Union
7
+
8
+ from openbb_core.app.model.abstract.error import OpenBBError
9
+ from openbb_core.provider.abstract.fetcher import Fetcher
10
+ from openbb_core.provider.standard_models.direction_of_trade import (
11
+ DirectionOfTradeData,
12
+ DirectionOfTradeQueryParams,
13
+ )
14
+ from openbb_core.provider.utils.errors import EmptyDataError
15
+ from openbb_imf.utils.dot_helpers import (
16
+ load_country_map,
17
+ load_country_to_code_map,
18
+ validate_countries,
19
+ )
20
+
21
+ dot_indicators_dict = {
22
+ "exports": "TXG_FOB_USD",
23
+ "imports": "TMG_CIF_USD",
24
+ "balance": "TBG_USD",
25
+ "all": "TXG_FOB_USD+TMG_CIF_USD+TBG_USD",
26
+ }
27
+
28
+ dot_titles_map = {
29
+ "TXG_FOB_USD": "Goods, Value of Exports, Free on board (FOB), US Dollars",
30
+ "TMG_CIF_USD": "Goods, Value of Imports, Cost, Insurance, Freight (CIF), US Dollars",
31
+ "TBG_USD": "Goods, Value of Trade Balance, US Dollars",
32
+ }
33
+
34
+
35
+ class ImfDirectionOfTradeQueryParams(DirectionOfTradeQueryParams):
36
+ """IMF Direction Of Trade Query Parameters."""
37
+
38
+ __json_schema_extra__ = {
39
+ "country": {
40
+ "multiple_items_allowed": True,
41
+ "choices": ["all"] + sorted(list(load_country_to_code_map())),
42
+ },
43
+ "counterpart": {
44
+ "multiple_items_allowed": True,
45
+ "choices": ["all"] + sorted(list(load_country_to_code_map())),
46
+ },
47
+ }
48
+
49
+
50
+ class ImfDirectionOfTradeData(DirectionOfTradeData):
51
+ """IMF Direction Of Trade Data."""
52
+
53
+
54
+ class ImfDirectionOfTradeFetcher(
55
+ Fetcher[ImfDirectionOfTradeQueryParams, list[ImfDirectionOfTradeData]]
56
+ ):
57
+ """IMF Direction Of Trade Fetcher."""
58
+
59
+ @staticmethod
60
+ def transform_query(params: dict[str, Any]) -> ImfDirectionOfTradeQueryParams:
61
+ """Transform query parameters."""
62
+ countries = params.get("country", "")
63
+ countries = countries.split(",") if countries else "all"
64
+ if countries != "all":
65
+ countries = validate_countries(countries)
66
+ counterparts = params.get("counterpart", "")
67
+ counterparts = counterparts.split(",") if params.get("counterpart") else "all"
68
+ if counterparts != "all":
69
+ counterparts = validate_countries(counterparts)
70
+ now = datetime.now().date()
71
+
72
+ if countries == "all" and counterparts == "all":
73
+ raise OpenBBError(
74
+ "Both 'country' and 'counterpart' cannot be None, or 'all'."
75
+ + " Please supply lowercase country names or two-letter ISO codes."
76
+ )
77
+ if countries == counterparts:
78
+ raise OpenBBError("The 'country' and 'counterpart' cannot be the same.")
79
+
80
+ params["country"] = countries
81
+ params["counterpart"] = counterparts
82
+
83
+ if not params.get("end_date"):
84
+ params["end_date"] = now.replace(month=12, day=31).strftime("%Y-%m-%d")
85
+
86
+ if (countries == "all" or counterparts == "all") and not params.get(
87
+ "start_date"
88
+ ):
89
+ params["start_date"] = now.replace(year=now.year - 1).strftime("%Y-%m-%d")
90
+
91
+ return ImfDirectionOfTradeQueryParams(**params)
92
+
93
+ @staticmethod
94
+ async def aextract_data(
95
+ query: ImfDirectionOfTradeQueryParams,
96
+ credentials: Optional[dict[str, str]],
97
+ **kwargs: Any,
98
+ ) -> list[dict]:
99
+ """Extract the data from the IMF API."""
100
+ # pylint: disable=import-outside-toplevel
101
+ from aiohttp.client_exceptions import ContentTypeError # noqa
102
+ from json import JSONDecodeError
103
+ from openbb_core.provider.utils.helpers import amake_request
104
+ from pandas import to_datetime
105
+ from pandas.tseries import offsets
106
+
107
+ start_date = query.start_date
108
+ end_date = query.end_date
109
+ frequency = query.frequency[0].upper()
110
+ country = query.country if query.country != "all" else ""
111
+ counterpart = query.counterpart if query.counterpart != "all" else ""
112
+ indicator = dot_indicators_dict[query.direction]
113
+ # Adjust the dates to the date relative to frequency.
114
+ # The API does not accept arbitrary dates, so we need to adjust them.
115
+ if start_date:
116
+ start_date = to_datetime(start_date)
117
+ if frequency == "Q":
118
+ start_date = offsets.QuarterBegin(startingMonth=1).rollback(start_date)
119
+ elif frequency == "A":
120
+ start_date = offsets.YearBegin().rollback(start_date)
121
+ else:
122
+ start_date = offsets.MonthBegin().rollback(start_date)
123
+ start_date = start_date.strftime("%Y-%m-%d") # type: ignore
124
+
125
+ if end_date:
126
+ end_date = to_datetime(end_date)
127
+ if frequency == "Q":
128
+ end_date = offsets.QuarterEnd().rollforward(end_date)
129
+ elif frequency == "A":
130
+ end_date = offsets.YearEnd().rollforward(end_date)
131
+ else:
132
+ end_date = offsets.MonthEnd().rollforward(end_date)
133
+ end_date = end_date.strftime("%Y-%m-%d") # type: ignore
134
+
135
+ date_range = ( # type: ignore
136
+ f"?startPeriod={start_date}&endPeriod={end_date}"
137
+ if start_date and end_date
138
+ else ""
139
+ )
140
+ base_url = "http://dataservices.imf.org/REST/SDMX_JSON.svc/"
141
+ key = f"CompactData/DOT/{frequency}.{country}.{indicator}.{counterpart}"
142
+ url = f"{base_url}{key}{date_range}"
143
+
144
+ try:
145
+ response = await amake_request(url, timeout=20)
146
+ except (JSONDecodeError, ContentTypeError) as e:
147
+ raise OpenBBError(
148
+ "Error fetching data; This might be rate-limiting. Try again later."
149
+ ) from e
150
+
151
+ if "ErrorDetails" in response:
152
+ raise OpenBBError(
153
+ f"{response['ErrorDetails'].get('Code')} -> {response['ErrorDetails'].get('Message')}" # type: ignore
154
+ )
155
+
156
+ series = response.get("CompactData", {}).get("DataSet", {}).pop("Series", {}) # type: ignore
157
+
158
+ if not series:
159
+ raise OpenBBError(f"No time series data found -> {url} -> {response}")
160
+
161
+ # If there is only one series, they ruturn a dict instead of a list.
162
+ if series and isinstance(series, dict):
163
+ series = [series]
164
+
165
+ return series
166
+
167
+ @staticmethod
168
+ def transform_data(
169
+ query: ImfDirectionOfTradeQueryParams,
170
+ data: list[dict],
171
+ **kwargs: Any,
172
+ ) -> list[ImfDirectionOfTradeData]:
173
+ """Transform the data."""
174
+ # pylint: disable=import-outside-toplevel
175
+ from openbb_imf.utils.constants import UNIT_MULTIPLIERS_MAP # noqa
176
+ from pandas import Categorical, DataFrame, to_datetime
177
+ from pandas.tseries import offsets
178
+
179
+ if not data:
180
+ raise EmptyDataError()
181
+
182
+ dot_code_to_country = load_country_map()
183
+ series = data
184
+ results: list = []
185
+
186
+ for s in series:
187
+ if "Obs" not in s:
188
+ continue
189
+ meta = {
190
+ k.replace("@", "").lower(): (
191
+ UNIT_MULTIPLIERS_MAP.get(str(v), v) if k == "@UNIT_MULT" else v
192
+ )
193
+ for k, v in s.items()
194
+ if k != "Obs"
195
+ }
196
+ _symbol = meta.get("indicator", "")
197
+ _title = None
198
+
199
+ _data = s.pop("Obs", [])
200
+
201
+ if isinstance(_data, dict):
202
+ _data = [_data]
203
+
204
+ for d in _data:
205
+ _date = d.pop("@TIME_PERIOD", None)
206
+ val: Union[float, None] = d.pop("@OBS_VALUE", None)
207
+ _ = d.pop("@OBS_STATUS", None)
208
+ val = float(val) if val else None
209
+ if not val:
210
+ continue
211
+
212
+ if _date:
213
+ offset = (
214
+ offsets.QuarterEnd
215
+ if "Q" in _date
216
+ else (
217
+ offsets.YearEnd
218
+ if len(str(_date)) == 4
219
+ else offsets.MonthEnd
220
+ )
221
+ )
222
+ _date = to_datetime(_date)
223
+ _date = _date + offset(0)
224
+ _date = _date.strftime("%Y-%m-%d")
225
+ vals = {
226
+ k: v
227
+ for k, v in {
228
+ "date": _date,
229
+ "symbol": _symbol,
230
+ "country": dot_code_to_country.get(
231
+ meta.get("ref_area"), meta.get("ref_area")
232
+ ),
233
+ "counterpart": dot_code_to_country.get(
234
+ meta.get("counterpart_area"), meta.get("counterpart_area")
235
+ ),
236
+ "title": dot_titles_map.get(_symbol),
237
+ "scale": meta.get("unit_mult"),
238
+ "value": val,
239
+ }.items()
240
+ if v
241
+ }
242
+
243
+ if (
244
+ vals.get("value")
245
+ and vals.get("date")
246
+ and vals.get("country") != vals.get("counterpart")
247
+ ):
248
+ d.update(vals)
249
+
250
+ if _data:
251
+ results.extend([d for d in _data if d])
252
+
253
+ df = DataFrame(results)
254
+ df["symbol"] = Categorical(
255
+ df["symbol"],
256
+ categories=list(dot_titles_map),
257
+ ordered=True,
258
+ )
259
+ df["country"] = Categorical(
260
+ df["country"],
261
+ categories=sorted(df.country.unique().tolist()),
262
+ ordered=True,
263
+ )
264
+ df["counterpart"] = Categorical(
265
+ df["counterpart"],
266
+ categories=sorted(df.counterpart.unique().tolist()),
267
+ ordered=True,
268
+ )
269
+ df = df.sort_values(by=["date", "country", "counterpart"])
270
+
271
+ return [
272
+ ImfDirectionOfTradeData.model_validate(r)
273
+ for r in df.to_dict(orient="records")
274
+ ]
openbb_platform/providers/imf/openbb_imf/models/economic_indicators.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IMF Economic Indicators Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from datetime import datetime
6
+ from typing import Any, Dict, List, Literal, Optional, Union
7
+
8
+ from openbb_core.app.model.abstract.error import OpenBBError
9
+ from openbb_core.provider.abstract.fetcher import Fetcher
10
+ from openbb_core.provider.standard_models.economic_indicators import (
11
+ EconomicIndicatorsData,
12
+ EconomicIndicatorsQueryParams,
13
+ )
14
+ from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
15
+ from openbb_core.provider.utils.errors import EmptyDataError
16
+ from openbb_imf.utils.constants import (
17
+ FSI_PRESETS,
18
+ IRFCL_PRESET,
19
+ IRFCL_TABLES,
20
+ load_symbols,
21
+ )
22
+ from openbb_imf.utils.irfcl_helpers import (
23
+ load_country_to_code_map,
24
+ validate_countries,
25
+ )
26
+ from pydantic import Field, field_validator
27
+
28
+
29
+ class ImfEconomicIndicatorsQueryParams(EconomicIndicatorsQueryParams):
30
+ """IMF Economic Indicators Query."""
31
+
32
+ __json_schema_extra__ = {
33
+ "symbol": {
34
+ "multiple_items_allowed": True,
35
+ },
36
+ "country": {
37
+ "multiple_items_allowed": True,
38
+ "choices": ["all"] + list(list(load_country_to_code_map())),
39
+ },
40
+ "frequency": {
41
+ "choices": ["annual", "quarter", "month"],
42
+ },
43
+ }
44
+ symbol: str = Field(
45
+ default="irfcl_top_lines",
46
+ description=QUERY_DESCRIPTIONS.get("symbol", "")
47
+ + " Use `available_indicators()` to get the list of available symbols."
48
+ + " Use 'IRFCL' to get all the data from International Reserves & Foreign Currency Liquidity indicators."
49
+ + " Use 'core_fsi' to get the core Financial Soundness Indicators."
50
+ + " Use 'core_fsi_underlying' to include underlying data for the core Financial Soundness Indicators."
51
+ + " Complete tables are available only by single country, and are keyed as described below."
52
+ + " The default is 'irfcl_top_lines'. Available presets not listed in `available_indicators()` are:"
53
+ + """\n
54
+ 'IRFCL': All the data from the set of indicators. Not compatible with multiple countries.
55
+ 'irfcl_top_lines': The default, top line items from the IRFCL data. Compatible with multiple countries.
56
+ 'reserve_assets_and_other_fx_assets': Table I of the IRFCL data. Not compatible with multiple countries.
57
+ 'predetermined_drains_on_fx_assets': Table II of the IRFCL data. Not compatible with multiple countries.
58
+ 'contingent_drains_fx_assets': Table III of the IRFCL data. Not compatible with multiple countries.
59
+ 'memorandum_items': The memorandum items table of the IRFCL data. Not compatible with multiple countries.
60
+ 'gold_reserves': Gold reserves as value in USD and Fine Troy Ounces. Compatible with multiple countries.
61
+ 'derivative_assets': Net derivative assets as value in USD. Compatible with multipile countries.
62
+ 'fsi_core': The core Financial Soundness Indicators. Compatible with multiple countries.
63
+ 'fsi_core_underlying': The core FSIs underlying series data. Not compatible with country='all'.
64
+ 'fsi_encouraged_set': The encouraged set of Financial Soundness Indicators. Not compatible with country='all'.
65
+ 'fsi_other': The other Financial Soundness Indicators. Not compatible with country='all'.
66
+ 'fsi_balance_sheets': Data categorized as Balance Sheets and Income Statements. Not compatible with country='all'.
67
+ 'fsi_all': All the Financial Soundness Indicators. Not compatible with multiple countries.
68
+ """,
69
+ )
70
+ frequency: Literal["annual", "quarter", "month"] = Field(
71
+ default="quarter",
72
+ description="Frequency of the data, default is 'quarter'.",
73
+ )
74
+
75
+ @field_validator("symbol", mode="before", check_fields=False)
76
+ @classmethod
77
+ def _count_presets(cls, v):
78
+ """Validate the symbol."""
79
+ if not v:
80
+ return v
81
+ presets = list(IRFCL_PRESET) + FSI_PRESETS
82
+ n_preset = 0
83
+ symbols = v.split(",")
84
+ for symbol in symbols:
85
+ n_preset += 1 if symbol in presets else 0
86
+ if n_preset > 1:
87
+ raise ValueError("only one preset symbol can be used at a time.")
88
+ return v
89
+
90
+
91
+ class ImfEconomicIndicatorsData(EconomicIndicatorsData):
92
+ """IMF Economic Indicators Data."""
93
+
94
+ __alias_dict__ = {
95
+ "symbol_root": "parent",
96
+ }
97
+
98
+ unit: Optional[str] = Field(
99
+ default=None,
100
+ description="The unit of the value.",
101
+ )
102
+ scale: Optional[str] = Field(
103
+ default=None,
104
+ description="The scale of the value.",
105
+ )
106
+ table: Optional[str] = Field(
107
+ default=None,
108
+ description="The name of the table associated with the symbol.",
109
+ )
110
+ level: Optional[int] = Field(
111
+ default=None,
112
+ description="The indentation level of the data, relative to the table and symbol_root",
113
+ )
114
+ order: Optional[Union[int, float]] = Field(
115
+ default=None,
116
+ description="Order of the data, relative to the table.",
117
+ )
118
+ reference_sector: Optional[str] = Field(
119
+ default=None,
120
+ description="The reference sector for the data.",
121
+ )
122
+ title: Optional[str] = Field(
123
+ default=None,
124
+ description="The title of the series associated with the symbol.",
125
+ )
126
+
127
+
128
+ class ImfEconomicIndicatorsFetcher(
129
+ Fetcher[ImfEconomicIndicatorsQueryParams, List[ImfEconomicIndicatorsData]]
130
+ ):
131
+ """IMF Economic Indicators Fetcher."""
132
+
133
+ @staticmethod
134
+ def transform_query(params: Dict[str, Any]) -> ImfEconomicIndicatorsQueryParams:
135
+ """Transform the query."""
136
+ symbols = params.get("symbol", "")
137
+ countries = params.get("country")
138
+ now = datetime.now().date()
139
+ symbols = (
140
+ "IRFCL"
141
+ if (("all" in symbols or "IRFCL" in symbols) and "fsi_all" not in symbols)
142
+ else symbols if symbols else "irfcl_top_lines"
143
+ )
144
+ incompatible = (
145
+ "fsi_other" in symbols
146
+ or "fsi_encouraged_set" in symbols
147
+ or "fsi_all" in symbols
148
+ or "fsi_core_underlying" in symbols
149
+ or "fsi_balance_sheets" in symbols
150
+ )
151
+ if (symbols == "IRFCL" or incompatible) and not (
152
+ countries or countries == "all"
153
+ ):
154
+ raise OpenBBError(
155
+ f"The selected symbol(s), {params.get('symbol')}, is not compatible with the all-countries group."
156
+ " Please provide country names or two-letter ISO country codes."
157
+ )
158
+
159
+ if countries:
160
+ params["country"] = validate_countries(countries)
161
+
162
+ if symbols and symbols in IRFCL_PRESET:
163
+ params["symbol"] = IRFCL_PRESET[symbols]
164
+ if symbols in IRFCL_TABLES and countries and countries.split(",") > 1:
165
+ raise OpenBBError(
166
+ f"Symbol '{symbols}' is a table and can only be used with one country."
167
+ )
168
+ elif symbols:
169
+ params["symbol"] = symbols
170
+
171
+ if not params.get("start_date") and (not countries or countries == "all"):
172
+ params["start_date"] = now.replace(
173
+ year=now.year - 1, month=1, day=1
174
+ ).strftime("%Y-%m-%d")
175
+
176
+ if not params.get("end_date"):
177
+ params["end_date"] = now.replace(month=12, day=31).strftime("%Y-%m-%d")
178
+
179
+ if (not symbols or symbols == "all") and not params.get("start_date"):
180
+ params["start_date"] = now.replace(year=now.year - 1).strftime("%Y-%m-%d")
181
+
182
+ return ImfEconomicIndicatorsQueryParams(**params)
183
+
184
+ @staticmethod
185
+ async def aextract_data(
186
+ query: ImfEconomicIndicatorsQueryParams,
187
+ credentials: Optional[Dict[str, Any]] = None,
188
+ **kwargs: Any,
189
+ ) -> List[Dict]:
190
+ """Extract the data."""
191
+ # pylint: disable = import-outside-toplevel
192
+ from openbb_imf.utils.fsi_helpers import _get_fsi_data # noqa
193
+ from openbb_imf.utils.irfcl_helpers import _get_irfcl_data
194
+ from warnings import warn
195
+
196
+ fsi_symbols = load_symbols("FSI")
197
+ irfcl_symbols = load_symbols("IRFCL")
198
+ symbols = query.symbol.split(",")
199
+ new_symbols_irfcl: Union[list, str] = []
200
+ new_symbols_fsi: Union[list, str] = []
201
+ for symbol in symbols:
202
+ if symbol in list(IRFCL_PRESET) + ["all", "IRFCL"]:
203
+ new_symbols_irfcl = symbol
204
+ elif symbol in FSI_PRESETS:
205
+ new_symbols_fsi = symbol
206
+ elif symbol.upper() in fsi_symbols:
207
+ new_symbols_fsi.append(symbol.upper()) # type: ignore
208
+ elif symbol.upper() in irfcl_symbols:
209
+ new_symbols_irfcl.append(symbol.upper()) # type: ignore
210
+
211
+ if not new_symbols_irfcl and not new_symbols_fsi:
212
+ raise OpenBBError(
213
+ f"No valid symbols found -> {query.symbol} -> "
214
+ "Use 'available_indicators(provider='imf')' to get the list of available symbols."
215
+ )
216
+
217
+ results: list = []
218
+ exceptions: list = []
219
+ try:
220
+ try:
221
+ if new_symbols_irfcl:
222
+ _kwargs = query.model_dump(exclude_none=True)
223
+ _kwargs["symbol"] = new_symbols_irfcl
224
+ results.extend(await _get_irfcl_data(**_kwargs))
225
+ except (EmptyDataError, OpenBBError) as e:
226
+ if new_symbols_fsi:
227
+ exceptions.append(
228
+ f"IRFCL dataset error -> {e.__class__.__name__}: {e}"
229
+ )
230
+ else:
231
+ raise
232
+ if new_symbols_fsi:
233
+ try:
234
+ _kwargs = query.model_dump(exclude_none=True)
235
+ _kwargs["symbol"] = new_symbols_fsi
236
+ results.extend(await _get_fsi_data(**_kwargs))
237
+ except (EmptyDataError, OpenBBError) as e:
238
+ if new_symbols_irfcl and len(results) > 0:
239
+ exceptions.append(
240
+ f"FSI dataset error -> {e.__class__.__name__}: {e}"
241
+ )
242
+ elif not new_symbols_irfcl:
243
+ raise
244
+ except OpenBBError as exc:
245
+ raise exc from exc
246
+
247
+ if not results:
248
+ raise EmptyDataError("No results returned for the query.")
249
+
250
+ if results and exceptions:
251
+ msgs = "\n".join(exceptions)
252
+ warn("An error occurred while fetching the data -> " + msgs)
253
+
254
+ return results
255
+
256
+ @staticmethod
257
+ def transform_data(
258
+ query: ImfEconomicIndicatorsQueryParams,
259
+ data: List[Dict],
260
+ **kwargs: Any,
261
+ ) -> List[ImfEconomicIndicatorsData]:
262
+ """Transform the data."""
263
+ # pylint: disable = import-outside-toplevel
264
+ from numpy import nan
265
+ from pandas import Categorical, DataFrame
266
+
267
+ if not data:
268
+ raise EmptyDataError("The data is empty.")
269
+
270
+ all_symbols = {
271
+ **load_symbols("all"),
272
+ }
273
+ df = DataFrame(data)
274
+
275
+ if df.empty:
276
+ raise EmptyDataError("The data is empty.")
277
+
278
+ df = df[df["symbol"].isin(all_symbols)]
279
+
280
+ if len(df) == 0:
281
+ raise OpenBBError("The data has a length of 0.")
282
+
283
+ df["symbol"] = Categorical(
284
+ df["symbol"],
285
+ categories=all_symbols,
286
+ ordered=True,
287
+ )
288
+ df["parent"] = Categorical(
289
+ df["parent"],
290
+ categories=all_symbols,
291
+ ordered=True,
292
+ )
293
+ df = df.sort_values(
294
+ by=["date", "parent", "symbol", "value"],
295
+ ascending=[True, True, True, False],
296
+ ).reset_index(drop=True)
297
+
298
+ df.loc[:, "title"] = df.symbol.apply(
299
+ lambda x: all_symbols.get(x, {}).get("title")
300
+ )
301
+ records = df.replace({nan: None}).to_dict(orient="records")
302
+
303
+ return [ImfEconomicIndicatorsData.model_validate(r) for r in records]
openbb_platform/providers/imf/openbb_imf/utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """IMF Utilities."""
openbb_platform/providers/imf/openbb_imf/utils/constants.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IMF Constants."""
2
+
3
+ # pylint: disable=line-too-long
4
+ # flake8: noqa: E501
5
+
6
+ FSI_PRESETS = [
7
+ "fsi_core",
8
+ "fsi_core_underlying",
9
+ "fsi_other",
10
+ "fsi_encouraged_set",
11
+ "fsi_balance_sheets",
12
+ "fsi_all",
13
+ ]
14
+
15
+ IRFCL_HEADLINE = "RAF_USD,RAFA_USD,RAFAFX_USD,RAOFA_USD,RAPFA_USD,RAFAIMF_USD,RAFASDR_USD,RAFAGOLD_USD,RACFA_USD,RAMDCD_USD,RAMFIFC_USD,RAMSR_USD"
16
+
17
+ RESERVE_ASSETS_AND_OTHER_FX_ASSETS = "RAF_USD,RAFA_USD,RAFAFX_USD,RAFAFXS_USD,RAFAFXSI_USD,RAFAFXCD_USD,RAFAFXCDN_USD,RAFAFXCDBI_USD,RAFAFXCDBIA_USD,RAFAFXCDBO_USD,RAFAFXCDBOA_USD,RAFAIMF_USD,RAFASDR_USD,RAFAGOLD_USD,RAFAGOLDV_OZT,RAFAO_USD,RAFAOF_USD,RAFAOL_USD,RAFAOO_USD,RAOFA_USD,RAOFAS_USD,RAOFAD_USD,RAOFAL_USD,RAOFAF_USD,RAOFAG_USD"
18
+
19
+ PREDETERMINED_DRAINS_ON_FX_ASSETS = "RAPFA_USD,RAPFALSD_USD,RAPFALSD_1M_USD,RAPFALSD_1M_3M_USD,RAPFALSD_3M_1Y_USD,RAPFALSDOP_USD,RAPFALSDOP_1M_USD,RAPFALSDOP_1M_3M_USD,RAPFALSDOP_3M_1Y_USD,RAPFALSDOI_USD,RAPFALSDOI_1M_USD,RAPFALSDOI_1M_3M_USD,RAPFALSDOI_3M_1Y_USD,RAPFALSDIP_USD,RAPFALSDIP_1M_USD,RAPFALSDIP_1M_3M_USD,RAPFALSDIP_3M_1Y_USD,RAPFALSDII_USD,RAPFALSDII_1M_USD,RAPFALSDII_1M_3M_USD,RAPFALSDII_3M_1Y_USD,RAPFAFFS_USD,RAPFAFFS_1M_USD,RAPFAFFS_1M_3M_USD,RAPFAFFS_3M_1Y_USD,RAPFAFFL_USD,RAPFAFFL_1M_USD,RAPFAFFL_1M_3M_USD,RAPFAFFL_3M_1Y_USD,RAPFAO_USD,RAPFAO_1M_USD,RAPFAO_1M_3M_USD,RAPFAO_3M_1Y_USD,RAPFAOOR_USD,RAPFAOOR_1M_USD,RAPFAOOR_1M_3M_USD,RAPFAOOR_3M_1Y_USD,RAPFAOIRR_USD,RAPFAOIRR_1M_USD,RAPFAOIRR_1M_3M_USD,RAPFAOIRR_3M_1Y_USD,RAPFAOOC_USD,RAPFAOOC_1M_USD,RAPFAOOC_1M_3M_USD,RAPFAOOC_3M_1Y_USD,RAPFAOIC_USD,RAPFAOIC_1M_USD,RAPFAOIC_1M_3M_USD,RAPFAOIC_3M_1Y_USD,RAPFAOOP_USD,RAPFAOOP_1M_USD,RAPFAOOP_1M_3M_USD,RAPFAOOP_3M_1Y_USD,RAPFAOIR_USD,RAPFAOIR_1M_USD,RAPFAOIR_1M_3M_USD,RAPFAOIR_3M_1Y_USD,RAFA_RAPFA_RO"
20
+
21
+ CONTINGENT_DRAINS_FX_ASSETS = "RACFA_USD,RACFAL_USD,RACFAL_1M_USD,RACFAL_1M_3M_USD,RACFAL_3M_1Y_USD,RACFALG_USD,RACFALG_1M_USD,RACFALG_1M_3M_USD,RACFALO_USD,RACFALO_1M_USD,RACFALO_1M_3M_USD,RACFALO_3M_1Y_USD,RACFAS_USD,RACFACB_USD,RACFACB_1M_USD,RACFACB_1M_3M_USD,RACFACB_3M_1Y_USD,RACFACBA_USD,RACFACBA_1M_USD,RACFACBA_1M_3M_USD,RACFACBA_3M_1Y_USD,RACFACBAOI_USD,RACFACBAOI_1M_USD,RACFACBAOI_1M_3M_USD,RACFACBAON_USD,RACFACBAON_1M_USD,RACFACBAON_1M_3M_USD,RACFACBAON_3M_1Y_USD,RACFACBABIS_USD,RACFACBAIMF_1M_USD,RACFACBAIMF_1M_3M_USD,RACFACBAIMF_3M_1Y_USD,RACFACBAIMF_USD,RACFACBFIR_USD,RACFACBFIR_1M_USD,RACFACBFIR_1M_3M_USD,RACFACBFIR_3M_1Y_USD,RACFACBFIO_USD,RACFACBFIO_1M_USD,RACFACBFIO_1M_3M_USD,RACFACBFIO_3M_1Y_USD,RACFACT_USD,RACFACT_1M_USD,RACFACT_1M_3M_USD,RACFACT_3M_1Y_USD,RACFACTA_USD,RACFACTA_1M_USD,RACFACTA_1M_3M_USD,RACFACTA_3M_1Y_USD,RACFACTAOI_USD,RACFACTAOI_1M_USD,RACFACTAOI_1M_3M_USD,RACFACTAOI_3M_1Y_USD,RACFACTAON_USD,RACFACTAON_1M_USD,RACFACTAON_1M_3M_USD,RACFACTAON_3M_1Y_USD,RACFACTABIS_USD,RACFACTABIS_1M_USD,RACFACTABIS_1M_3M_USD,RACFACTABIS_3M_1Y_USD,RACFACTAIMF_USD,RACFACTAIMF_1M_USD,RACFACTFIR_USD,RACFACTFIR_1M_USD,RACFACTFIR_1M_3M_USD,RACFACTFIR_3M_1Y_USD,RACFACTFIO_USD,RACFACTFIO_1M_USD,RACFACTFIO_1M_3M_USD,RACFACTFIO_3M_1Y_USD,RACFAPPS_USD,RACFAPPS_1M_USD,RACFAPPS_1M_3M_USD,RACFAPPS_3M_1Y_USD,RACFAPPSBP_USD,RACFAPPSBP_1M_USD,RACFAPPSBP_1M_3M_USD,RACFAPPSBP_3M_1Y_USD,RACFAPPSWC_USD,RACFAPPSWC_1M_USD,RACFAPPSWC_1M_3M_USD,RACFAPPSWC_3M_1Y_USD,RACFAPPL_USD,RACFAPPL_1M_USD,RACFAPPL_1M_3M_USD,RACFAPPL_3M_1Y_USD,RACFAPPLBC_USD,RACFAPPLBC_1M_USD,RACFAPPLBC_1M_3M_USD,RACFAPPLBC_3M_1Y_USD,RACFAPPLWP_USD,RACFAPPLWP_1M_USD,RACFAPPLWP_1M_3M_USD,RACFAPPLWP_3M_1Y_USD,RACFAMPAS_USD,RACFAMPAS_1M_USD,RACFAMPAS_1M_3M_USD,RACFAMPAS_3M_1Y_USD,RACFAMPAL_USD,RACFAMPAL_1M_USD,RACFAMPAL_1M_3M_USD,RACFAMPAL_3M_1Y_USD,RACFAMPBS_USD,RACFAMPBS_1M_USD,RACFAMPBS_1M_3M_USD,RACFAMPBS_3M_1Y_USD,RACFAMPBL_USD,RACFAMPBL_1M_USD,RACFAMPBL_1M_3M_USD,RACFAMPBL_3M_1Y_USD,RACFAMPCS_USD,RACFAMPCS_1M_USD,RACFAMPCS_1M_3M_USD,RACFAMPCS_3M_1Y_USD,RACFAMPCL_USD,RACFAMPCL_1M_USD,RACFAMPCL_1M_3M_USD,RACFAMPCL_3M_1Y_USD,RACFAMPDS_USD,RACFAMPDS_1M_USD,RACFAMPDS_1M_3M_USD,RACFAMPDS_3M_1Y_USD,RACFAMPDL_USD,RACFAMPDL_1M_USD,RACFAMPDL_1M_3M_USD,RACFAMPDL_3M_1Y_USD,RACFAMPES_USD,RACFAMPES_1M_USD,RACFAMPES_1M_3M_USD,RACFAMPES_3M_1Y_USD,RACFAMPEL_USD,RACFAMPEL_1M_USD,RACFAMPEL_1M_3M_USD,RACFAMPEL_3M_1Y_USD,RACFAMPFS_USD,RACFAMPFS_1M_USD,RACFAMPFS_1M_3M_USD,RACFAMPFS_3M_1Y_USD,RACFAMPFL_USD,RACFAMPFL_1M_USD,RACFAMPFL_1M_3M_USD,RACFAMPFL_3M_1Y_USD"
22
+
23
+ IRFCL_MEMORANDUM_ITEMS = "RAMDCD_USD,RAMFIFC_USD,RAMPA_USD,RAMFFS_USD,RAMPAOA_USD,RAMSR_USD,RAMSRLRI_USD,RAMSRLRN_USD,RAMSRBRI_USD,RAMSRBAN_USD,RAMFDA_USD,RAMFDAF_USD,RAMFDAU_USD,RAMFDAW_USD,RAMFDAP_USD,RAMFDAO_USD,RAMFFL_USD,RAMPPS_USD,RAMPPSBP_USD,RAMPPSWC_USD,RAMPPL_USD,RAMPPLBP_USD,RAMPPLWC_USD,RAMCR_USD,RAMCRISDR_USD,RAMCRIC_USD_USD,RAMCRIC_EUR_USD,RAMCRIC_CNY_USD,RAMCRIC_JPY_USD,RAMCRIC_GBP_USD,RAMCROSDR_USD"
24
+
25
+ IRFCL_TABLES = {
26
+ "reserve_assets_and_other_fx_assets": RESERVE_ASSETS_AND_OTHER_FX_ASSETS,
27
+ "predetermined_drains_on_fx_assets": PREDETERMINED_DRAINS_ON_FX_ASSETS,
28
+ "contingent_drains_fx_assets": CONTINGENT_DRAINS_FX_ASSETS,
29
+ "memorandum_items": IRFCL_MEMORANDUM_ITEMS,
30
+ }
31
+
32
+ IRFCL_PRESET = {
33
+ "irfcl_top_lines": IRFCL_HEADLINE,
34
+ **IRFCL_TABLES,
35
+ "gold_reserves": "RAFAGOLD_USD,RAFAGOLDV_OZT",
36
+ "derivative_assets": "RAMFDA_USD",
37
+ }
38
+
39
+ FREQUENCY_DICT = {
40
+ "month": "M",
41
+ "quarter": "Q",
42
+ "annual": "A",
43
+ }
44
+ REF_SECTORS_DICT = {
45
+ "government_ex_social_security": "S1311",
46
+ "central_bank": "S121",
47
+ "monetary_authorities": "S1X",
48
+ "all_sectors": "",
49
+ }
50
+
51
+ SECTOR_MAP = {v: k for k, v in REF_SECTORS_DICT.items()}
52
+
53
+ UNIT_MULTIPLIERS_MAP = {
54
+ "0": "Units",
55
+ "2": "Hundreds",
56
+ "3": "Thousands",
57
+ "6": "Millions",
58
+ "9": "Billions",
59
+ "12": "Trillions",
60
+ "N15": "Quadrillionths",
61
+ "N14": "Hundred Trillionths",
62
+ "N13": "Ten Trillionths",
63
+ "N12": "Trillionths",
64
+ "N11": "Hundred Billionths",
65
+ "N10": "Ten Billionths",
66
+ "N9": "Billionths",
67
+ "N8": "Hundred Millionths",
68
+ "N7": "Ten Millionths",
69
+ "N6": "Millionths",
70
+ "N5": "Hundred Thousandths",
71
+ "N4": "Ten Thousandths",
72
+ "N3": "Thousandths",
73
+ "N2": "Hundredths",
74
+ "N1": "Tenths",
75
+ "1": "Tens",
76
+ "4": "Ten Thousands",
77
+ "5": "Hundred Thousands",
78
+ "7": "Ten Millions",
79
+ "8": "Hundred Millions",
80
+ "10": "Ten Billions",
81
+ "11": "Hundred Billions",
82
+ "13": "Ten Trillions",
83
+ "14": "Hundred Trillions",
84
+ "15": "Quadrillions",
85
+ }
86
+
87
+ TIME_FORMAT_MAP = {
88
+ "P1Y": "Annual",
89
+ "P6M": "Bi-annual",
90
+ "P3M": "Quarterly",
91
+ "P1M": "Monthly",
92
+ "P7D": "Weekly",
93
+ "P1D": "Daily",
94
+ }
95
+
96
+ REF_SECTOR_MAP = {
97
+ "S1311": "Central government excluding social security",
98
+ "S121": "Central Bank",
99
+ "S1X": "Monetary Authorities",
100
+ "1C_AS": "All Sectors",
101
+ "AllSectorsIncludingAllSectors": "All Sectors Including All Sectors",
102
+ }
103
+
104
+
105
+ def load_symbols(dataset: str) -> dict:
106
+ """Load IMF symbol list."""
107
+ # pylint: disable=import-outside-toplevel
108
+ import json # noqa
109
+ from json.decoder import JSONDecodeError
110
+ from pathlib import Path
111
+ from openbb_core.app.model.abstract.error import OpenBBError
112
+
113
+ try:
114
+ symbols_file = Path(__file__).parents[1].joinpath("assets", "imf_symbols.json")
115
+ with symbols_file.open(encoding="utf-8") as file:
116
+ symbols = json.load(file)
117
+ except (FileNotFoundError, JSONDecodeError) as e:
118
+ raise OpenBBError(
119
+ f"Failed to load IMF symbols from the static file: {e}"
120
+ ) from e
121
+
122
+ if dataset == "all":
123
+ return symbols
124
+
125
+ return {k: v for k, v in symbols.items() if v["dataset"] == dataset}
openbb_platform/providers/imf/openbb_imf/utils/dot_helpers.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Direction Of Trade Utilities."""
2
+
3
+ from openbb_core.app.model.abstract.error import OpenBBError
4
+
5
+
6
+ def load_country_map() -> dict:
7
+ """Load IMF IRFCL country map."""
8
+ # pylint: disable=import-outside-toplevel
9
+ import json # noqa
10
+ from json.decoder import JSONDecodeError
11
+ from pathlib import Path
12
+
13
+ try:
14
+ country_map_file = (
15
+ Path(__file__).parents[1].joinpath("assets", "imf_country_map.json")
16
+ )
17
+ with country_map_file.open(encoding="utf-8") as file:
18
+ country_map_dict = json.load(file)
19
+ except (FileNotFoundError, JSONDecodeError) as e:
20
+ raise OpenBBError(f"Failed to load IMF DOT country map: {e}") from e
21
+
22
+ return country_map_dict
23
+
24
+
25
+ def load_country_to_code_map() -> dict:
26
+ """Load a map of lowercase country name to 2-letter ISO symbol."""
27
+ # pylint: disable=import-outside-toplevel
28
+ from warnings import warn # noqa
29
+
30
+ try:
31
+ return {
32
+ (
33
+ "euro_area"
34
+ if k == "U2"
35
+ else v.lower()
36
+ .replace(".", "")
37
+ .replace(",", "")
38
+ .replace(":", "")
39
+ .split("(")[0]
40
+ .strip()
41
+ .replace(" ", "_")
42
+ ): k
43
+ for k, v in load_country_map().items()
44
+ if not k.startswith("1C_ALL")
45
+ and "Report" not in v
46
+ and k not in ("GW", "_X")
47
+ }
48
+ except OpenBBError:
49
+ warn(f"Failed to load country to code map. -> {OpenBBError}")
50
+ return {}
51
+
52
+
53
+ def validate_countries(countries: list[str]) -> str:
54
+ """Validate the list of countries."""
55
+ # pylint: disable=import-outside-toplevel
56
+ from warnings import warn
57
+
58
+ try:
59
+ country_to_code_map = load_country_to_code_map()
60
+ except OpenBBError as e:
61
+ raise OpenBBError(f"Failed to load country to code map: {e}") from e
62
+ if not country_to_code_map:
63
+ raise OpenBBError("Failed to load country to code map.")
64
+
65
+ new_countries: list = []
66
+
67
+ for country in countries:
68
+ if country == "all":
69
+ return country
70
+ if country in country_to_code_map:
71
+ new_countries.append(country_to_code_map.get(country))
72
+ elif country.upper() in country_to_code_map.values():
73
+ new_countries.append(country.upper())
74
+ elif country.lower() == "eu":
75
+ new_countries.append(country_to_code_map.get("european_union"))
76
+ elif country.lower() == "ea":
77
+ new_countries.append(country_to_code_map.get("euro_area"))
78
+ else:
79
+ warn(f"Invalid country {country}")
80
+ continue
81
+
82
+ if not new_countries:
83
+ raise OpenBBError(
84
+ f"No valid countries found. Please choose from {list(country_to_code_map)}"
85
+ )
86
+
87
+ return "+".join(new_countries)
openbb_platform/providers/imf/openbb_imf/utils/fsi_helpers.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IMF FSI Data Set Helpers."""
2
+
3
+ from typing import Optional
4
+
5
+
6
+ def load_fsi_symbols() -> dict:
7
+ """Load IMF FSI symbols."""
8
+ # pylint: disable=import-outside-toplevel
9
+ from openbb_imf.utils.constants import load_symbols
10
+
11
+ return load_symbols("FSI")
12
+
13
+
14
+ def validate_symbols(symbols) -> str:
15
+ """Validate the IMF FSI symbols."""
16
+ # pylint: disable=import-outside-toplevel
17
+ from warnings import warn # noqa
18
+ from openbb_core.app.model.abstract.error import OpenBBError
19
+ from openbb_imf.utils.constants import FSI_PRESETS
20
+
21
+ fsi_symbols = load_fsi_symbols()
22
+
23
+ if isinstance(symbols, str):
24
+ symbols = symbols.split(",")
25
+ elif not isinstance(symbols, list):
26
+ raise OpenBBError("Invalid symbols list.")
27
+
28
+ new_symbols: list = []
29
+
30
+ for symbol in symbols:
31
+ if symbol in FSI_PRESETS:
32
+ return symbol
33
+ if symbol.upper() not in fsi_symbols:
34
+ warn(f"Unsupported IMF FSI symbol: {symbol}")
35
+ new_symbols.append(symbol.upper())
36
+
37
+ return "+".join(new_symbols) if len(new_symbols) > 1 else new_symbols[0]
38
+
39
+
40
+ # pylint: disable=too-many-branches,too-many-statements,too-many-locals
41
+ async def _get_fsi_data(**kwargs) -> list[dict]: # noqa:PLR0912
42
+ """Get IMF FSI data.
43
+ This function is not intended to be called directly,
44
+ but through the `ImfEconomicIndicatorsFetcher` class.
45
+ """
46
+ # pylint: disable=import-outside-toplevel
47
+ from datetime import date # noqa
48
+ from openbb_core.app.model.abstract.error import OpenBBError
49
+ from openbb_imf.utils.helpers import get_data
50
+ from openbb_imf.utils.irfcl_helpers import load_country_to_code_map
51
+ from openbb_imf.utils import constants
52
+ from pandas import to_datetime
53
+ from pandas.tseries import offsets
54
+
55
+ countries = kwargs.get("country", "")
56
+ countries = (
57
+ "" if countries == "all" else countries.replace(",", "+") if countries else ""
58
+ )
59
+ frequency = constants.FREQUENCY_DICT.get(kwargs.get("frequency", "quarter"), "Q")
60
+ start_date = kwargs.get("start_date", "")
61
+ end_date = kwargs.get("end_date", "")
62
+ all_symbols = load_fsi_symbols()
63
+ core_only = [k for k, v in all_symbols.items() if v.get("table") == "fsi_core"]
64
+ encouraged_set = [
65
+ k for k, v in all_symbols.items() if v.get("table") == "fsi_encouraged_set"
66
+ ]
67
+ indicator = kwargs.get("symbol")
68
+ indicators = (
69
+ "+".join(core_only if indicator == "fsi_core" else encouraged_set)
70
+ if indicator in ["fsi_core", "fsi_encouraged_set"]
71
+ else (
72
+ ""
73
+ if indicator
74
+ in ["fsi_other", "fsi_all", "fsi_core_underlying", "fsi_balance_sheets"]
75
+ else validate_symbols(indicator)
76
+ )
77
+ )
78
+
79
+ if not indicators and not countries:
80
+ raise OpenBBError(
81
+ "All countries not supported for this group of indicators. Please supply a country."
82
+ )
83
+
84
+ if not start_date and not end_date and not countries:
85
+ start_date = (
86
+ date.today().replace(year=date.today().year - 1).strftime("%Y-%m-%d")
87
+ )
88
+
89
+ if start_date and not end_date:
90
+ end_date = date.today().strftime("%Y-%m-%d")
91
+
92
+ # Adjust the dates to the date relative to frequency.
93
+ # The API does not accept arbitrary dates, so we need to adjust them.
94
+ if start_date:
95
+ start_date = to_datetime(start_date)
96
+ if frequency == "Q":
97
+ start_date = offsets.QuarterBegin(startingMonth=1).rollback(start_date)
98
+ elif frequency == "A":
99
+ start_date = offsets.YearBegin().rollback(start_date)
100
+ else:
101
+ start_date = offsets.MonthBegin().rollback(start_date)
102
+ start_date = start_date.strftime("%Y-%m-%d")
103
+
104
+ if end_date:
105
+ end_date = to_datetime(end_date)
106
+ if frequency == "Q":
107
+ end_date = offsets.QuarterEnd().rollforward(end_date)
108
+ elif frequency == "A":
109
+ end_date = offsets.YearEnd().rollforward(end_date)
110
+ else:
111
+ end_date = offsets.MonthEnd().rollforward(end_date)
112
+ end_date = end_date.strftime("%Y-%m-%d")
113
+
114
+ date_range = (
115
+ f"?startPeriod={start_date}&endPeriod={end_date}"
116
+ if start_date and end_date
117
+ else ""
118
+ )
119
+ base_url = "http://dataservices.imf.org/REST/SDMX_JSON.svc/"
120
+ key = f"CompactData/FSI/{frequency}.{countries}.{indicators}"
121
+ url = f"{base_url}{key}{date_range}"
122
+ series = await get_data(url)
123
+ data: list = []
124
+ all_symbols = load_fsi_symbols()
125
+
126
+ if indicator in ["fsi_core", "fsi_encouraged_set"]:
127
+ all_symbols = {
128
+ k: v for k, v in all_symbols.items() if v.get("table") == indicator
129
+ }
130
+ elif indicator == "fsi_core_underlying":
131
+ all_symbols = {
132
+ k: v
133
+ for k, v in all_symbols.items()
134
+ if "core set" in v.get("title", "").lower() and v.get("unit") != "Percent"
135
+ }
136
+ elif indicator == "fsi_balance_sheets":
137
+ all_symbols = {
138
+ k: v
139
+ for k, v in all_symbols.items()
140
+ if "balance sheets" in v.get("title", "").lower()
141
+ }
142
+ elif indicator == "fsi_other":
143
+ all_symbols = {
144
+ k: v
145
+ for k, v in all_symbols.items()
146
+ if "Additional FSIs" in v.get("title", "")
147
+ }
148
+
149
+ country_map_dict = {
150
+ v: k.replace("_", " ").title().replace("Ecb", "ECB")
151
+ for k, v in load_country_to_code_map().items()
152
+ }
153
+ # Iterate over the series to extract observations and map the metadata.
154
+ for s in series:
155
+ if "Obs" not in s:
156
+ continue
157
+ meta = {
158
+ k.replace("@", "").lower(): (
159
+ constants.UNIT_MULTIPLIERS_MAP.get(str(v), v)
160
+ if k == "@UNIT_MULT"
161
+ else v
162
+ )
163
+ for k, v in s.items()
164
+ if k != "Obs"
165
+ }
166
+ _symbol = meta.get("indicator")
167
+ _parent: Optional[str] = None
168
+ _order: Optional[str] = None
169
+ _level: Optional[str] = None
170
+ _table: Optional[str] = None
171
+ _title: Optional[str] = None
172
+ _unit: Optional[str] = None
173
+
174
+ if _symbol not in all_symbols:
175
+ continue
176
+
177
+ _table = all_symbols.get(_symbol, {}).get("table", "")
178
+ if _table and indicator == "fsi_core_underlying":
179
+ _table = _table.replace("fsi_other", "fsi_core_underlying") # type: ignore
180
+ _parent = all_symbols.get(_symbol, {}).get("parent", "")
181
+ _order = all_symbols.get(_symbol, {}).get("order", "")
182
+ _level = all_symbols.get(_symbol, {}).get("level", "")
183
+ _title = all_symbols.get(_symbol, {}).get("title", "")
184
+ if _title:
185
+ _title = " - ".join(_title.split(", ")[1:-1]).replace(
186
+ "Financial Soundness Indicators - ", ""
187
+ )
188
+ _unit = all_symbols.get(_symbol, {}).get("unit", "")
189
+
190
+ _data = s.pop("Obs", [])
191
+
192
+ if isinstance(_data, dict):
193
+ _data = [_data]
194
+
195
+ for d in _data:
196
+ _date = d.pop("@TIME_PERIOD", None)
197
+ val = d.pop("@OBS_VALUE", None)
198
+ _ = d.pop("@OBS_STATUS", None)
199
+ if not val:
200
+ continue
201
+ val = float(val)
202
+ if _unit and _unit.lower() == "percent":
203
+ val = val / 100
204
+ if _date:
205
+ offset = (
206
+ offsets.QuarterEnd
207
+ if "Q" in _date
208
+ else offsets.YearEnd if len(str(_date)) == 4 else offsets.MonthEnd
209
+ )
210
+ _date = to_datetime(_date)
211
+ _date = _date + offset(0)
212
+ _date = _date.strftime("%Y-%m-%d")
213
+ vals = {
214
+ k: v
215
+ for k, v in {
216
+ "date": _date,
217
+ "table": _table,
218
+ "symbol": _symbol,
219
+ "parent": _parent if _parent else _symbol,
220
+ "level": _level,
221
+ "order": _order,
222
+ "country": country_map_dict.get(
223
+ meta.get("ref_area"), meta.get("ref_area")
224
+ ),
225
+ "reference_sector": None,
226
+ "title": _title,
227
+ "value": val,
228
+ "unit": (
229
+ _unit.upper() if _unit and _unit in ["usd", "eur"] else _unit
230
+ ),
231
+ "scale": (
232
+ "Basis Points"
233
+ if _unit and _unit.lower() == "percent"
234
+ else meta.get("unit_mult")
235
+ ),
236
+ }.items()
237
+ if v
238
+ }
239
+
240
+ if vals.get("value") and vals.get("date"):
241
+ d.update(vals)
242
+ data.append(d)
243
+
244
+ if not data:
245
+ raise OpenBBError(f"No data found for, '{indicator}', in, '{countries}'.")
246
+
247
+ return data
openbb_platform/providers/imf/openbb_imf/utils/helpers.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IMF Helper Utilities."""
2
+
3
+
4
+ async def get_data(url: str) -> list[dict]:
5
+ """Get data from the IMF API."""
6
+ # pylint: disable=import-outside-toplevel
7
+
8
+ from aiohttp.client_exceptions import ContentTypeError # noqa
9
+ from json.decoder import JSONDecodeError
10
+ from openbb_core.provider.utils.helpers import amake_request
11
+ from openbb_core.app.model.abstract.error import OpenBBError
12
+
13
+ try:
14
+ response = await amake_request(url, timeout=20)
15
+ except (JSONDecodeError, ContentTypeError) as e:
16
+ raise OpenBBError(
17
+ "Error fetching data; This might be rate-limiting. Try again later."
18
+ ) from e
19
+
20
+ if "ErrorDetails" in response:
21
+ raise OpenBBError(
22
+ f"{response['ErrorDetails'].get('Code')} -> {response['ErrorDetails'].get('Message')}" # type: ignore
23
+ )
24
+
25
+ series = response.get("CompactData", {}).get("DataSet", {}).pop("Series", {}) # type: ignore
26
+
27
+ if not series:
28
+ raise OpenBBError(f"No time series data found -> {response}")
29
+
30
+ # If there is only one series, they ruturn a dict instead of a list.
31
+ if series and isinstance(series, dict):
32
+ series = [series]
33
+
34
+ return series
openbb_platform/providers/imf/openbb_imf/utils/irfcl_helpers.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IMF IRFCL Data Set Helpers."""
2
+
3
+ from typing import Optional
4
+
5
+
6
+ def load_irfcl_symbols() -> dict:
7
+ """Load IMF IRFCL symbols."""
8
+ # pylint: disable=import-outside-toplevel
9
+ from openbb_imf.utils.constants import load_symbols
10
+
11
+ return load_symbols("IRFCL")
12
+
13
+
14
+ def load_country_map() -> dict:
15
+ """Load IMF IRFCL country map."""
16
+ # pylint: disable=import-outside-toplevel
17
+ import json # noqa
18
+ from json.decoder import JSONDecodeError
19
+ from pathlib import Path
20
+ from openbb_core.app.model.abstract.error import OpenBBError
21
+
22
+ try:
23
+ country_map_file = (
24
+ Path(__file__).parents[1].joinpath("assets", "imf_country_map.json")
25
+ )
26
+ with country_map_file.open(encoding="utf-8") as file:
27
+ country_map_dict = json.load(file)
28
+ except (FileNotFoundError, JSONDecodeError) as e:
29
+ raise OpenBBError(f"Failed to load IMF IRFCL country map: {e}") from e
30
+
31
+ return {
32
+ k: v.split(",")[0].split("_(")[0]
33
+ for k, v in country_map_dict.items()
34
+ if len(k) == 2
35
+ and k[0] not in ("5", "1", "7")
36
+ and k not in ("X0", "R1", "GW", "F1", "F6")
37
+ }
38
+
39
+
40
+ def load_country_to_code_map() -> dict:
41
+ """Load a map of lowercase country name to 2-letter ISO symbol."""
42
+ return {
43
+ (
44
+ "euro_area"
45
+ if k == "U2"
46
+ else v.lower()
47
+ .replace(" ", "_")
48
+ .replace("`", "")
49
+ .split(",")[0]
50
+ .split("_(")[0]
51
+ ): k
52
+ for k, v in load_country_map().items()
53
+ }
54
+
55
+
56
+ def validate_countries(countries) -> str:
57
+ """Validate the country and convert to a 2-letter ISO country code."""
58
+ # pylint: disable=import-outside-toplevel
59
+ from warnings import warn # noqa
60
+ from openbb_core.app.model.abstract.error import OpenBBError
61
+
62
+ country_map_dict = load_country_to_code_map()
63
+
64
+ if isinstance(countries, str):
65
+ countries = countries.split(",")
66
+ elif not isinstance(countries, list):
67
+ raise OpenBBError("Invalid countries list.")
68
+
69
+ new_countries: list = []
70
+
71
+ if "all" in countries or "ALL" in countries:
72
+ return "all"
73
+
74
+ for country in countries:
75
+ if country.lower() not in country_map_dict and country.upper() not in list(
76
+ country_map_dict.values()
77
+ ):
78
+ warn(f"Invalid IMF IRFCL country: {country}")
79
+ continue
80
+
81
+ if country.upper() in list(country_map_dict.values()):
82
+ new_countries.append(country.upper())
83
+ else:
84
+ new_countries.append(country_map_dict.get(country, country).upper())
85
+
86
+ new_countries = [c for c in new_countries if c]
87
+
88
+ if not new_countries:
89
+ raise OpenBBError("No valid countries found in the supplied list.")
90
+
91
+ return ",".join(new_countries)
92
+
93
+
94
+ def validate_symbols(symbols) -> str:
95
+ """Validate the IMF IRFCL symbols."""
96
+ # pylint: disable=import-outside-toplevel
97
+ from warnings import warn # noqa
98
+ from openbb_core.app.model.abstract.error import OpenBBError
99
+ from openbb_imf.utils.constants import IRFCL_PRESET
100
+
101
+ irfcl_symbols = load_irfcl_symbols()
102
+
103
+ if isinstance(symbols, str):
104
+ symbols = symbols.split(",")
105
+ elif not isinstance(symbols, list):
106
+ raise OpenBBError("Invalid symbols list.")
107
+
108
+ if "IRFCL" in symbols or "all" in symbols:
109
+ return "all"
110
+
111
+ new_symbols: list = []
112
+
113
+ for symbol in symbols:
114
+ if symbol in IRFCL_PRESET:
115
+ return IRFCL_PRESET[symbol].replace(",", "+")
116
+ if symbol.upper() not in irfcl_symbols:
117
+ warn(f"Invalid IMF IRFCL symbol: {symbol}")
118
+ new_symbols.append(symbol.upper())
119
+
120
+ return "+".join(new_symbols) if len(new_symbols) > 1 else new_symbols[0]
121
+
122
+
123
+ # We use this as a helper to allow future expansion of the supported IMF indicators.
124
+ # Each database has its own nuances with URL construction and schemas.
125
+
126
+
127
+ # pylint: disable=too-many-branches,too-many-statements,too-many-locals
128
+ async def _get_irfcl_data(**kwargs) -> list[dict]:
129
+ """Get IMF IRFCL data.
130
+ This function is not intended to be called directly,
131
+ but through the `ImfEconomicIndicatorsFetcher` class.
132
+ """
133
+ # pylint: disable=import-outside-toplevel
134
+ from openbb_core.app.model.abstract.error import OpenBBError
135
+ from openbb_imf.utils import constants
136
+ from openbb_imf.utils.helpers import get_data
137
+ from pandas import to_datetime
138
+ from pandas.tseries import offsets
139
+
140
+ countries = kwargs.get("country", "")
141
+ countries = (
142
+ "" if countries == "all" else countries.replace(",", "+") if countries else ""
143
+ )
144
+
145
+ frequency = constants.FREQUENCY_DICT.get(kwargs.get("frequency", "quarter"), "Q")
146
+ sector = kwargs.get("sector", "monetary_authorities")
147
+ sector = constants.REF_SECTORS_DICT.get(sector, "")
148
+ start_date = kwargs.get("start_date", "")
149
+ end_date = kwargs.get("end_date", "")
150
+
151
+ # Adjust the dates to the date relative to frequency.
152
+ # The API does not accept arbitrary dates, so we need to adjust them.
153
+ if start_date:
154
+ start_date = to_datetime(start_date)
155
+ if frequency == "Q":
156
+ start_date = offsets.QuarterBegin(startingMonth=1).rollback(start_date)
157
+ elif frequency == "A":
158
+ start_date = offsets.YearBegin().rollback(start_date)
159
+ else:
160
+ start_date = offsets.MonthBegin().rollback(start_date)
161
+ start_date = start_date.strftime("%Y-%m-%d")
162
+
163
+ if end_date:
164
+ end_date = to_datetime(end_date)
165
+ if frequency == "Q":
166
+ end_date = offsets.QuarterEnd().rollforward(end_date)
167
+ elif frequency == "A":
168
+ end_date = offsets.YearEnd().rollforward(end_date)
169
+ else:
170
+ end_date = offsets.MonthEnd().rollforward(end_date)
171
+ end_date = end_date.strftime("%Y-%m-%d")
172
+
173
+ indicator = kwargs.get("symbol")
174
+ indicators = validate_symbols(indicator) if indicator else ""
175
+ indicators = "" if indicators == "all" else indicators
176
+
177
+ if not indicators and not countries:
178
+ raise OpenBBError("Country is required when returning the complete dataset.")
179
+
180
+ date_range = (
181
+ f"?startPeriod={start_date}&endPeriod={end_date}"
182
+ if start_date and end_date
183
+ else ""
184
+ )
185
+ base_url = "http://dataservices.imf.org/REST/SDMX_JSON.svc/"
186
+ key = f"CompactData/IRFCL/{frequency}.{countries}.{indicators}.{sector}"
187
+ url = f"{base_url}{key}{date_range}"
188
+
189
+ series = await get_data(url)
190
+
191
+ data: list = []
192
+ all_symbols = load_irfcl_symbols()
193
+ country_map_dict = {
194
+ v: k.replace("_", " ").title().replace("Ecb", "ECB")
195
+ for k, v in load_country_to_code_map().items()
196
+ }
197
+ # Iterate over the series to extract observations and map the metadata.
198
+ for s in series:
199
+ if "Obs" not in s:
200
+ continue
201
+ meta = {
202
+ k.replace("@", "").lower(): (
203
+ constants.UNIT_MULTIPLIERS_MAP.get(str(v), v)
204
+ if k == "@UNIT_MULT"
205
+ else v
206
+ )
207
+ for k, v in s.items()
208
+ if k != "Obs"
209
+ }
210
+ _symbol = meta.get("indicator")
211
+ _parent: Optional[str] = None
212
+ _order: Optional[str] = None
213
+ _level: Optional[str] = None
214
+ _table: Optional[str] = None
215
+ _title: Optional[str] = None
216
+ _unit: Optional[str] = None
217
+
218
+ if _symbol not in all_symbols:
219
+ continue
220
+
221
+ _table = all_symbols.get(_symbol, {}).get("table")
222
+ _parent = all_symbols.get(_symbol, {}).get("parent", "")
223
+ _order = all_symbols.get(_symbol, {}).get("order", "")
224
+ _level = all_symbols.get(_symbol, {}).get("level", "")
225
+ _title = all_symbols.get(_symbol, {}).get("title", "").replace(", ", " - ")
226
+ _unit = all_symbols.get(_symbol, {}).get("unit", "")
227
+
228
+ if _title:
229
+ _title = " - ".join(_title.split(", ")[:-1])
230
+
231
+ _data = s.pop("Obs", [])
232
+
233
+ if isinstance(_data, dict):
234
+ _data = [_data]
235
+
236
+ for d in _data:
237
+ _date = d.pop("@TIME_PERIOD", None)
238
+ val = d.pop("@OBS_VALUE", None)
239
+ _ = d.pop("@OBS_STATUS", None)
240
+
241
+ if not val:
242
+ continue
243
+
244
+ if _date:
245
+ offset = (
246
+ offsets.QuarterEnd
247
+ if "Q" in _date
248
+ else offsets.YearEnd if len(str(_date)) == 4 else offsets.MonthEnd
249
+ )
250
+ _date = to_datetime(_date)
251
+ _date = _date + offset(0)
252
+ _date = _date.strftime("%Y-%m-%d")
253
+ vals = {
254
+ k: v
255
+ for k, v in {
256
+ "date": _date,
257
+ "table": _table,
258
+ "symbol": _symbol,
259
+ "parent": _parent,
260
+ "order": _order,
261
+ "level": _level,
262
+ "country": country_map_dict.get(
263
+ meta.get("ref_area"), meta.get("ref_area")
264
+ ),
265
+ "reference_sector": constants.SECTOR_MAP.get(
266
+ meta.get("ref_sector"), meta.get("ref_sector")
267
+ ),
268
+ "title": _title,
269
+ "value": float(val) if val else None,
270
+ "unit": _unit,
271
+ "scale": meta.get("unit_mult"),
272
+ }.items()
273
+ if v
274
+ }
275
+
276
+ if vals.get("value") and vals.get("date"):
277
+ d.update(vals)
278
+ data.append(d)
279
+
280
+ if not data:
281
+ raise OpenBBError(f"No data found for '{indicator}' in '{countries}'.")
282
+
283
+ return data
openbb_platform/providers/imf/poetry.lock ADDED
The diff for this file is too large to render. See raw diff
 
openbb_platform/providers/imf/pyproject.toml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "openbb-imf"
3
+ version = "1.1.1"
4
+ description = "https://datahelp.imf.org/knowledgebase/articles/630877-api"
5
+ authors = ["OpenBB Team <hello@openbb.co>"]
6
+ license = "AGPL-3.0-only"
7
+ readme = "README.md"
8
+ packages = [{ include = "openbb_imf" }]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = ">=3.9.21,<3.13"
12
+ openbb-core = "^1.4.6"
13
+
14
+ [build-system]
15
+ requires = ["poetry-core"]
16
+ build-backend = "poetry.core.masonry.api"
17
+
18
+ [tool.poetry.plugins."openbb_provider_extension"]
19
+ imf = "openbb_imf:imf_provider"
openbb_platform/providers/imf/tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """IMF Provider Fetcher Tests."""
openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_direction_of_trade_fetcher_urllib3_v1.yaml ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interactions:
2
+ - request:
3
+ body: null
4
+ headers:
5
+ Accept:
6
+ - application/json
7
+ Accept-Encoding:
8
+ - gzip, deflate
9
+ Connection:
10
+ - keep-alive
11
+ method: GET
12
+ uri: http://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/DOT/A.US.TXG_FOB_USD.W00+B0?startPeriod=2020-01-01&endPeriod=2023-12-31
13
+ response:
14
+ body:
15
+ string: !!binary |
16
+ H4sIAAAAAAAEA6SSX6+aQBDFv4qhL216gQVRlCdRsCURsfxprH0gK4xeEgHDbtXG3O/e2dWm1zRp
17
+ btIXEs6e35nZmb0qs7Y+0oJ7lFPFuSqTS31omHNhleIoz5wfHV0/n8/aua+13V43CTH0dbhIimeo
18
+ qVo1jNOmAOXpD1i+AZR+VjlMxizagvKqbR7BxAvXsmYHrP3RFcB0IYUL/UYx/WTmRK+BMbqHnuiV
19
+ YbMd7CvGu58aK+uL5B/sMuKGaBdWSgypEq/PoDtVWEar6p0Ei9todC9K3+oTmfJyYor/cx8M+Qy0
20
+ hE4sJfAwqrBKwzQGoI6GI1u1LPyMh9uhurUte0TtHRkbBKkUGEf3jh6YWMuqgyPtoETJJKalkrFq
21
+ ktQYOQPiWBb6E2hKkFUmlXAZM4LqktYgKovn4Bxos0ceGjx4x+HC8ScI58rLkzJrG46vR1izOEBd
22
+ rAHnKZ7MfYwIpXCA43PbABo+9ozee2zlQ29o9tWhaRLlBYNiKKA6vW5ks9mICh5uJgEuR4CLeCWJ
23
+ oqK/hmHsvW6JbvaPPWIzCXQVIPL9qkzmsf8FYRflSezPczf2XfzPEiEESy+YuWkUo5KuP+XzaJpn
24
+ iSeOZlG2TP145cbpb2ZKxEG2DNI8zBYpMkMhpEHoIxmHrpBWxjcUo+29vDxc+XEQeeg3iSkzommS
25
+ f3UXmS+0ft8eWJpt2wbeHDv+CzEwcPKA2L9ao3cbBoIQiqK9OLYQjz+VONpS6N1jhx6N5GALuAIO
26
+ 0gVKlNehkd9GHVVGZrCQQ6RbFJ0elMEm8Zhrnmu/G0Vf/OW4mxQm3e1kbIw+HLupIj1LQB7Z/Ler
27
+ cDiWUSdM5TBrg5W1lzso1ltUP7LXzLwBuF028KsFAAA=
28
+ headers:
29
+ Cache-Control:
30
+ - private
31
+ Connection:
32
+ - Keep-Alive
33
+ Content-Encoding:
34
+ - gzip
35
+ Content-Length:
36
+ - '659'
37
+ Content-Type:
38
+ - application/json; charset=utf-8
39
+ Date:
40
+ - Fri, 20 Sep 2024 22:50:44 GMT
41
+ Set-Cookie: null
42
+ Vary:
43
+ - Accept-Encoding
44
+ X-Content-Type-Options:
45
+ - nosniff
46
+ X-Frame-Options:
47
+ - SAMEORIGIN
48
+ X-Permitted-Cross-Domain-Policies:
49
+ - none
50
+ X-XSS-Protection:
51
+ - 1; mode=block
52
+ status:
53
+ code: 200
54
+ message: OK
55
+ version: 1
openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_direction_of_trade_fetcher_urllib3_v2.yaml ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interactions:
2
+ - request:
3
+ body: null
4
+ headers:
5
+ Accept:
6
+ - application/json
7
+ Accept-Encoding:
8
+ - gzip, deflate
9
+ Connection:
10
+ - keep-alive
11
+ method: GET
12
+ uri: http://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/DOT/A.US.TXG_FOB_USD.W00+B0?startPeriod=2020-01-01&endPeriod=2023-12-31
13
+ response:
14
+ body:
15
+ string: !!binary |
16
+ H4sIAAAAAAAEA6SSX6+aQBDFv4qhL216gWVBUJ5EwZZExPKnsfaBbGFUEgHDUrUx97t3Fm3uNU2T
17
+ m/Rxz57fmZmdvUqzpjqyvHNZxyT7Kk0u1aHm9oWXki3tu+5oq+r5fFbOutK0O5USoqnrYBHne6iY
18
+ XNa8Y3UO0tMLWLwB7P28tHkfs2hy1pVN/QjGbrDua7bAm59tDlwVUrBQbxRXTzQjagWcsx0MRK8c
19
+ m21hV/Ku/aXworr0/IO9j7ghyoUXPYZUgeNzaE8lllHKatuD+e1pVDdM3uoTmf1w4hX/Zx4M+Qys
20
+ gFYsxXcxKtcLTaNkLBcFMWVDHxGZWflQHm1Nczg2iAHWFqkEeIfuLTtwsZZVC0fWQoESJdSQkack
21
+ 0Ua2MbIJQX8MdXGrMimFS5sJdckqEJXFd7APrN4hDzXa33Vw6fDgB3Pp+UmaNXWHv0dY08hHff/y
22
+ Ze7PiFACBzjumxrQ8HGgDd5jKx8GJtVlk1IiPWNQBDmUp9eNbDYbUcHFzcTQ9U+Ai3gliaKiv5pj
23
+ 7L1ugW4O/94jNhNDWwIi36/SZB55XxB2UJ5E3jxzIs/BcxoLwV+6/sxJwgiVZP0pm4fTLI1dcTUL
24
+ 02XiRSsnSv4wUyIu0qWfZEG6SJAxhZD4gYdkFDhCWmnfUAx/3Mv3lysv8kMX/ZTQPiOcxtlXZ5F6
25
+ QtN1a2golmVpODl2/BeiYeDkAbHo+Her9G7DMAwDAXSX1AHBo/jtskUqj8LdLbuMICCFBzjw+MgE
26
+ BdJyk5HfzDBkKqlCXTahsYS8wpzCWcVfffR79ntQ9Hv/5+dpUqhUlZGyMmqz7KKKsEgBmUfx367C
27
+ bphGFdAhm1kLrMxeZiCfZxnjkj26+wT887SyqwUAAA==
28
+ headers:
29
+ Cache-Control:
30
+ - private
31
+ Connection:
32
+ - Keep-Alive
33
+ Content-Encoding:
34
+ - gzip
35
+ Content-Length:
36
+ - '658'
37
+ Content-Type:
38
+ - application/json; charset=utf-8
39
+ Date:
40
+ - Fri, 20 Sep 2024 22:48:00 GMT
41
+ Set-Cookie: null
42
+ Vary:
43
+ - Accept-Encoding
44
+ X-Content-Type-Options:
45
+ - nosniff
46
+ X-Frame-Options:
47
+ - SAMEORIGIN
48
+ X-Permitted-Cross-Domain-Policies:
49
+ - none
50
+ X-XSS-Protection:
51
+ - 1; mode=block
52
+ status:
53
+ code: 200
54
+ message: OK
55
+ version: 1
openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_economic_indicators_fetcher_urllib3_v1.yaml ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interactions:
2
+ - request:
3
+ body: null
4
+ headers:
5
+ Accept:
6
+ - application/json
7
+ Accept-Encoding:
8
+ - gzip, deflate
9
+ Connection:
10
+ - keep-alive
11
+ method: GET
12
+ uri: http://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/IRFCL/M.JP.RAMFDA_USD.S1X?startPeriod=2023-01-01&endPeriod=2023-12-31
13
+ response:
14
+ body:
15
+ string: !!binary |
16
+ H4sIAAAAAAAEA6SSX4+aQBTFv4qhL226yICiC09S/qQ0oBawMfaBTJmrkggYZlZtzH733hlNttuk
17
+ zSZ9nHPP7557Z+aq+V1zpJUIqKCae9Vml+bQcvfCa83V9kIcXcM4n8/D82jY9TvDIsQ01mmSV3to
18
+ qF63XNC2Au3hBWRvAJWf1y5XbZKuoqLu2tdgHqRrldkD7576CrghpTQxbhQ3TlZJjAY4pzsYyFk5
19
+ DtvDruai/znkrLko/pVdtbghwwtnCkOK4foc+lONMcO62Sqwul2NEWeRn7zdKfuqBeVN/s9O2OQz
20
+ UAa9fJg4wFbjEXu02cTW2eN2pI8dRnUHto5Ox4wSx7bNCiykCuAC3Vt64PJplj0caQ8MJYtYY504
21
+ ukkKc+qatmvZ6M+hvafMaukyfYLqnDYgk+WXcA+03SEPLRbeCbgIPMRppD0/aH7XCvxB0rrKYtT3
22
+ L9/mfpUIFXCA475rAQ0fB+bgPY7yYTCxRvrEsoj2jI0yqKA+gVp3VjM0bjYbmRBQQXMQcYCSeozf
23
+ RBkrJ2w5Fu/JDP38n6+JA+XQ14DQVZtFWfgV6RTVWRZGpZeFHp6/LKUQz4PY94pFhkrmpVHglas8
24
+ kBVpzUP/VsrNtdRW87go01VSoHsihSJOwzJaZKknpaUpQxY/MPc7BqviMsziRYB+i+BtEFNSi095
25
+ +c1LViHK+tQZ48J/sVt/2m0y/dXnHNwADIMwFN2l90oBAnGG6SjZvTmDjM9P1ufaslYfXM+ssY1r
26
+ z1pMGx6Fo2tZhesCj0HhNjfnu3BfQbmMwrt3kcwxJj/XrF8NPOc7dz97tJr2sAUAAA==
27
+ headers:
28
+ Cache-Control:
29
+ - private
30
+ Connection:
31
+ - Keep-Alive
32
+ Content-Encoding:
33
+ - gzip
34
+ Content-Length:
35
+ - '619'
36
+ Content-Type:
37
+ - application/json; charset=utf-8
38
+ Date:
39
+ - Tue, 10 Sep 2024 21:15:25 GMT
40
+ Set-Cookie: null
41
+ Vary:
42
+ - Accept-Encoding
43
+ X-Content-Type-Options:
44
+ - nosniff
45
+ X-Frame-Options:
46
+ - SAMEORIGIN
47
+ X-Permitted-Cross-Domain-Policies:
48
+ - none
49
+ X-XSS-Protection:
50
+ - 1; mode=block
51
+ status:
52
+ code: 200
53
+ message: OK
54
+ version: 1
openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_economic_indicators_fetcher_urllib3_v2.yaml ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interactions:
2
+ - request:
3
+ body: null
4
+ headers:
5
+ Accept:
6
+ - application/json
7
+ Accept-Encoding:
8
+ - gzip, deflate
9
+ Connection:
10
+ - keep-alive
11
+ method: GET
12
+ uri: http://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/IRFCL/M.JP.RAMFDA_USD.S1X?startPeriod=2023-01-01&endPeriod=2023-12-31
13
+ response:
14
+ body:
15
+ string: !!binary |
16
+ H4sIAAAAAAAEA6SSXY+aQBSG/4qhN226yDB+ULmS8pHSgFrAxtgLMg5HJREwDKs2Zv97z4w27W6y
17
+ zSa95J3nPe/54Kq5TXVkvPNYxzT7qk0v1aEW9kWUmq3tu+5oG8b5fO6fB/2m3RmUENNYxVHK91Ax
18
+ vaxFx2oO2sMfY/EGo+JFaQtVJmo468qmfm5MvXilMlsQzWPLQRhSiiPj5hLGiebEqEAItoOe7FVg
19
+ sy3sStG1P/uiqC7K/wxXJW6W/kUUyoauAscX0J5KjOmX1VYZ+W01RpgEbvR2UtZVA8pN/s9MWOQL
20
+ sAJaeZjQw1IFZeaWTLjOh9ZAH47ISN8UfKOTicVhY32a8BFFVwaiQ3rLDkKeZtHCkbVQoEQJHSKt
21
+ EyszB/aQ2qMx8inU95RpKSnTJajOWAUyWf4S9oHVO/RDjQ/vOrh0+BHGgfb0oLlN3TGOwlVbJiHq
22
+ 8hS4U/nb3FeJpgwOcNw3NSDwsWf23mMrH3pjOtDHlBLtCQslwKE8gRp3WhYIrtdrmeDhdVLo1ArU
23
+ Mf4Sf3dYC+TvyQXy4p/XxIZSaEtA01WbBon/Dd0xqtPED3In8R38/rqQQjjzQtfJ5gkqiRMHnpMv
24
+ U0++SDT13dtTaq6ktpyFWR4vowzpsRSyMPbzYJ7EjpQWpgyZbzD3Bwarx4WfhHMPeUpwG8SUrvnn
25
+ NP/uREsfZd2aDHHgV3D6Eh8R63V68JL+1eecmwAMxEAU7cW5QfdRjEvZ3r2xhDTxY/ikMGupOpJn
26
+ rVUj08Kt8dhavHHymGOicZaceTaubiNHaHx7R6w8QOZzqvoli+d85+4HilPlVbAFAAA=
27
+ headers:
28
+ Cache-Control:
29
+ - private
30
+ Connection:
31
+ - Keep-Alive
32
+ Content-Encoding:
33
+ - gzip
34
+ Content-Length:
35
+ - '620'
36
+ Content-Type:
37
+ - application/json; charset=utf-8
38
+ Date:
39
+ - Sat, 07 Sep 2024 17:42:56 GMT
40
+ Set-Cookie: null
41
+ Vary:
42
+ - Accept-Encoding
43
+ X-Content-Type-Options:
44
+ - nosniff
45
+ X-Frame-Options:
46
+ - SAMEORIGIN
47
+ X-Permitted-Cross-Domain-Policies:
48
+ - none
49
+ X-XSS-Protection:
50
+ - 1; mode=block
51
+ status:
52
+ code: 200
53
+ message: OK
54
+ version: 1
openbb_platform/providers/imf/tests/test_imf_fetchers.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IMF Fetcher Tests."""
2
+
3
+ from datetime import date
4
+
5
+ import pytest
6
+ from openbb_core.app.service.user_service import UserService
7
+ from openbb_imf.models.available_indicators import ImfAvailableIndicatorsFetcher
8
+ from openbb_imf.models.direction_of_trade import ImfDirectionOfTradeFetcher
9
+ from openbb_imf.models.economic_indicators import ImfEconomicIndicatorsFetcher
10
+
11
+ test_credentials = UserService().default_user_settings.credentials.model_dump(
12
+ mode="json"
13
+ )
14
+
15
+
16
+ def scrub_string(key):
17
+ """Scrub a string from the response."""
18
+
19
+ def before_record_response(response):
20
+ response["headers"][key] = response["headers"].update({key: None})
21
+ return response
22
+
23
+ return before_record_response
24
+
25
+
26
+ @pytest.fixture(scope="module")
27
+ def vcr_config():
28
+ """VCR configuration."""
29
+ return {
30
+ "filter_headers": [("User-Agent", None)],
31
+ "before_record_response": [
32
+ scrub_string("Set-Cookie"),
33
+ ],
34
+ }
35
+
36
+
37
+ @pytest.mark.record_http
38
+ def test_imf_economic_indicators_fetcher(credentials=test_credentials):
39
+ """Test the IMF EconomicIndicators fetcher."""
40
+ params = {
41
+ "country": "JP",
42
+ "frequency": "month",
43
+ "symbol": "RAMFDA_USD",
44
+ "start_date": date(2023, 1, 1),
45
+ "end_date": date(2023, 12, 31),
46
+ }
47
+
48
+ fetcher = ImfEconomicIndicatorsFetcher()
49
+ result = fetcher.test(params, credentials)
50
+ assert result is None
51
+
52
+
53
+ # The data for this request are local files, so we can't record them.
54
+ def test_imf_available_indicators_fetcher(credentials=test_credentials):
55
+ """Test the IMF Available Indicators fetcher."""
56
+ params = {}
57
+
58
+ fetcher = ImfAvailableIndicatorsFetcher()
59
+ result = fetcher.test(params, credentials)
60
+ assert result is None
61
+
62
+
63
+ @pytest.mark.record_http
64
+ def test_imf_direction_of_trade_fetcher(credentials=test_credentials):
65
+ """Test the ImfDirectionOfTrade fetcher."""
66
+ params = {
67
+ "provider": "imf",
68
+ "country": "us",
69
+ "counterpart": "world,eu",
70
+ "frequency": "annual",
71
+ "direction": "exports",
72
+ "start_date": date(2020, 1, 1),
73
+ "end_date": date(2023, 1, 1),
74
+ }
75
+
76
+ fetcher = ImfDirectionOfTradeFetcher()
77
+ result = fetcher.test(params, credentials)
78
+ assert result is None
openbb_platform/providers/intrinio/README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenBB Intrinio Provider
2
+
3
+ This extension integrates the [Intrinio](https://intrinio.com/) data provider into the OpenBB Platform.
4
+
5
+ ## Installation
6
+
7
+ To install the extension:
8
+
9
+ ```bash
10
+ pip install openbb-intrinio
11
+ ```
12
+
13
+ Documentation available [here](https://docs.openbb.co/platform/developer_guide/contributing).
openbb_platform/providers/intrinio/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Intrinio provider."""
openbb_platform/providers/intrinio/openbb_intrinio/__init__.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Provider Modules."""
2
+
3
+ from openbb_core.provider.abstract.provider import Provider
4
+ from openbb_intrinio.models.balance_sheet import IntrinioBalanceSheetFetcher
5
+ from openbb_intrinio.models.calendar_ipo import IntrinioCalendarIpoFetcher
6
+ from openbb_intrinio.models.cash_flow import IntrinioCashFlowStatementFetcher
7
+ from openbb_intrinio.models.company_filings import IntrinioCompanyFilingsFetcher
8
+ from openbb_intrinio.models.company_news import IntrinioCompanyNewsFetcher
9
+ from openbb_intrinio.models.currency_pairs import IntrinioCurrencyPairsFetcher
10
+ from openbb_intrinio.models.equity_historical import IntrinioEquityHistoricalFetcher
11
+ from openbb_intrinio.models.equity_info import IntrinioEquityInfoFetcher
12
+ from openbb_intrinio.models.equity_quote import IntrinioEquityQuoteFetcher
13
+ from openbb_intrinio.models.equity_search import IntrinioEquitySearchFetcher
14
+ from openbb_intrinio.models.etf_holdings import IntrinioEtfHoldingsFetcher
15
+ from openbb_intrinio.models.etf_info import IntrinioEtfInfoFetcher
16
+ from openbb_intrinio.models.etf_price_performance import (
17
+ IntrinioEtfPricePerformanceFetcher,
18
+ )
19
+ from openbb_intrinio.models.etf_search import IntrinioEtfSearchFetcher
20
+ from openbb_intrinio.models.financial_ratios import IntrinioFinancialRatiosFetcher
21
+ from openbb_intrinio.models.forward_ebitda_estimates import (
22
+ IntrinioForwardEbitdaEstimatesFetcher,
23
+ )
24
+ from openbb_intrinio.models.forward_eps_estimates import (
25
+ IntrinioForwardEpsEstimatesFetcher,
26
+ )
27
+ from openbb_intrinio.models.forward_pe_estimates import (
28
+ IntrinioForwardPeEstimatesFetcher,
29
+ )
30
+ from openbb_intrinio.models.forward_sales_estimates import (
31
+ IntrinioForwardSalesEstimatesFetcher,
32
+ )
33
+ from openbb_intrinio.models.fred_series import IntrinioFredSeriesFetcher
34
+ from openbb_intrinio.models.historical_attributes import (
35
+ IntrinioHistoricalAttributesFetcher,
36
+ )
37
+ from openbb_intrinio.models.historical_dividends import (
38
+ IntrinioHistoricalDividendsFetcher,
39
+ )
40
+ from openbb_intrinio.models.historical_market_cap import (
41
+ IntrinioHistoricalMarketCapFetcher,
42
+ )
43
+ from openbb_intrinio.models.income_statement import IntrinioIncomeStatementFetcher
44
+ from openbb_intrinio.models.index_historical import IntrinioIndexHistoricalFetcher
45
+ from openbb_intrinio.models.insider_trading import IntrinioInsiderTradingFetcher
46
+
47
+ # from openbb_intrinio.models.institutional_ownership import (
48
+ # IntrinioInstitutionalOwnershipFetcher,
49
+ # )
50
+ from openbb_intrinio.models.key_metrics import IntrinioKeyMetricsFetcher
51
+ from openbb_intrinio.models.latest_attributes import IntrinioLatestAttributesFetcher
52
+ from openbb_intrinio.models.market_snapshots import IntrinioMarketSnapshotsFetcher
53
+ from openbb_intrinio.models.options_chains import IntrinioOptionsChainsFetcher
54
+ from openbb_intrinio.models.options_snapshots import IntrinioOptionsSnapshotsFetcher
55
+ from openbb_intrinio.models.options_unusual import IntrinioOptionsUnusualFetcher
56
+ from openbb_intrinio.models.price_target_consensus import (
57
+ IntrinioPriceTargetConsensusFetcher,
58
+ )
59
+ from openbb_intrinio.models.reported_financials import IntrinioReportedFinancialsFetcher
60
+ from openbb_intrinio.models.search_attributes import (
61
+ IntrinioSearchAttributesFetcher,
62
+ )
63
+ from openbb_intrinio.models.share_statistics import IntrinioShareStatisticsFetcher
64
+ from openbb_intrinio.models.world_news import IntrinioWorldNewsFetcher
65
+
66
+ intrinio_provider = Provider(
67
+ name="intrinio",
68
+ website="https://intrinio.com",
69
+ description="""Intrinio is a financial data platform that provides real-time and
70
+ historical financial market data to businesses and developers through an API.""",
71
+ credentials=["api_key"],
72
+ fetcher_dict={
73
+ "BalanceSheet": IntrinioBalanceSheetFetcher,
74
+ "CalendarIpo": IntrinioCalendarIpoFetcher,
75
+ "CashFlowStatement": IntrinioCashFlowStatementFetcher,
76
+ "CompanyFilings": IntrinioCompanyFilingsFetcher,
77
+ "CompanyNews": IntrinioCompanyNewsFetcher,
78
+ "CurrencyPairs": IntrinioCurrencyPairsFetcher,
79
+ "EquityHistorical": IntrinioEquityHistoricalFetcher,
80
+ "EquityInfo": IntrinioEquityInfoFetcher,
81
+ "EquityQuote": IntrinioEquityQuoteFetcher,
82
+ "EquitySearch": IntrinioEquitySearchFetcher,
83
+ "EtfHistorical": IntrinioEquityHistoricalFetcher,
84
+ "EtfHoldings": IntrinioEtfHoldingsFetcher,
85
+ "EtfInfo": IntrinioEtfInfoFetcher,
86
+ "EtfPricePerformance": IntrinioEtfPricePerformanceFetcher,
87
+ "EtfSearch": IntrinioEtfSearchFetcher,
88
+ "FinancialRatios": IntrinioFinancialRatiosFetcher,
89
+ "ForwardEbitdaEstimates": IntrinioForwardEbitdaEstimatesFetcher,
90
+ "ForwardEpsEstimates": IntrinioForwardEpsEstimatesFetcher,
91
+ "ForwardPeEstimates": IntrinioForwardPeEstimatesFetcher,
92
+ "ForwardSalesEstimates": IntrinioForwardSalesEstimatesFetcher,
93
+ "FredSeries": IntrinioFredSeriesFetcher,
94
+ "HistoricalAttributes": IntrinioHistoricalAttributesFetcher,
95
+ "HistoricalDividends": IntrinioHistoricalDividendsFetcher,
96
+ "HistoricalMarketCap": IntrinioHistoricalMarketCapFetcher,
97
+ "IncomeStatement": IntrinioIncomeStatementFetcher,
98
+ "IndexHistorical": IntrinioIndexHistoricalFetcher,
99
+ "InsiderTrading": IntrinioInsiderTradingFetcher,
100
+ # "InstitutionalOwnership": IntrinioInstitutionalOwnershipFetcher, # Disabled due to unreliable Intrinio endpoint
101
+ "KeyMetrics": IntrinioKeyMetricsFetcher,
102
+ "LatestAttributes": IntrinioLatestAttributesFetcher,
103
+ "MarketSnapshots": IntrinioMarketSnapshotsFetcher,
104
+ "OptionsChains": IntrinioOptionsChainsFetcher,
105
+ "OptionsSnapshots": IntrinioOptionsSnapshotsFetcher,
106
+ "OptionsUnusual": IntrinioOptionsUnusualFetcher,
107
+ "PriceTargetConsensus": IntrinioPriceTargetConsensusFetcher,
108
+ "ReportedFinancials": IntrinioReportedFinancialsFetcher,
109
+ "SearchAttributes": IntrinioSearchAttributesFetcher,
110
+ "ShareStatistics": IntrinioShareStatisticsFetcher,
111
+ "WorldNews": IntrinioWorldNewsFetcher,
112
+ },
113
+ repr_name="Intrinio",
114
+ deprecated_credentials={"API_INTRINIO_KEY": "intrinio_api_key"},
115
+ instructions="Go to: https://intrinio.com/starter-plan\n\n![Intrinio](https://user-images.githubusercontent.com/85772166/219207556-fcfee614-59f1-46ae-bff4-c63dd2f6991d.png)\n\nAn API key will be issued with a subscription. Find the token value within the account dashboard.", # noqa: E501 pylint: disable=line-too-long
116
+ )
openbb_platform/providers/intrinio/openbb_intrinio/models/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Intrinio models directory."""
openbb_platform/providers/intrinio/openbb_intrinio/models/balance_sheet.py ADDED
@@ -0,0 +1,506 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Balance Sheet Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from typing import Any, Dict, List, Literal, Optional
6
+ from warnings import warn
7
+
8
+ from openbb_core.provider.abstract.fetcher import Fetcher
9
+ from openbb_core.provider.standard_models.balance_sheet import (
10
+ BalanceSheetData,
11
+ BalanceSheetQueryParams,
12
+ )
13
+ from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
14
+ from openbb_core.provider.utils.helpers import ClientResponse, amake_requests
15
+ from openbb_intrinio.utils.helpers import get_data_one
16
+ from pydantic import Field, field_validator, model_validator
17
+
18
+
19
+ class IntrinioBalanceSheetQueryParams(BalanceSheetQueryParams):
20
+ """Intrinio Balance Sheet Query.
21
+
22
+ Source: https://docs.intrinio.com/documentation/web_api/get_company_fundamentals_v2
23
+ Source: https://docs.intrinio.com/documentation/web_api/get_fundamental_standardized_financials_v2
24
+ """
25
+
26
+ __json_schema_extra__ = {
27
+ "period": {
28
+ "choices": ["annual", "quarter"],
29
+ }
30
+ }
31
+
32
+ period: Literal["annual", "quarter"] = Field(
33
+ default="annual",
34
+ description=QUERY_DESCRIPTIONS.get("period", ""),
35
+ )
36
+ fiscal_year: Optional[int] = Field(
37
+ default=None,
38
+ description="The specific fiscal year. Reports do not go beyond 2008.",
39
+ )
40
+
41
+ @field_validator("symbol", mode="after", check_fields=False)
42
+ @classmethod
43
+ def handle_symbol(cls, v) -> str:
44
+ """Handle symbols with a dash and replace it with a dot for Intrinio."""
45
+ return v.replace("-", ".")
46
+
47
+
48
+ class IntrinioBalanceSheetData(BalanceSheetData):
49
+ """Intrinio Balance Sheet Data."""
50
+
51
+ __alias_dict__ = {
52
+ "cash_and_cash_equivalents": "cashandequivalents",
53
+ "restricted_cash": "restrictedcash",
54
+ "short_term_investments": "shortterminvestments",
55
+ "federal_funds_sold": "fedfundssold",
56
+ "note_and_lease_receivable": "notereceivable",
57
+ "interest_bearing_deposits_at_other_banks": "interestbearingdepositsatotherbanks",
58
+ "accounts_receivable": "accountsreceivable",
59
+ "time_deposits_placed_and_other_short_term_investments": "timedepositsplaced",
60
+ "inventories": "netinventory",
61
+ "trading_account_securities": "tradingaccountsecurities",
62
+ "prepaid_expenses": "prepaidexpenses",
63
+ "loans_and_leases": "loansandleases",
64
+ "allowance_for_loan_and_lease_losses": "allowanceforloanandleaselosses",
65
+ "current_deferred_refundable_income_taxes": "currentdeferredtaxassets",
66
+ "other_current_assets": "othercurrentassets",
67
+ "loans_and_leases_net_of_allowance": "netloansandleases",
68
+ "other_current_non_operating_assets": "othercurrentnonoperatingassets",
69
+ "loans_held_for_sale": "loansheldforsale",
70
+ "total_current_assets": "totalcurrentassets",
71
+ "accrued_investment_income": "accruedinvestmentincome",
72
+ "plant_property_equipment_gross": "grossppe",
73
+ "customer_and_other_receivables": "customerandotherreceivables",
74
+ "accumulated_depreciation": "accumulateddepreciation",
75
+ "premises_and_equipment_net": "netpremisesandequipment",
76
+ "plant_property_equipment_net": "netppe",
77
+ "mortgage_servicing_rights": "mortgageservicingrights",
78
+ "long_term_investments": "longterminvestments",
79
+ "unearned_premiums_asset": "unearnedpremiumsdebit",
80
+ "non_current_note_lease_receivables": "noncurrentnotereceivables",
81
+ "deferred_acquisition_cost": "deferredacquisitioncost",
82
+ "goodwill": "goodwill",
83
+ "separate_account_business_assets": "separateaccountbusinessassets",
84
+ "intangible_assets": "intangibleassets",
85
+ "non_current_deferred_refundable_income_taxes": "noncurrentdeferredtaxassets",
86
+ "employee_benefit_assets": "employeebenefitassets",
87
+ "other_assets": "otherassets",
88
+ "other_non_current_operating_assets": "othernoncurrentassets",
89
+ "total_assets": "totalassets",
90
+ "other_non_current_non_operating_assets": "othernoncurrentnonoperatingassets",
91
+ "non_interest_bearing_deposits": "noninterestbearingdeposits",
92
+ "interest_bearing_deposits": "interestbearingdeposits",
93
+ "total_non_current_assets": "totalnoncurrentassets",
94
+ "federal_funds_purchased_and_securities_sold": "fedfundspurchased",
95
+ "short_term_debt": "shorttermdebt",
96
+ "bankers_acceptance_out_standing": "bankersacceptances",
97
+ "accrued_interest_payable": "accruedinterestpayable",
98
+ "accounts_payable": "accountspayable",
99
+ "accrued_expenses": "accruedexpenses",
100
+ "other_short_term_payables": "othershorttermpayables",
101
+ "long_term_debt": "longtermdebt",
102
+ "customer_deposits": "customerdeposits",
103
+ "capital_lease_obligations": "capitalleaseobligations",
104
+ "dividends_payable": "dividendspayable",
105
+ "claims_and_claim_expense": "claimsandclaimexpenses",
106
+ "current_deferred_revenue": "currentdeferredrevenue",
107
+ "future_policy_benefits": "futurepolicybenefits",
108
+ "current_deferred_payable_income_tax_liabilities": "currentdeferredtaxliabilities",
109
+ "current_employee_benefit_liabilities": "currentemployeebenefitliabilities",
110
+ "unearned_premiums_liability": "unearnedpremiumscredit",
111
+ "other_taxes_payable": "othertaxespayable",
112
+ "policy_holder_funds": "policyholderfunds",
113
+ "other_current_liabilities": "othercurrentliabilities",
114
+ "participating_policy_holder_equity": "participatingpolicyholderequity",
115
+ "other_current_non_operating_liabilities": "othercurrentnonoperatingliabilities",
116
+ "separate_account_business_liabilities": "separateaccountbusinessliabilities",
117
+ "total_current_liabilities": "totalcurrentliabilities",
118
+ "other_long_term_liabilities": "otherlongtermliabilities",
119
+ "total_liabilities": "totalliabilities",
120
+ "commitments_contingencies": "commitmentsandcontingencies",
121
+ "asset_retirement_reserve_litigation_obligation": "assetretirementandlitigationobligation",
122
+ "redeemable_non_controlling_interest": "redeemablenoncontrollinginterest",
123
+ "non_current_deferred_revenue": "noncurrentdeferredrevenue",
124
+ "preferred_stock": "totalpreferredequity",
125
+ "common_stock": "commonequity",
126
+ "non_current_deferred_payable_income_tax_liabilities": "noncurrentdeferredtaxliabilities",
127
+ "non_current_employee_benefit_liabilities": "noncurrentemployeebenefitliabilities",
128
+ "retained_earnings": "retainedearnings",
129
+ "other_non_current_operating_liabilities": "othernoncurrentliabilities",
130
+ "treasury_stock": "treasurystock",
131
+ "accumulated_other_comprehensive_income": "aoci",
132
+ "other_non_current_non_operating_liabilities": "othernoncurrentnonoperatingliabilities",
133
+ "other_equity_adjustments": "otherequity",
134
+ "total_non_current_liabilities": "totalnoncurrentliabilities",
135
+ "total_common_equity": "totalcommonequity",
136
+ "total_preferred_common_equity": "totalequity",
137
+ "non_controlling_interest": "noncontrollinginterests",
138
+ "total_equity_non_controlling_interests": "totalequityandnoncontrollinginterests",
139
+ "total_liabilities_shareholders_equity": "totalliabilitiesandequity",
140
+ }
141
+ reported_currency: Optional[str] = Field(
142
+ description="The currency in which the balance sheet is reported.",
143
+ default=None,
144
+ )
145
+ cash_and_cash_equivalents: Optional[float] = Field(
146
+ description="Cash and cash equivalents.", default=None
147
+ )
148
+ cash_and_due_from_banks: Optional[float] = Field(
149
+ description="Cash and due from banks.", default=None
150
+ )
151
+ restricted_cash: Optional[float] = Field(
152
+ description="Restricted cash.", default=None
153
+ )
154
+ short_term_investments: Optional[float] = Field(
155
+ description="Short term investments.", default=None
156
+ )
157
+ federal_funds_sold: Optional[float] = Field(
158
+ description="Federal funds sold.", default=None
159
+ )
160
+ accounts_receivable: Optional[float] = Field(
161
+ description="Accounts receivable.", default=None
162
+ )
163
+ note_and_lease_receivable: Optional[float] = Field(
164
+ description="Note and lease receivable. (Vendor non-trade receivables)",
165
+ default=None,
166
+ )
167
+ inventories: Optional[float] = Field(description="Net Inventories.", default=None)
168
+ customer_and_other_receivables: Optional[float] = Field(
169
+ description="Customer and other receivables.", default=None
170
+ )
171
+ interest_bearing_deposits_at_other_banks: Optional[float] = Field(
172
+ description="Interest bearing deposits at other banks.", default=None
173
+ )
174
+ time_deposits_placed_and_other_short_term_investments: Optional[float] = Field(
175
+ description="Time deposits placed and other short term investments.",
176
+ default=None,
177
+ )
178
+ trading_account_securities: Optional[float] = Field(
179
+ description="Trading account securities.", default=None
180
+ )
181
+ loans_and_leases: Optional[float] = Field(
182
+ description="Loans and leases.", default=None
183
+ )
184
+ allowance_for_loan_and_lease_losses: Optional[float] = Field(
185
+ description="Allowance for loan and lease losses.", default=None
186
+ )
187
+ current_deferred_refundable_income_taxes: Optional[float] = Field(
188
+ description="Current deferred refundable income taxes.", default=None
189
+ )
190
+ other_current_assets: Optional[float] = Field(
191
+ description="Other current assets.", default=None
192
+ )
193
+ loans_and_leases_net_of_allowance: Optional[float] = Field(
194
+ description="Loans and leases net of allowance.", default=None
195
+ )
196
+ accrued_investment_income: Optional[float] = Field(
197
+ description="Accrued investment income.", default=None
198
+ )
199
+ other_current_non_operating_assets: Optional[float] = Field(
200
+ description="Other current non-operating assets.", default=None
201
+ )
202
+ loans_held_for_sale: Optional[float] = Field(
203
+ description="Loans held for sale.", default=None
204
+ )
205
+ prepaid_expenses: Optional[float] = Field(
206
+ description="Prepaid expenses.", default=None
207
+ )
208
+ total_current_assets: Optional[float] = Field(
209
+ description="Total current assets.", default=None
210
+ )
211
+ plant_property_equipment_gross: Optional[float] = Field(
212
+ description="Plant property equipment gross.", default=None
213
+ )
214
+ accumulated_depreciation: Optional[float] = Field(
215
+ description="Accumulated depreciation.", default=None
216
+ )
217
+ premises_and_equipment_net: Optional[float] = Field(
218
+ description="Net premises and equipment.", default=None
219
+ )
220
+ plant_property_equipment_net: Optional[float] = Field(
221
+ description="Net plant property equipment.", default=None
222
+ )
223
+ long_term_investments: Optional[float] = Field(
224
+ description="Long term investments.", default=None
225
+ )
226
+ mortgage_servicing_rights: Optional[float] = Field(
227
+ description="Mortgage servicing rights.", default=None
228
+ )
229
+ unearned_premiums_asset: Optional[float] = Field(
230
+ description="Unearned premiums asset.", default=None
231
+ )
232
+ non_current_note_lease_receivables: Optional[float] = Field(
233
+ description="Non-current note lease receivables.", default=None
234
+ )
235
+ deferred_acquisition_cost: Optional[float] = Field(
236
+ description="Deferred acquisition cost.", default=None
237
+ )
238
+ goodwill: Optional[float] = Field(description="Goodwill.", default=None)
239
+ separate_account_business_assets: Optional[float] = Field(
240
+ description="Separate account business assets.", default=None
241
+ )
242
+ non_current_deferred_refundable_income_taxes: Optional[float] = Field(
243
+ description="Noncurrent deferred refundable income taxes.", default=None
244
+ )
245
+ intangible_assets: Optional[float] = Field(
246
+ description="Intangible assets.", default=None
247
+ )
248
+ employee_benefit_assets: Optional[float] = Field(
249
+ description="Employee benefit assets.", default=None
250
+ )
251
+ other_assets: Optional[float] = Field(description="Other assets.", default=None)
252
+ other_non_current_operating_assets: Optional[float] = Field(
253
+ description="Other noncurrent operating assets.", default=None
254
+ )
255
+ other_non_current_non_operating_assets: Optional[float] = Field(
256
+ description="Other noncurrent non-operating assets.", default=None
257
+ )
258
+ interest_bearing_deposits: Optional[float] = Field(
259
+ description="Interest bearing deposits.", default=None
260
+ )
261
+ total_non_current_assets: Optional[float] = Field(
262
+ description="Total noncurrent assets.", default=None
263
+ )
264
+ total_assets: Optional[float] = Field(description="Total assets.", default=None)
265
+ non_interest_bearing_deposits: Optional[float] = Field(
266
+ description="Non interest bearing deposits.", default=None
267
+ )
268
+ federal_funds_purchased_and_securities_sold: Optional[float] = Field(
269
+ description="Federal funds purchased and securities sold.", default=None
270
+ )
271
+ bankers_acceptance_outstanding: Optional[float] = Field(
272
+ description="Bankers acceptance outstanding.", default=None
273
+ )
274
+ short_term_debt: Optional[float] = Field(
275
+ description="Short term debt.", default=None
276
+ )
277
+ accounts_payable: Optional[float] = Field(
278
+ description="Accounts payable.", default=None
279
+ )
280
+ current_deferred_revenue: Optional[float] = Field(
281
+ description="Current deferred revenue.", default=None
282
+ )
283
+ current_deferred_payable_income_tax_liabilities: Optional[float] = Field(
284
+ description="Current deferred payable income tax liabilities.", default=None
285
+ )
286
+ accrued_interest_payable: Optional[float] = Field(
287
+ description="Accrued interest payable.", default=None
288
+ )
289
+ accrued_expenses: Optional[float] = Field(
290
+ description="Accrued expenses.", default=None
291
+ )
292
+ other_short_term_payables: Optional[float] = Field(
293
+ description="Other short term payables.", default=None
294
+ )
295
+ customer_deposits: Optional[float] = Field(
296
+ description="Customer deposits.", default=None
297
+ )
298
+ dividends_payable: Optional[float] = Field(
299
+ description="Dividends payable.", default=None
300
+ )
301
+ claims_and_claim_expense: Optional[float] = Field(
302
+ description="Claims and claim expense.", default=None
303
+ )
304
+ future_policy_benefits: Optional[float] = Field(
305
+ description="Future policy benefits.", default=None
306
+ )
307
+ current_employee_benefit_liabilities: Optional[float] = Field(
308
+ description="Current employee benefit liabilities.", default=None
309
+ )
310
+ unearned_premiums_liability: Optional[float] = Field(
311
+ description="Unearned premiums liability.", default=None
312
+ )
313
+ other_taxes_payable: Optional[float] = Field(
314
+ description="Other taxes payable.", default=None
315
+ )
316
+ policy_holder_funds: Optional[float] = Field(
317
+ description="Policy holder funds.", default=None
318
+ )
319
+ other_current_liabilities: Optional[float] = Field(
320
+ description="Other current liabilities.", default=None
321
+ )
322
+ other_current_non_operating_liabilities: Optional[float] = Field(
323
+ description="Other current non-operating liabilities.", default=None
324
+ )
325
+ separate_account_business_liabilities: Optional[float] = Field(
326
+ description="Separate account business liabilities.", default=None
327
+ )
328
+ total_current_liabilities: Optional[float] = Field(
329
+ description="Total current liabilities.", default=None
330
+ )
331
+ long_term_debt: Optional[float] = Field(description="Long term debt.", default=None)
332
+ other_long_term_liabilities: Optional[float] = Field(
333
+ description="Other long term liabilities.", default=None
334
+ )
335
+ non_current_deferred_revenue: Optional[float] = Field(
336
+ description="Non-current deferred revenue.", default=None
337
+ )
338
+ non_current_deferred_payable_income_tax_liabilities: Optional[float] = Field(
339
+ description="Non-current deferred payable income tax liabilities.", default=None
340
+ )
341
+ non_current_employee_benefit_liabilities: Optional[float] = Field(
342
+ description="Non-current employee benefit liabilities.", default=None
343
+ )
344
+ other_non_current_operating_liabilities: Optional[float] = Field(
345
+ description="Other non-current operating liabilities.", default=None
346
+ )
347
+ other_non_current_non_operating_liabilities: Optional[float] = Field(
348
+ description="Other non-current, non-operating liabilities.", default=None
349
+ )
350
+ total_non_current_liabilities: Optional[float] = Field(
351
+ description="Total non-current liabilities.", default=None
352
+ )
353
+ capital_lease_obligations: Optional[float] = Field(
354
+ description="Capital lease obligations.", default=None
355
+ )
356
+ asset_retirement_reserve_litigation_obligation: Optional[float] = Field(
357
+ description="Asset retirement reserve litigation obligation.", default=None
358
+ )
359
+ total_liabilities: Optional[float] = Field(
360
+ description="Total liabilities.", default=None
361
+ )
362
+ commitments_contingencies: Optional[float] = Field(
363
+ description="Commitments contingencies.", default=None
364
+ )
365
+ redeemable_non_controlling_interest: Optional[float] = Field(
366
+ description="Redeemable non-controlling interest.", default=None
367
+ )
368
+ preferred_stock: Optional[float] = Field(
369
+ description="Preferred stock.", default=None
370
+ )
371
+ common_stock: Optional[float] = Field(description="Common stock.", default=None)
372
+ retained_earnings: Optional[float] = Field(
373
+ description="Retained earnings.", default=None
374
+ )
375
+ treasury_stock: Optional[float] = Field(description="Treasury stock.", default=None)
376
+ accumulated_other_comprehensive_income: Optional[float] = Field(
377
+ description="Accumulated other comprehensive income.", default=None
378
+ )
379
+ participating_policy_holder_equity: Optional[float] = Field(
380
+ description="Participating policy holder equity.", default=None
381
+ )
382
+ other_equity_adjustments: Optional[float] = Field(
383
+ description="Other equity adjustments.", default=None
384
+ )
385
+ total_common_equity: Optional[float] = Field(
386
+ description="Total common equity.", default=None
387
+ )
388
+ total_preferred_common_equity: Optional[float] = Field(
389
+ description="Total preferred common equity.", default=None
390
+ )
391
+ non_controlling_interest: Optional[float] = Field(
392
+ description="Non-controlling interest.", default=None
393
+ )
394
+ total_equity_non_controlling_interests: Optional[float] = Field(
395
+ description="Total equity non-controlling interests.", default=None
396
+ )
397
+ total_liabilities_shareholders_equity: Optional[float] = Field(
398
+ description="Total liabilities and shareholders equity.", default=None
399
+ )
400
+
401
+ @model_validator(mode="before")
402
+ @classmethod
403
+ def replace_zero(cls, values): # pylint: disable=no-self-argument
404
+ """Check for zero values and replace with None."""
405
+ return (
406
+ {k: None if v == 0 else v for k, v in values.items()}
407
+ if isinstance(values, dict)
408
+ else values
409
+ )
410
+
411
+
412
+ class IntrinioBalanceSheetFetcher(
413
+ Fetcher[
414
+ IntrinioBalanceSheetQueryParams,
415
+ List[IntrinioBalanceSheetData],
416
+ ]
417
+ ):
418
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
419
+
420
+ @staticmethod
421
+ def transform_query(params: Dict[str, Any]) -> IntrinioBalanceSheetQueryParams:
422
+ """Transform the query params."""
423
+ return IntrinioBalanceSheetQueryParams(**params)
424
+
425
+ @staticmethod
426
+ async def aextract_data(
427
+ query: IntrinioBalanceSheetQueryParams,
428
+ credentials: Optional[Dict[str, str]],
429
+ **kwargs: Any,
430
+ ) -> List[Dict]:
431
+ """Return the raw data from the Intrinio endpoint."""
432
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
433
+ statement_code = "balance_sheet_statement"
434
+ fundamentals_data: Dict = {}
435
+ base_url = "https://api-v2.intrinio.com"
436
+ period = "FY" if query.period == "annual" else "QTR"
437
+ fundamentals_url = (
438
+ f"{base_url}/companies/{query.symbol}/fundamentals?"
439
+ f"statement_code={statement_code}&type={period}"
440
+ )
441
+ if query.fiscal_year is not None:
442
+ if query.fiscal_year < 2008:
443
+ warn("Financials data is only available from 2008 and later.")
444
+ query.fiscal_year = 2008
445
+ fundamentals_url = fundamentals_url + f"&fiscal_year={query.fiscal_year}"
446
+ fundamentals_url = fundamentals_url + f"&api_key={api_key}"
447
+ fundamentals_data = (await get_data_one(fundamentals_url, **kwargs)).get(
448
+ "fundamentals", []
449
+ )
450
+ fiscal_periods = [
451
+ f"{item['fiscal_year']}-{item['fiscal_period']}"
452
+ for item in fundamentals_data
453
+ ]
454
+ fiscal_periods = fiscal_periods[: query.limit]
455
+
456
+ async def callback(response: ClientResponse, _: Any) -> Dict:
457
+ """Return the response."""
458
+ statement_data = await response.json() # type: ignore
459
+ return {
460
+ "period_ending": statement_data["fundamental"]["end_date"], # type: ignore
461
+ "fiscal_year": statement_data["fundamental"]["fiscal_year"], # type: ignore
462
+ "fiscal_period": statement_data["fundamental"]["fiscal_period"], # type: ignore
463
+ "financials": statement_data["standardized_financials"], # type: ignore
464
+ }
465
+
466
+ urls = [
467
+ f"{base_url}/fundamentals/{query.symbol}-{statement_code}-{p}/standardized_financials?api_key={api_key}"
468
+ for p in fiscal_periods
469
+ ]
470
+
471
+ return await amake_requests(urls, callback, **kwargs) # type: ignore
472
+
473
+ @staticmethod
474
+ def transform_data(
475
+ query: IntrinioBalanceSheetQueryParams, data: List[Dict], **kwargs: Any
476
+ ) -> List[IntrinioBalanceSheetData]:
477
+ """Return the transformed data."""
478
+ transformed_data: List[IntrinioBalanceSheetData] = []
479
+ period = "FY" if query.period == "annual" else "QTR"
480
+ units = []
481
+ for item in data:
482
+ sub_dict: Dict[str, Any] = {}
483
+
484
+ for sub_item in item["financials"]:
485
+ field_name = sub_item["data_tag"]["tag"]
486
+ unit = sub_item["data_tag"].get("unit", "")
487
+ if unit and len(unit) == 3:
488
+ units.append(unit)
489
+ sub_dict[field_name] = (
490
+ float(sub_item["value"])
491
+ if sub_item["value"] and sub_item["value"] != 0
492
+ else None
493
+ )
494
+
495
+ sub_dict["period_ending"] = item["period_ending"]
496
+ sub_dict["fiscal_year"] = item["fiscal_year"]
497
+ sub_dict["fiscal_period"] = item["fiscal_period"]
498
+ sub_dict["reported_currency"] = list(set(units))[0]
499
+
500
+ # Intrinio does not return Q4 data but FY data instead
501
+ if period == "QTR" and item["fiscal_period"] == "FY":
502
+ sub_dict["fiscal_period"] = "Q4"
503
+
504
+ transformed_data.append(IntrinioBalanceSheetData(**sub_dict))
505
+
506
+ return transformed_data
openbb_platform/providers/intrinio/openbb_intrinio/models/calendar_ipo.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio IPO Calendar Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from typing import Any, Dict, List, Literal, Optional
6
+
7
+ from openbb_core.provider.abstract.fetcher import Fetcher
8
+ from openbb_core.provider.standard_models.calendar_ipo import (
9
+ CalendarIpoData,
10
+ CalendarIpoQueryParams,
11
+ )
12
+ from openbb_core.provider.utils.errors import EmptyDataError
13
+ from openbb_core.provider.utils.helpers import get_querystring
14
+ from openbb_intrinio.utils.helpers import get_data_one
15
+ from openbb_intrinio.utils.references import IntrinioCompany, IntrinioSecurity
16
+ from pydantic import Field
17
+
18
+
19
+ class IntrinioCalendarIpoQueryParams(CalendarIpoQueryParams):
20
+ """Intrinio IPO Calendar Query.
21
+
22
+ Source: https://docs.intrinio.com/documentation/web_api/get_company_ipos_v2
23
+ """
24
+
25
+ __alias_dict__ = {
26
+ "symbol": "ticker",
27
+ "limit": "page_size",
28
+ "min_value": "offer_amount_greater_than",
29
+ "max_value": "offer_amount_less_than",
30
+ }
31
+
32
+ status: Optional[Literal["upcoming", "priced", "withdrawn"]] = Field(
33
+ description="Status of the IPO. [upcoming, priced, or withdrawn]", default=None
34
+ )
35
+ min_value: Optional[int] = Field(
36
+ description="Return IPOs with an offer dollar amount greater than the given amount.",
37
+ default=None,
38
+ )
39
+ max_value: Optional[int] = Field(
40
+ description="Return IPOs with an offer dollar amount less than the given amount.",
41
+ default=None,
42
+ )
43
+
44
+
45
+ class IntrinioCalendarIpoData(CalendarIpoData):
46
+ """Intrinio IPO Calendar Data."""
47
+
48
+ __alias_dict__ = {"symbol": "ticker", "ipo_date": "date"}
49
+
50
+ status: Optional[Literal["upcoming", "priced", "withdrawn"]] = Field(
51
+ description=(
52
+ "The status of the IPO. Upcoming IPOs have not taken place yet but are expected to. "
53
+ "Priced IPOs have taken place. Withdrawn IPOs were expected to take place, but were subsequently withdrawn."
54
+ ),
55
+ default=None,
56
+ )
57
+ exchange: Optional[str] = Field(
58
+ description=(
59
+ "The acronym of the stock exchange that the company is going to trade publicly on. "
60
+ "Typically NYSE or NASDAQ."
61
+ ),
62
+ default=None,
63
+ )
64
+ offer_amount: Optional[float] = Field(
65
+ description="The total dollar amount of shares offered in the IPO. Typically this is share price * share count",
66
+ default=None,
67
+ )
68
+ share_price: Optional[float] = Field(
69
+ description="The price per share at which the IPO was offered.", default=None
70
+ )
71
+ share_price_lowest: Optional[float] = Field(
72
+ description=(
73
+ "The expected lowest price per share at which the IPO will be offered. "
74
+ "Before an IPO is priced, companies typically provide a range of prices per share at which "
75
+ "they expect to offer the IPO (typically available for upcoming IPOs)."
76
+ ),
77
+ default=None,
78
+ )
79
+ share_price_highest: Optional[float] = Field(
80
+ description=(
81
+ "The expected highest price per share at which the IPO will be offered. "
82
+ "Before an IPO is priced, companies typically provide a range of prices per share at which "
83
+ "they expect to offer the IPO (typically available for upcoming IPOs)."
84
+ ),
85
+ default=None,
86
+ )
87
+ share_count: Optional[int] = Field(
88
+ description="The number of shares offered in the IPO.", default=None
89
+ )
90
+ share_count_lowest: Optional[int] = Field(
91
+ description=(
92
+ "The expected lowest number of shares that will be offered in the IPO. Before an IPO is priced, "
93
+ "companies typically provide a range of shares that they expect to offer in the IPO "
94
+ "(typically available for upcoming IPOs)."
95
+ ),
96
+ default=None,
97
+ )
98
+ share_count_highest: Optional[int] = Field(
99
+ description=(
100
+ "The expected highest number of shares that will be offered in the IPO. Before an IPO is priced, "
101
+ "companies typically provide a range of shares that they expect to offer in the IPO "
102
+ "(typically available for upcoming IPOs)."
103
+ ),
104
+ default=None,
105
+ )
106
+ announcement_url: Optional[str] = Field(
107
+ description="The URL to the company's announcement of the IPO", default=None
108
+ )
109
+ sec_report_url: Optional[str] = Field(
110
+ description=(
111
+ "The URL to the company's S-1, S-1/A, F-1, or F-1/A SEC filing, which is required to be filed "
112
+ "before an IPO takes place."
113
+ ),
114
+ default=None,
115
+ )
116
+ open_price: Optional[float] = Field(
117
+ description="The opening price at the beginning of the first trading day (only available for priced IPOs).",
118
+ default=None,
119
+ )
120
+ close_price: Optional[float] = Field(
121
+ description="The closing price at the end of the first trading day (only available for priced IPOs).",
122
+ default=None,
123
+ )
124
+ volume: Optional[int] = Field(
125
+ description="The volume at the end of the first trading day (only available for priced IPOs).",
126
+ default=None,
127
+ )
128
+ day_change: Optional[float] = Field(
129
+ description=(
130
+ "The percentage change between the open price and the close price on the first trading day "
131
+ "(only available for priced IPOs)."
132
+ ),
133
+ default=None,
134
+ )
135
+ week_change: Optional[float] = Field(
136
+ description=(
137
+ "The percentage change between the open price on the first trading day and the close price approximately "
138
+ "a week after the first trading day (only available for priced IPOs)."
139
+ ),
140
+ default=None,
141
+ )
142
+ month_change: Optional[float] = Field(
143
+ description=(
144
+ "The percentage change between the open price on the first trading day and the close price approximately "
145
+ "a month after the first trading day (only available for priced IPOs)."
146
+ ),
147
+ default=None,
148
+ )
149
+ id: Optional[str] = Field(description="The Intrinio ID of the IPO.", default=None)
150
+ company: Optional[IntrinioCompany] = Field(
151
+ description="The company that is going public via the IPO.", default=None
152
+ )
153
+ security: Optional[IntrinioSecurity] = Field(
154
+ description="The primary Security for the Company that is going public via the IPO",
155
+ default=None,
156
+ )
157
+
158
+
159
+ class IntrinioCalendarIpoFetcher(
160
+ Fetcher[IntrinioCalendarIpoQueryParams, List[IntrinioCalendarIpoData]]
161
+ ):
162
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
163
+
164
+ @staticmethod
165
+ def transform_query(params: Dict[str, Any]) -> IntrinioCalendarIpoQueryParams:
166
+ """Transform the query params."""
167
+ return IntrinioCalendarIpoQueryParams(**params)
168
+
169
+ @staticmethod
170
+ async def aextract_data(
171
+ query: IntrinioCalendarIpoQueryParams,
172
+ credentials: Optional[Dict[str, str]],
173
+ **kwargs: Any,
174
+ ) -> List[Dict]:
175
+ """Return the raw data from the Intrinio endpoint."""
176
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
177
+
178
+ base_url = "https://api-v2.intrinio.com/companies/ipos"
179
+ query_str = get_querystring(query.model_dump(by_alias=True), [])
180
+ url = f"{base_url}?{query_str}&api_key={api_key}"
181
+
182
+ data = await get_data_one(url, **kwargs)
183
+
184
+ return data.get("initial_public_offerings", [])
185
+
186
+ @staticmethod
187
+ def transform_data(
188
+ query: IntrinioCalendarIpoQueryParams, data: List[Dict], **kwargs: Any
189
+ ) -> List[IntrinioCalendarIpoData]:
190
+ """Return the transformed data."""
191
+ if not data:
192
+ raise EmptyDataError("The request was returned empty.")
193
+ return [IntrinioCalendarIpoData.model_validate(d) for d in data]
openbb_platform/providers/intrinio/openbb_intrinio/models/cash_flow.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Cash Flow Statement Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from typing import Any, Dict, List, Literal, Optional
6
+ from warnings import warn
7
+
8
+ from openbb_core.app.model.abstract.error import OpenBBError
9
+ from openbb_core.provider.abstract.fetcher import Fetcher
10
+ from openbb_core.provider.standard_models.cash_flow import (
11
+ CashFlowStatementData,
12
+ CashFlowStatementQueryParams,
13
+ )
14
+ from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
15
+ from openbb_core.provider.utils.helpers import ClientResponse, amake_requests
16
+ from openbb_intrinio.utils.helpers import get_data_one
17
+ from pydantic import Field, field_validator, model_validator
18
+
19
+
20
+ class IntrinioCashFlowStatementQueryParams(CashFlowStatementQueryParams):
21
+ """Intrinio Cash Flow Statement Query.
22
+
23
+ Source: https://docs.intrinio.com/documentation/web_api/get_company_fundamentals_v2
24
+ Source: https://docs.intrinio.com/documentation/web_api/get_fundamental_standardized_financials_v2
25
+ """
26
+
27
+ __json_schema_extra__ = {
28
+ "period": {
29
+ "choices": ["annual", "quarter", "ttm", "ytd"],
30
+ }
31
+ }
32
+
33
+ period: Literal["annual", "quarter", "ttm", "ytd"] = Field(
34
+ default="annual",
35
+ description=QUERY_DESCRIPTIONS.get("period", ""),
36
+ )
37
+ fiscal_year: Optional[int] = Field(
38
+ default=None,
39
+ description="The specific fiscal year. Reports do not go beyond 2008.",
40
+ )
41
+
42
+ @field_validator("symbol", mode="after", check_fields=False)
43
+ @classmethod
44
+ def handle_symbol(cls, v) -> str:
45
+ """Handle symbols with a dash and replace it with a dot for Intrinio."""
46
+ return v.replace("-", ".")
47
+
48
+
49
+ class IntrinioCashFlowStatementData(CashFlowStatementData):
50
+ """Intrinio Cash Flow Statement Data."""
51
+
52
+ __alias_dict__ = {
53
+ "cash_and_equivalents": "cashandequivalents",
54
+ "acquisitions": "acquisitions",
55
+ "amortization_expense": "amortizationexpense",
56
+ "cash_income_taxes_paid": "cashincometaxespaid",
57
+ "cash_interest_paid": "cashinterestpaid",
58
+ "cash_interest_received": "cashinterestreceived",
59
+ "depreciation_expense": "depreciationexpense",
60
+ "divestitures": "divestitures",
61
+ "effect_of_exchange_rate_changes": "effectofexchangeratechanges",
62
+ "changes_in_operating_assets_and_liabilities": "increasedecreaseinoperatingcapital",
63
+ "issuance_of_common_equity": "issuanceofcommonequity",
64
+ "issuance_of_debt": "issuanceofdebt",
65
+ "issuance_of_preferred_equity": "issuanceofpreferredequity",
66
+ "loans_held_for_sale": "loansheldforsalenet",
67
+ "net_cash_from_continuing_financing_activities": "netcashfromcontinuingfinancingactivities",
68
+ "net_cash_from_continuing_investing_activities": "netcashfromcontinuinginvestingactivities",
69
+ "net_cash_from_continuing_operating_activities": "netcashfromcontinuingoperatingactivities",
70
+ "net_cash_from_discontinued_financing_activities": "netcashfromdiscontinuedfinancingactivities",
71
+ "net_cash_from_discontinued_investing_activities": "netcashfromdiscontinuedinvestingactivities",
72
+ "net_cash_from_discontinued_operating_activities": "netcashfromdiscontinuedoperatingactivities",
73
+ "net_cash_from_financing_activities": "netcashfromfinancingactivities",
74
+ "net_cash_from_investing_activities": "netcashfrominvestingactivities",
75
+ "net_cash_from_operating_activities": "netcashfromoperatingactivities",
76
+ "net_change_in_cash_and_equivalents": "netchangeincash",
77
+ "net_change_in_deposits": "netchangeindeposits",
78
+ "net_income": "netincome",
79
+ "net_income_continuing_operations": "netincomecontinuing",
80
+ "net_income_discontinued_operations": "netincomediscontinued",
81
+ "net_increase_in_fed_funds_sold": "netincreaseinfedfundssold",
82
+ "non_cash_adjustments_to_reconcile_net_income": "noncashadjustmentstonetincome",
83
+ "other_financing_activities": "otherfinancingactivitiesnet",
84
+ "other_investing_activities": "otherinvestingactivitiesnet",
85
+ "other_net_changes_in_cash": "othernetchangesincash",
86
+ "payment_of_dividends": "paymentofdividends",
87
+ "provision_for_loan_losses": "provisionforloanlosses",
88
+ "purchase_of_investments": "purchaseofinvestments",
89
+ "purchase_of_investment_securities": "purchaseofinvestments",
90
+ "purchase_of_property_plant_and_equipment": "purchaseofplantpropertyandequipment",
91
+ "repayment_of_debt": "repaymentofdebt",
92
+ "repurchase_of_common_equity": "repurchaseofcommonequity",
93
+ "repurchase_of_preferred_equity": "repurchaseofpreferredequity",
94
+ "sale_and_maturity_of_investments": "saleofinvestments",
95
+ "sale_of_property_plant_and_equipment": "saleofplantpropertyandequipment",
96
+ }
97
+
98
+ reported_currency: Optional[str] = Field(
99
+ description="The currency in which the balance sheet is reported.",
100
+ default=None,
101
+ )
102
+ net_income_continuing_operations: Optional[float] = Field(
103
+ default=None, description="Net Income (Continuing Operations)"
104
+ )
105
+ net_income_discontinued_operations: Optional[float] = Field(
106
+ default=None, description="Net Income (Discontinued Operations)"
107
+ )
108
+ net_income: Optional[float] = Field(
109
+ default=None, description="Consolidated Net Income."
110
+ )
111
+ provision_for_loan_losses: Optional[float] = Field(
112
+ default=None, description="Provision for Loan Losses"
113
+ )
114
+ provision_for_credit_losses: Optional[float] = Field(
115
+ default=None, description="Provision for credit losses"
116
+ )
117
+ depreciation_expense: Optional[float] = Field(
118
+ default=None, description="Depreciation Expense."
119
+ )
120
+ amortization_expense: Optional[float] = Field(
121
+ default=None, description="Amortization Expense."
122
+ )
123
+ share_based_compensation: Optional[float] = Field(
124
+ default=None, description="Share-based compensation."
125
+ )
126
+ non_cash_adjustments_to_reconcile_net_income: Optional[float] = Field(
127
+ default=None, description="Non-Cash Adjustments to Reconcile Net Income."
128
+ )
129
+ changes_in_operating_assets_and_liabilities: Optional[float] = Field(
130
+ default=None, description="Changes in Operating Assets and Liabilities (Net)"
131
+ )
132
+ net_cash_from_continuing_operating_activities: Optional[float] = Field(
133
+ default=None, description="Net Cash from Continuing Operating Activities"
134
+ )
135
+ net_cash_from_discontinued_operating_activities: Optional[float] = Field(
136
+ default=None, description="Net Cash from Discontinued Operating Activities"
137
+ )
138
+ net_cash_from_operating_activities: Optional[float] = Field(
139
+ default=None, description="Net Cash from Operating Activities"
140
+ )
141
+ divestitures: Optional[float] = Field(default=None, description="Divestitures")
142
+ sale_of_property_plant_and_equipment: Optional[float] = Field(
143
+ default=None, description="Sale of Property, Plant, and Equipment"
144
+ )
145
+ acquisitions: Optional[float] = Field(default=None, description="Acquisitions")
146
+ purchase_of_investments: Optional[float] = Field(
147
+ default=None, description="Purchase of Investments"
148
+ )
149
+ purchase_of_investment_securities: Optional[float] = Field(
150
+ default=None, description="Purchase of Investment Securities"
151
+ )
152
+ sale_and_maturity_of_investments: Optional[float] = Field(
153
+ default=None, description="Sale and Maturity of Investments"
154
+ )
155
+ loans_held_for_sale: Optional[float] = Field(
156
+ default=None, description="Loans Held for Sale (Net)"
157
+ )
158
+ purchase_of_property_plant_and_equipment: Optional[float] = Field(
159
+ default=None, description="Purchase of Property, Plant, and Equipment"
160
+ )
161
+ other_investing_activities: Optional[float] = Field(
162
+ default=None, description="Other Investing Activities (Net)"
163
+ )
164
+ net_cash_from_continuing_investing_activities: Optional[float] = Field(
165
+ default=None, description="Net Cash from Continuing Investing Activities"
166
+ )
167
+ net_cash_from_discontinued_investing_activities: Optional[float] = Field(
168
+ default=None, description="Net Cash from Discontinued Investing Activities"
169
+ )
170
+ net_cash_from_investing_activities: Optional[float] = Field(
171
+ default=None, description="Net Cash from Investing Activities"
172
+ )
173
+ payment_of_dividends: Optional[float] = Field(
174
+ default=None, description="Payment of Dividends"
175
+ )
176
+ repurchase_of_common_equity: Optional[float] = Field(
177
+ default=None, description="Repurchase of Common Equity"
178
+ )
179
+ repurchase_of_preferred_equity: Optional[float] = Field(
180
+ default=None, description="Repurchase of Preferred Equity"
181
+ )
182
+ issuance_of_common_equity: Optional[float] = Field(
183
+ default=None, description="Issuance of Common Equity"
184
+ )
185
+ issuance_of_preferred_equity: Optional[float] = Field(
186
+ default=None, description="Issuance of Preferred Equity"
187
+ )
188
+ issuance_of_debt: Optional[float] = Field(
189
+ default=None, description="Issuance of Debt"
190
+ )
191
+ repayment_of_debt: Optional[float] = Field(
192
+ default=None, description="Repayment of Debt"
193
+ )
194
+ other_financing_activities: Optional[float] = Field(
195
+ default=None, description="Other Financing Activities (Net)"
196
+ )
197
+ cash_interest_received: Optional[float] = Field(
198
+ default=None, description="Cash Interest Received"
199
+ )
200
+ net_change_in_deposits: Optional[float] = Field(
201
+ default=None, description="Net Change in Deposits"
202
+ )
203
+ net_increase_in_fed_funds_sold: Optional[float] = Field(
204
+ default=None, description="Net Increase in Fed Funds Sold"
205
+ )
206
+ net_cash_from_continuing_financing_activities: Optional[float] = Field(
207
+ default=None, description="Net Cash from Continuing Financing Activities"
208
+ )
209
+ net_cash_from_discontinued_financing_activities: Optional[float] = Field(
210
+ default=None, description="Net Cash from Discontinued Financing Activities"
211
+ )
212
+ net_cash_from_financing_activities: Optional[float] = Field(
213
+ default=None, description="Net Cash from Financing Activities"
214
+ )
215
+ effect_of_exchange_rate_changes: Optional[float] = Field(
216
+ default=None, description="Effect of Exchange Rate Changes"
217
+ )
218
+ other_net_changes_in_cash: Optional[float] = Field(
219
+ default=None, description="Other Net Changes in Cash"
220
+ )
221
+ net_change_in_cash_and_equivalents: Optional[float] = Field(
222
+ default=None, description="Net Change in Cash and Equivalents"
223
+ )
224
+ cash_income_taxes_paid: Optional[float] = Field(
225
+ default=None, description="Cash Income Taxes Paid"
226
+ )
227
+ cash_interest_paid: Optional[float] = Field(
228
+ default=None, description="Cash Interest Paid"
229
+ )
230
+
231
+ @model_validator(mode="before")
232
+ @classmethod
233
+ def replace_zero(cls, values):
234
+ """Check for zero values and replace with None."""
235
+ return (
236
+ {k: None if v == 0 else v for k, v in values.items()}
237
+ if isinstance(values, dict)
238
+ else values
239
+ )
240
+
241
+
242
+ class IntrinioCashFlowStatementFetcher(
243
+ Fetcher[
244
+ IntrinioCashFlowStatementQueryParams,
245
+ List[IntrinioCashFlowStatementData],
246
+ ]
247
+ ):
248
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
249
+
250
+ @staticmethod
251
+ def transform_query(params: Dict[str, Any]) -> IntrinioCashFlowStatementQueryParams:
252
+ """Transform the query params."""
253
+ return IntrinioCashFlowStatementQueryParams(**params)
254
+
255
+ @staticmethod
256
+ async def aextract_data(
257
+ query: IntrinioCashFlowStatementQueryParams,
258
+ credentials: Optional[Dict[str, str]],
259
+ **kwargs: Any,
260
+ ) -> List[Dict]:
261
+ """Return the raw data from the Intrinio endpoint."""
262
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
263
+ statement_code = "cash_flow_statement"
264
+ if query.period in ["quarter", "annual"]:
265
+ period_type = "FY" if query.period == "annual" else "QTR"
266
+ elif query.period in ["ttm", "ytd"]:
267
+ period_type = query.period.upper()
268
+ else:
269
+ raise OpenBBError(f"Period '{query.period}' not supported.")
270
+
271
+ base_url = "https://api-v2.intrinio.com"
272
+ fundamentals_url = (
273
+ f"{base_url}/companies/{query.symbol}/fundamentals?"
274
+ f"statement_code={statement_code}&type={period_type}"
275
+ )
276
+ if query.fiscal_year is not None:
277
+ if query.fiscal_year < 2008:
278
+ warn("Financials data is only available from 2008 and later.")
279
+ query.fiscal_year = 2008
280
+ fundamentals_url = fundamentals_url + f"&fiscal_year={query.fiscal_year}"
281
+ fundamentals_url = fundamentals_url + f"&api_key={api_key}"
282
+ fundamentals_data = (await get_data_one(fundamentals_url, **kwargs)).get(
283
+ "fundamentals", []
284
+ )
285
+
286
+ fiscal_periods = [
287
+ f"{item['fiscal_year']}-{item['fiscal_period']}"
288
+ for item in fundamentals_data
289
+ ]
290
+ fiscal_periods = fiscal_periods[: query.limit]
291
+
292
+ async def callback(response: ClientResponse, _: Any) -> Dict:
293
+ """Return the response."""
294
+ statement_data = await response.json()
295
+ return {
296
+ "period_ending": statement_data["fundamental"]["end_date"], # type: ignore
297
+ "fiscal_period": statement_data["fundamental"]["fiscal_period"], # type: ignore
298
+ "fiscal_year": statement_data["fundamental"]["fiscal_year"], # type: ignore
299
+ "financials": statement_data["standardized_financials"], # type: ignore
300
+ }
301
+
302
+ intrinio_id = f"{query.symbol}-{statement_code}"
303
+ urls = [
304
+ f"{base_url}/fundamentals/{intrinio_id}-{period}/standardized_financials?api_key={api_key}"
305
+ for period in fiscal_periods
306
+ ]
307
+
308
+ return await amake_requests(urls, callback, **kwargs)
309
+
310
+ @staticmethod
311
+ def transform_data(
312
+ query: IntrinioCashFlowStatementQueryParams, data: List[Dict], **kwargs: Any
313
+ ) -> List[IntrinioCashFlowStatementData]:
314
+ """Return the transformed data."""
315
+ transformed_data: List[IntrinioCashFlowStatementData] = []
316
+ units = []
317
+ for item in data:
318
+ sub_dict: Dict[str, Any] = {}
319
+
320
+ for sub_item in item["financials"]:
321
+ unit = sub_item["data_tag"].get("unit", "")
322
+ if unit and len(unit) == 3:
323
+ units.append(unit)
324
+ field_name = sub_item["data_tag"]["tag"]
325
+ sub_dict[field_name] = (
326
+ float(sub_item["value"])
327
+ if sub_item["value"] and sub_item["value"] != 0
328
+ else None
329
+ )
330
+
331
+ sub_dict["period_ending"] = item["period_ending"]
332
+ sub_dict["fiscal_year"] = item["fiscal_year"]
333
+ sub_dict["fiscal_period"] = item["fiscal_period"]
334
+ sub_dict["reported_currency"] = list(set(units))[0]
335
+
336
+ transformed_data.append(IntrinioCashFlowStatementData(**sub_dict))
337
+
338
+ return transformed_data
openbb_platform/providers/intrinio/openbb_intrinio/models/company_filings.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Company Filings Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from datetime import (
6
+ date as dateType,
7
+ datetime,
8
+ )
9
+ from typing import Any, Optional
10
+
11
+ from dateutil.relativedelta import relativedelta
12
+ from openbb_core.provider.abstract.annotated_result import AnnotatedResult
13
+ from openbb_core.provider.abstract.fetcher import Fetcher
14
+ from openbb_core.provider.standard_models.company_filings import (
15
+ CompanyFilingsData,
16
+ CompanyFilingsQueryParams,
17
+ )
18
+ from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
19
+ from pydantic import Field, field_validator
20
+
21
+
22
+ class IntrinioCompanyFilingsQueryParams(CompanyFilingsQueryParams):
23
+ """Intrinio Company Filings Query.
24
+
25
+ Source: https://docs.intrinio.com/documentation/web_api/get_company_filings_v2
26
+ """
27
+
28
+ __alias_dict__ = {"form_type": "report_type", "limit": "page_size"}
29
+
30
+ form_type: Optional[str] = Field(
31
+ default=None, description="SEC form type to filter by."
32
+ )
33
+ start_date: Optional[dateType] = Field(
34
+ default=None,
35
+ description=QUERY_DESCRIPTIONS["start_date"],
36
+ )
37
+ end_date: Optional[dateType] = Field(
38
+ default=None,
39
+ description=QUERY_DESCRIPTIONS["end_date"],
40
+ )
41
+ limit: Optional[int] = Field(
42
+ default=None,
43
+ description=QUERY_DESCRIPTIONS["limit"],
44
+ )
45
+ thea_enabled: Optional[bool] = Field(
46
+ default=None,
47
+ description="Return filings that have been read by Intrinio's Thea NLP.",
48
+ )
49
+
50
+ @field_validator("symbol", mode="before", check_fields=False)
51
+ @classmethod
52
+ def _validate_symbol(cls, v):
53
+ """Validate symbol."""
54
+ if not v:
55
+ raise ValueError("Symbol is required for Intrinio.")
56
+ return v
57
+
58
+
59
+ class IntrinioCompanyFilingsData(CompanyFilingsData):
60
+ """Intrinio Company Filings Data."""
61
+
62
+ id: str = Field(description="Intrinio ID of the filing.")
63
+ period_end_date: Optional[dateType] = Field(
64
+ default=None,
65
+ description="Ending date of the fiscal period for the filing.",
66
+ )
67
+ accepted_date: Optional[datetime] = Field(
68
+ default=None, description="Accepted date of the filing."
69
+ )
70
+ sec_unique_id: str = Field(description="SEC unique ID of the filing.")
71
+ filing_url: Optional[str] = Field(
72
+ default=None, description="URL to the filing page."
73
+ )
74
+ instance_url: Optional[str] = Field(
75
+ default=None,
76
+ description="URL for the XBRL filing for the report.",
77
+ )
78
+ industry_group: str = Field(description="Industry group of the company.")
79
+ industry_category: str = Field(description="Industry category of the company.")
80
+ word_count: Optional[int] = Field(
81
+ default=None, description="Number of words in the filing, if available."
82
+ )
83
+
84
+
85
+ class IntrinioCompanyFilingsFetcher(
86
+ Fetcher[
87
+ IntrinioCompanyFilingsQueryParams,
88
+ list[IntrinioCompanyFilingsData],
89
+ ]
90
+ ):
91
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
92
+
93
+ @staticmethod
94
+ def transform_query(params: dict[str, Any]) -> IntrinioCompanyFilingsQueryParams:
95
+ """Transform the query."""
96
+ transformed_params = params
97
+
98
+ now = datetime.now().date()
99
+ if params.get("start_date") is None and params.get("form_type") is None:
100
+ transformed_params["start_date"] = now - relativedelta(years=1)
101
+ if params.get("end_date") is None and params.get("form_type") is None:
102
+ transformed_params["end_date"] = now
103
+
104
+ return IntrinioCompanyFilingsQueryParams(**transformed_params)
105
+
106
+ @staticmethod
107
+ async def aextract_data(
108
+ query: IntrinioCompanyFilingsQueryParams,
109
+ credentials: Optional[dict[str, str]],
110
+ **kwargs: Any,
111
+ ) -> dict:
112
+ """Return the raw data from the Intrinio endpoint."""
113
+ # pylint: disable=import-outside-toplevel
114
+ from openbb_core.provider.utils.errors import EmptyDataError, OpenBBError
115
+ from openbb_core.provider.utils.helpers import (
116
+ get_async_requests_session,
117
+ get_querystring,
118
+ )
119
+
120
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
121
+
122
+ base_url = "https://api-v2.intrinio.com/companies"
123
+ query_str = get_querystring(
124
+ query.model_dump(by_alias=True), ["symbol", "limit", "page_size"]
125
+ )
126
+ url = f"{base_url}/{query.symbol}/filings?{query_str}&page_size={query.limit or 10000}&api_key={api_key}"
127
+ results: list = []
128
+ metadata: dict = {}
129
+ session = await get_async_requests_session()
130
+
131
+ async with await session.get(url) as response:
132
+ if response.status != 200:
133
+ raise OpenBBError(
134
+ f"Error fetching data from Intrinio: {response.status} -> {response.text}"
135
+ )
136
+ result = await response.json()
137
+ if filings := result.get("filings", []):
138
+ results.extend(filings)
139
+
140
+ metadata = result.get("company", {})
141
+
142
+ while next_page := result.get("next_page"):
143
+ url += f"&next_page={next_page}"
144
+ async with await session.get(url) as next_response:
145
+ if response.status != 200:
146
+ raise OpenBBError(
147
+ f"Error fetching data from Intrinio: {response.status} -> {response.text}"
148
+ )
149
+ result = await next_response.json()
150
+ if filings := result.get("filings", []):
151
+ results.extend(filings)
152
+
153
+ if not results:
154
+ raise EmptyDataError("No data was returned for the symbol provided.")
155
+
156
+ return {"data": results, "metadata": metadata}
157
+
158
+ @staticmethod
159
+ def transform_data(
160
+ query: IntrinioCompanyFilingsQueryParams, data: dict, **kwargs: Any
161
+ ) -> AnnotatedResult[list[IntrinioCompanyFilingsData]]:
162
+ """Return the transformed data."""
163
+ return AnnotatedResult(
164
+ result=[
165
+ IntrinioCompanyFilingsData.model_validate(
166
+ {
167
+ k: v
168
+ for k, v in d.items()
169
+ if k not in ["thea_enabled", "earnings_release"]
170
+ }
171
+ )
172
+ for d in data.get("data", [])
173
+ ],
174
+ metadata=data.get("metadata", {}),
175
+ )
openbb_platform/providers/intrinio/openbb_intrinio/models/company_news.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Company News Model."""
2
+
3
+ import asyncio
4
+ from datetime import datetime, timedelta
5
+ from typing import Any, Dict, List, Literal, Optional
6
+ from warnings import warn
7
+
8
+ from openbb_core.app.model.abstract.error import OpenBBError
9
+ from openbb_core.provider.abstract.fetcher import Fetcher
10
+ from openbb_core.provider.standard_models.company_news import (
11
+ CompanyNewsData,
12
+ CompanyNewsQueryParams,
13
+ )
14
+ from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
15
+ from openbb_core.provider.utils.helpers import (
16
+ amake_request,
17
+ get_querystring,
18
+ )
19
+ from openbb_intrinio.utils.helpers import get_data
20
+ from openbb_intrinio.utils.references import IntrinioSecurity
21
+ from pydantic import Field, field_validator
22
+
23
+
24
+ class IntrinioCompanyNewsQueryParams(CompanyNewsQueryParams):
25
+ """Intrinio Company News Query.
26
+
27
+ Source: https://docs.intrinio.com/documentation/web_api/get_company_news_v2
28
+ """
29
+
30
+ __alias_dict__ = {
31
+ "limit": "page_size",
32
+ "source": "specific_source",
33
+ }
34
+ __json_schema_extra__ = {
35
+ "symbol": {"multiple_items_allowed": True},
36
+ "source": {
37
+ "choices": ["yahoo", "moody", "moody_us_news", "moody_us_press_releases"]
38
+ },
39
+ "sentiment": {"choices": ["positive", "neutral", "negative"]},
40
+ }
41
+
42
+ source: Optional[
43
+ Literal["yahoo", "moody", "moody_us_news", "moody_us_press_releases"]
44
+ ] = Field(
45
+ default=None,
46
+ description="The source of the news article.",
47
+ )
48
+ sentiment: Optional[Literal["positive", "neutral", "negative"]] = Field(
49
+ default=None,
50
+ description="Return news only from this source.",
51
+ )
52
+ language: Optional[str] = Field(
53
+ default=None,
54
+ description="Filter by language. Unsupported for yahoo source.",
55
+ )
56
+ topic: Optional[str] = Field(
57
+ default=None,
58
+ description="Filter by topic. Unsupported for yahoo source.",
59
+ )
60
+ word_count_greater_than: Optional[int] = Field(
61
+ default=None,
62
+ description="News stories will have a word count greater than this value."
63
+ + " Unsupported for yahoo source.",
64
+ )
65
+ word_count_less_than: Optional[int] = Field(
66
+ default=None,
67
+ description="News stories will have a word count less than this value."
68
+ + " Unsupported for yahoo source.",
69
+ )
70
+ is_spam: Optional[bool] = Field(
71
+ default=None,
72
+ description="Filter whether it is marked as spam or not."
73
+ + " Unsupported for yahoo source.",
74
+ )
75
+ business_relevance_greater_than: Optional[float] = Field(
76
+ default=None,
77
+ ge=0,
78
+ le=1,
79
+ description="News stories will have a business relevance score more than this value."
80
+ + " Unsupported for yahoo source. Value is a decimal between 0 and 1.",
81
+ )
82
+ business_relevance_less_than: Optional[float] = Field(
83
+ default=None,
84
+ ge=0,
85
+ le=1,
86
+ description="News stories will have a business relevance score less than this value."
87
+ + " Unsupported for yahoo source. Value is a decimal between 0 and 1.",
88
+ )
89
+
90
+ @field_validator("symbol", mode="before", check_fields=False)
91
+ @classmethod
92
+ def _symbol_mandatory(cls, v):
93
+ """Symbol mandatory validator."""
94
+ if not v:
95
+ raise OpenBBError("Required field missing -> symbol")
96
+ return v
97
+
98
+
99
+ class IntrinioCompanyNewsData(CompanyNewsData):
100
+ """Intrinio Company News Data."""
101
+
102
+ __alias_dict__ = {
103
+ "date": "publication_date",
104
+ "sentiment": "article_sentiment",
105
+ "sentiment_confidence": "article_sentiment_confidence",
106
+ "symbols": "symbol",
107
+ }
108
+ source: Optional[str] = Field(
109
+ default=None,
110
+ description="The source of the news article.",
111
+ )
112
+ summary: Optional[str] = Field(
113
+ default=None,
114
+ description="The summary of the news article.",
115
+ )
116
+ topics: Optional[str] = Field(
117
+ default=None,
118
+ description="The topics related to the news article.",
119
+ )
120
+ word_count: Optional[int] = Field(
121
+ default=None,
122
+ description="The word count of the news article.",
123
+ )
124
+ business_relevance: Optional[float] = Field(
125
+ default=None,
126
+ description=" How strongly correlated the news article is to the business",
127
+ )
128
+ sentiment: Optional[str] = Field(
129
+ default=None,
130
+ description="The sentiment of the news article - i.e, negative, positive.",
131
+ )
132
+ sentiment_confidence: Optional[float] = Field(
133
+ default=None,
134
+ description="The confidence score of the sentiment rating.",
135
+ )
136
+ language: Optional[str] = Field(
137
+ default=None,
138
+ description="The language of the news article.",
139
+ )
140
+ spam: Optional[bool] = Field(
141
+ default=None,
142
+ description="Whether the news article is spam.",
143
+ )
144
+ copyright: Optional[str] = Field(
145
+ default=None,
146
+ description="The copyright notice of the news article.",
147
+ )
148
+ id: str = Field(description="Article ID.")
149
+ security: Optional[IntrinioSecurity] = Field(
150
+ default=None,
151
+ description="The Intrinio Security object. Contains the security details related to the news article.",
152
+ )
153
+
154
+ @field_validator("publication_date", mode="before", check_fields=False)
155
+ @classmethod
156
+ def date_validate(cls, v):
157
+ """Return the date as a datetime object."""
158
+ return datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.000Z")
159
+
160
+ @field_validator("topics", mode="before", check_fields=False)
161
+ @classmethod
162
+ def topics_validate(cls, v):
163
+ """Parse the topics as a string."""
164
+ if v:
165
+ topics = [t.get("name") for t in v if t and t not in ["", " "]]
166
+ return ", ".join(topics)
167
+ return None
168
+
169
+ @field_validator("copyright", mode="before", check_fields=False)
170
+ @classmethod
171
+ def copyright_validate(cls, v):
172
+ """Clean empty strings."""
173
+ return None if v in ["", " "] else v
174
+
175
+
176
+ class IntrinioCompanyNewsFetcher(
177
+ Fetcher[
178
+ IntrinioCompanyNewsQueryParams,
179
+ List[IntrinioCompanyNewsData],
180
+ ]
181
+ ):
182
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
183
+
184
+ @staticmethod
185
+ def transform_query(params: Dict[str, Any]) -> IntrinioCompanyNewsQueryParams:
186
+ """Transform the query params."""
187
+ transformed_params = params
188
+ if not transformed_params.get("start_date"):
189
+ transformed_params["start_date"] = (
190
+ datetime.now() - timedelta(days=365)
191
+ ).date()
192
+ if not transformed_params.get("end_date"):
193
+ transformed_params["end_date"] = (datetime.now() + timedelta(days=1)).date()
194
+ if transformed_params["start_date"] == transformed_params["end_date"]:
195
+ transformed_params["end_date"] = (
196
+ transformed_params["end_date"] + timedelta(days=1)
197
+ ).date()
198
+ return IntrinioCompanyNewsQueryParams(**transformed_params)
199
+
200
+ @staticmethod
201
+ async def aextract_data(
202
+ query: IntrinioCompanyNewsQueryParams,
203
+ credentials: Optional[Dict[str, str]],
204
+ **kwargs: Any,
205
+ ) -> List[Dict]:
206
+ """Return the raw data from the Intrinio endpoint."""
207
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
208
+
209
+ base_url = "https://api-v2.intrinio.com/companies"
210
+ ignore = (
211
+ ["symbol", "page_size", "is_spam"]
212
+ if not query.source or query.source == "yahoo"
213
+ else ["symbol", "page_size"]
214
+ )
215
+ query_str = get_querystring(query.model_dump(by_alias=True), ignore)
216
+ symbols = query.symbol.split(",") if query.symbol else []
217
+ news: List = []
218
+
219
+ async def callback(response, session):
220
+ """Response callback."""
221
+ result = await response.json()
222
+
223
+ if isinstance(result, dict) and "error" in result:
224
+ if "api key" in result.get("message", "").lower():
225
+ raise UnauthorizedError(
226
+ f"Unauthorized Intrinio request -> {result.get('message')}"
227
+ )
228
+ raise OpenBBError(f"Error in Intrinio request -> {result}")
229
+
230
+ symbol = response.url.parts[-2]
231
+ _data = result.get("news", [])
232
+ data = []
233
+ data.extend([{"symbol": symbol, **d} for d in _data])
234
+ articles = len(data)
235
+ next_page = result.get("next_page")
236
+ # query.limit can be None...
237
+ limit = query.limit or 2500
238
+ while next_page and limit > articles:
239
+ url = (
240
+ f"{base_url}/{symbol}/news?{query_str}"
241
+ + f"&page_size={query.limit}&api_key={api_key}&next_page={next_page}"
242
+ )
243
+ result = await get_data(url, session=session, **kwargs)
244
+ _data = result.get("news", [])
245
+ if _data:
246
+ data.extend([{"symbol": symbol, **d} for d in _data])
247
+ articles = len(data)
248
+ next_page = result.get("next_page")
249
+ return data
250
+
251
+ seen = set()
252
+
253
+ async def get_one(symbol):
254
+ """Get the data for one symbol."""
255
+ url = f"{base_url}/{symbol}/news?{query_str}&page_size={query.limit}&api_key={api_key}"
256
+ data = await amake_request(url, response_callback=callback, **kwargs)
257
+ if not data:
258
+ warn(f"No data found for: {symbol}")
259
+ if data:
260
+ data = [x for x in data if not (x["url"] in seen or seen.add(x["url"]))] # type: ignore
261
+ news.extend(
262
+ sorted(data, key=lambda x: x["publication_date"], reverse=True)[
263
+ : query.limit
264
+ ]
265
+ )
266
+
267
+ tasks = [get_one(symbol) for symbol in symbols]
268
+
269
+ await asyncio.gather(*tasks)
270
+
271
+ if not news:
272
+ raise EmptyDataError(
273
+ "Error: The request was returned as empty."
274
+ + " Try adjusting the requested date ranges, if applicable."
275
+ )
276
+
277
+ return news
278
+
279
+ # pylint: disable=unused-argument
280
+ @staticmethod
281
+ def transform_data(
282
+ query: IntrinioCompanyNewsQueryParams, data: List[Dict], **kwargs: Any
283
+ ) -> List[IntrinioCompanyNewsData]:
284
+ """Return the transformed data."""
285
+ results: List[IntrinioCompanyNewsData] = []
286
+ for item in data:
287
+ body = item.get("body", {})
288
+ if not body:
289
+ item["text"] = item.pop("summary")
290
+ if body:
291
+ _ = item.pop("body")
292
+ item["publication_date"] = body.get("publication_date", None)
293
+ item["text"] = body.get("body", None)
294
+ results.append(IntrinioCompanyNewsData.model_validate(item))
295
+ return results
openbb_platform/providers/intrinio/openbb_intrinio/models/currency_pairs.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Currency Available Pairs Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from openbb_core.provider.abstract.fetcher import Fetcher
8
+ from openbb_core.provider.standard_models.currency_pairs import (
9
+ CurrencyPairsData,
10
+ CurrencyPairsQueryParams,
11
+ )
12
+ from openbb_core.provider.utils.errors import EmptyDataError
13
+ from pydantic import Field
14
+
15
+
16
+ class IntrinioCurrencyPairsQueryParams(CurrencyPairsQueryParams):
17
+ """Intrinio Currency Available Pairs Query.
18
+
19
+ Source: https://docs.intrinio.com/documentation/web_api/get_forex_pairs_v2
20
+ """
21
+
22
+
23
+ class IntrinioCurrencyPairsData(CurrencyPairsData):
24
+ """Intrinio Currency Available Pairs Data."""
25
+
26
+ __alias_dict__ = {"symbol": "code"}
27
+
28
+ base_currency: str = Field(
29
+ description="ISO 4217 currency code of the base currency."
30
+ )
31
+ quote_currency: str = Field(
32
+ description="ISO 4217 currency code of the quote currency."
33
+ )
34
+
35
+
36
+ class IntrinioCurrencyPairsFetcher(
37
+ Fetcher[
38
+ IntrinioCurrencyPairsQueryParams,
39
+ List[IntrinioCurrencyPairsData],
40
+ ]
41
+ ):
42
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
43
+
44
+ @staticmethod
45
+ def transform_query(params: Dict[str, Any]) -> IntrinioCurrencyPairsQueryParams:
46
+ """Transform the query params."""
47
+ return IntrinioCurrencyPairsQueryParams(**params)
48
+
49
+ @staticmethod
50
+ async def aextract_data(
51
+ query: IntrinioCurrencyPairsQueryParams,
52
+ credentials: Optional[Dict[str, str]],
53
+ **kwargs: Any,
54
+ ) -> List[Dict]:
55
+ """Return the raw data from the Intrinio endpoint."""
56
+ # pylint: disable=import-outside-toplevel
57
+ from openbb_intrinio.utils.helpers import get_data_many
58
+
59
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
60
+
61
+ base_url = "https://api-v2.intrinio.com"
62
+ url = f"{base_url}/forex/pairs?api_key={api_key}"
63
+ return await get_data_many(url, "pairs", **kwargs)
64
+
65
+ @staticmethod
66
+ def transform_data(
67
+ query: IntrinioCurrencyPairsQueryParams, data: List[Dict], **kwargs: Any
68
+ ) -> List[IntrinioCurrencyPairsData]:
69
+ """Return the transformed data."""
70
+ # pylint: disable=import-outside-toplevel
71
+ from pandas import DataFrame
72
+
73
+ if not data:
74
+ raise EmptyDataError("The request was returned empty.")
75
+ df = DataFrame(data)
76
+ if query.query:
77
+ df = df[
78
+ df["code"].str.contains(query.query, case=False)
79
+ | df["base_currency"].str.contains(query.query, case=False)
80
+ | df["quote_currency"].str.contains(query.query, case=False)
81
+ ]
82
+ if len(df) == 0:
83
+ raise EmptyDataError(
84
+ f"No results were found with the query supplied. -> {query.query}"
85
+ + " Hint: Names and descriptions are not searchable from Intrinio, try 3-letter symbols."
86
+ )
87
+ return [
88
+ IntrinioCurrencyPairsData.model_validate(d)
89
+ for d in df.to_dict(orient="records")
90
+ ]
openbb_platform/providers/intrinio/openbb_intrinio/models/equity_historical.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Equity Historical Price Model."""
2
+
3
+ # pylint: disable = unused-argument
4
+
5
+ from datetime import datetime, time
6
+ from typing import Any, Dict, List, Literal, Optional
7
+
8
+ from dateutil.relativedelta import relativedelta
9
+ from openbb_core.app.model.abstract.error import OpenBBError
10
+ from openbb_core.provider.abstract.fetcher import Fetcher
11
+ from openbb_core.provider.standard_models.equity_historical import (
12
+ EquityHistoricalData,
13
+ EquityHistoricalQueryParams,
14
+ )
15
+ from openbb_core.provider.utils.descriptions import (
16
+ DATA_DESCRIPTIONS,
17
+ QUERY_DESCRIPTIONS,
18
+ )
19
+ from openbb_core.provider.utils.errors import EmptyDataError
20
+ from openbb_core.provider.utils.helpers import (
21
+ ClientResponse,
22
+ ClientSession,
23
+ amake_request,
24
+ get_querystring,
25
+ )
26
+ from pydantic import Field, PrivateAttr, model_validator
27
+
28
+
29
+ class IntrinioEquityHistoricalQueryParams(EquityHistoricalQueryParams):
30
+ """Intrinio Equity Historical Price Query.
31
+
32
+ Source: https://docs.intrinio.com/documentation/web_api/get_security_interval_prices_v2
33
+ """
34
+
35
+ __json_schema_extra__ = {
36
+ "interval": {
37
+ "choices": [
38
+ "1m",
39
+ "5m",
40
+ "10m",
41
+ "15m",
42
+ "30m",
43
+ "60m",
44
+ "1h",
45
+ "1d",
46
+ "1W",
47
+ "1M",
48
+ "1Q",
49
+ "1Y",
50
+ ],
51
+ },
52
+ }
53
+
54
+ symbol: str = Field(
55
+ description="A Security identifier (Ticker, FIGI, ISIN, CUSIP, Intrinio ID)."
56
+ )
57
+ interval: Literal[
58
+ "1m", "5m", "10m", "15m", "30m", "60m", "1h", "1d", "1W", "1M", "1Q", "1Y"
59
+ ] = Field(default="1d", description=QUERY_DESCRIPTIONS.get("interval", ""))
60
+ start_time: Optional[time] = Field(
61
+ default=None,
62
+ description="Return intervals starting at the specified time on the `start_date` formatted as 'HH:MM:SS'.",
63
+ )
64
+ end_time: Optional[time] = Field(
65
+ default=None,
66
+ description="Return intervals stopping at the specified time on the `end_date` formatted as 'HH:MM:SS'.",
67
+ )
68
+ timezone: Optional[str] = Field(
69
+ default="America/New_York",
70
+ description="Timezone of the data, in the IANA format (Continent/City).",
71
+ )
72
+ source: Literal["realtime", "delayed", "nasdaq_basic"] = Field(
73
+ default="realtime", description="The source of the data."
74
+ )
75
+ _interval_size: Literal["1m", "5m", "10m", "15m", "30m", "60m", "1h"] = PrivateAttr(
76
+ default=None
77
+ )
78
+ _frequency: Literal["daily", "weekly", "monthly", "quarterly", "yearly"] = (
79
+ PrivateAttr(default=None)
80
+ )
81
+
82
+ # pylint: disable=protected-access
83
+ @model_validator(mode="after")
84
+ @classmethod
85
+ def set_time_params(cls, values: "IntrinioEquityHistoricalQueryParams"):
86
+ """Set the default start & end date and time params for Intrinio API."""
87
+ frequency_dict = {
88
+ "1d": "daily",
89
+ "1W": "weekly",
90
+ "1M": "monthly",
91
+ "1Q": "quarterly",
92
+ "1Y": "yearly",
93
+ }
94
+
95
+ if values.interval in ["1m", "5m", "10m", "15m", "30m", "60m", "1h"]:
96
+ values._interval_size = values.interval # type: ignore
97
+ elif values.interval in ["1d", "1W", "1M", "1Q", "1Y"]:
98
+ values._frequency = frequency_dict[values.interval] # type: ignore
99
+
100
+ return values
101
+
102
+
103
+ class IntrinioEquityHistoricalData(EquityHistoricalData):
104
+ """Intrinio Equity Historical Price Data."""
105
+
106
+ __alias_dict__ = {
107
+ "date": "time",
108
+ "change_percent": "percent_change",
109
+ "interval": "frequency",
110
+ "intra_period": "intraperiod",
111
+ }
112
+
113
+ average: Optional[float] = Field(
114
+ default=None,
115
+ description="Average trade price of an individual equity during the interval.",
116
+ )
117
+ change: Optional[float] = Field(
118
+ default=None,
119
+ description="Change in the price of the symbol from the previous day.",
120
+ )
121
+ change_percent: Optional[float] = Field(
122
+ default=None,
123
+ description="Percent change in the price of the symbol from the previous day.",
124
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
125
+ )
126
+ adj_open: Optional[float] = Field(
127
+ default=None,
128
+ description="The adjusted open price.",
129
+ )
130
+ adj_high: Optional[float] = Field(
131
+ default=None,
132
+ description="The adjusted high price.",
133
+ )
134
+ adj_low: Optional[float] = Field(
135
+ default=None,
136
+ description="The adjusted low price.",
137
+ )
138
+ adj_close: Optional[float] = Field(
139
+ default=None,
140
+ description=DATA_DESCRIPTIONS.get("adj_close", ""),
141
+ )
142
+ adj_volume: Optional[float] = Field(
143
+ default=None,
144
+ description="The adjusted volume.",
145
+ )
146
+ fifty_two_week_high: Optional[float] = Field(
147
+ default=None,
148
+ description="52 week high price for the symbol.",
149
+ )
150
+ fifty_two_week_low: Optional[float] = Field(
151
+ default=None,
152
+ description="52 week low price for the symbol.",
153
+ )
154
+ factor: Optional[float] = Field(
155
+ default=None,
156
+ description="factor by which to multiply equity prices before this "
157
+ "date, in order to calculate historically-adjusted equity prices.",
158
+ )
159
+ split_ratio: Optional[float] = Field(
160
+ default=None,
161
+ description="Ratio of the equity split, if a split occurred.",
162
+ )
163
+ dividend: Optional[float] = Field(
164
+ default=None,
165
+ description="Dividend amount, if a dividend was paid.",
166
+ )
167
+ close_time: Optional[datetime] = Field(
168
+ default=None,
169
+ description="The timestamp that represents the end of the interval span.",
170
+ )
171
+ interval: Optional[str] = Field(
172
+ default=None,
173
+ description="The data time frequency.",
174
+ )
175
+ intra_period: Optional[bool] = Field(
176
+ default=None,
177
+ description="If true, the equity price represents an unfinished period "
178
+ "(be it day, week, quarter, month, or year), meaning that the close "
179
+ "price is the latest price available, not the official close price "
180
+ "for the period",
181
+ )
182
+
183
+
184
+ class IntrinioEquityHistoricalFetcher(
185
+ Fetcher[
186
+ IntrinioEquityHistoricalQueryParams,
187
+ List[IntrinioEquityHistoricalData],
188
+ ]
189
+ ):
190
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
191
+
192
+ @staticmethod
193
+ def transform_query(params: Dict[str, Any]) -> IntrinioEquityHistoricalQueryParams:
194
+ """Transform the query params."""
195
+ transformed_params = params
196
+
197
+ now = datetime.now().date()
198
+ if params.get("start_date") is None:
199
+ transformed_params["start_date"] = now - relativedelta(years=1)
200
+
201
+ if params.get("end_date") is None:
202
+ transformed_params["end_date"] = now
203
+
204
+ if params.get("start_time") is None:
205
+ transformed_params["start_time"] = time(0, 0, 0)
206
+
207
+ if params.get("end_time") is None:
208
+ transformed_params["end_time"] = time(23, 59, 59)
209
+
210
+ return IntrinioEquityHistoricalQueryParams(**transformed_params)
211
+
212
+ # pylint: disable=protected-access
213
+ @staticmethod
214
+ async def aextract_data(
215
+ query: IntrinioEquityHistoricalQueryParams,
216
+ credentials: Optional[Dict[str, str]],
217
+ **kwargs: Any,
218
+ ) -> List[Dict]:
219
+ """Return the raw data from the Intrinio endpoint."""
220
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
221
+ base_url = f"https://api-v2.intrinio.com/securities/{query.symbol}/prices"
222
+ query_str = get_querystring(
223
+ query.model_dump(by_alias=True), ["symbol", "interval"]
224
+ )
225
+
226
+ if query._interval_size:
227
+ base_url += f"/intervals?interval_size={query._interval_size}"
228
+ data_key = "intervals"
229
+ elif query._frequency:
230
+ base_url += f"?frequency={query._frequency}"
231
+ data_key = "stock_prices"
232
+
233
+ async def callback(response: ClientResponse, session: ClientSession) -> list:
234
+ """Return the response."""
235
+ init_response = await response.json()
236
+ if "error" in init_response:
237
+ raise OpenBBError(
238
+ f"Intrinio Error Message -> {init_response['error']}: {init_response.get('message')}" # type: ignore
239
+ )
240
+
241
+ all_data: list = init_response.get(data_key, []) # type: ignore
242
+
243
+ next_page = init_response.get("next_page", None) # type: ignore
244
+ while next_page:
245
+ url = response.url.update_query(next_page=next_page).human_repr()
246
+ response_data = await session.get_json(url)
247
+
248
+ all_data.extend(response_data.get(data_key, [])) # type: ignore
249
+ next_page = response_data.get("next_page", None) # type: ignore
250
+
251
+ return all_data
252
+
253
+ url = f"{base_url}&{query_str}&api_key={api_key}"
254
+
255
+ return await amake_request(url, response_callback=callback, **kwargs) # type: ignore
256
+
257
+ @staticmethod
258
+ def transform_data(
259
+ query: IntrinioEquityHistoricalQueryParams,
260
+ data: List[Dict],
261
+ **kwargs: Any,
262
+ ) -> List[IntrinioEquityHistoricalData]:
263
+ """Return the transformed data."""
264
+ if not data:
265
+ raise EmptyDataError("The request was returned empty.")
266
+ date_col = (
267
+ "time"
268
+ if query.interval in ["1m", "5m", "10m", "15m", "30m", "60m", "1h"]
269
+ else "date"
270
+ )
271
+ return [
272
+ IntrinioEquityHistoricalData.model_validate(d)
273
+ for d in sorted(data, key=lambda x: x[date_col], reverse=False)
274
+ ]
openbb_platform/providers/intrinio/openbb_intrinio/models/equity_info.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Equity Info Model."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from openbb_core.provider.abstract.fetcher import Fetcher
6
+ from openbb_core.provider.standard_models.equity_info import (
7
+ EquityInfoData,
8
+ EquityInfoQueryParams,
9
+ )
10
+ from openbb_core.provider.utils.helpers import (
11
+ amake_requests,
12
+ )
13
+ from openbb_intrinio.utils.helpers import response_callback
14
+ from pydantic import Field
15
+
16
+
17
+ class IntrinioEquityInfoQueryParams(EquityInfoQueryParams):
18
+ """Intrinio Equity Info Query.
19
+
20
+ Source: https://docs.intrinio.com/documentation/web_api/get_company_v2
21
+ """
22
+
23
+ __json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
24
+
25
+
26
+ class IntrinioEquityInfoData(EquityInfoData):
27
+ """Intrinio Equity Info Data."""
28
+
29
+ __alias_dict__ = {
30
+ "symbol": "ticker",
31
+ }
32
+
33
+ id: str = Field(default=None, description="Intrinio ID for the company.")
34
+ thea_enabled: Optional[bool] = Field(
35
+ default=None, description="Whether the company has been enabled for Thea."
36
+ )
37
+
38
+
39
+ class IntrinioEquityInfoFetcher(
40
+ Fetcher[
41
+ IntrinioEquityInfoQueryParams,
42
+ List[IntrinioEquityInfoData],
43
+ ]
44
+ ):
45
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
46
+
47
+ @staticmethod
48
+ def transform_query(params: Dict[str, Any]) -> IntrinioEquityInfoQueryParams:
49
+ """Transform the query."""
50
+ return IntrinioEquityInfoQueryParams(**params)
51
+
52
+ # pylint: disable=W0613:unused-argument
53
+ @staticmethod
54
+ async def aextract_data(
55
+ query: IntrinioEquityInfoQueryParams,
56
+ credentials: Optional[Dict[str, str]],
57
+ **kwargs: Any,
58
+ ) -> Dict:
59
+ """Return the raw data from the Intrinio endpoint."""
60
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
61
+ base_url = "https://api-v2.intrinio.com"
62
+
63
+ urls = [
64
+ f"{base_url}/companies/{s.strip()}?api_key={api_key}"
65
+ for s in query.symbol.split(",")
66
+ ]
67
+
68
+ return await amake_requests(urls, response_callback, **kwargs)
69
+
70
+ # pylint: disable=W0613:unused-argument
71
+ @staticmethod
72
+ def transform_data(
73
+ query: IntrinioEquityInfoQueryParams,
74
+ data: List[Dict],
75
+ **kwargs: Any,
76
+ ) -> List[IntrinioEquityInfoData]:
77
+ """Transform the data."""
78
+ return [IntrinioEquityInfoData.model_validate(d) for d in data]
openbb_platform/providers/intrinio/openbb_intrinio/models/equity_quote.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Equity Quote Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+ import re
5
+ import warnings
6
+ from datetime import datetime
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from openbb_core.provider.abstract.fetcher import Fetcher
10
+ from openbb_core.provider.standard_models.equity_quote import (
11
+ EquityQuoteData,
12
+ EquityQuoteQueryParams,
13
+ )
14
+ from openbb_core.provider.utils.helpers import (
15
+ ClientResponse,
16
+ amake_requests,
17
+ )
18
+ from openbb_intrinio.utils.references import SOURCES, VENUES, IntrinioSecurity
19
+ from pydantic import Field, field_validator
20
+
21
+ _warn = warnings.warn
22
+
23
+
24
+ class IntrinioEquityQuoteQueryParams(EquityQuoteQueryParams):
25
+ """Intrinio Equity Quote Query.
26
+
27
+ Source: https://docs.intrinio.com/documentation/web_api/get_security_realtime_price_v2
28
+ """
29
+
30
+ __json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
31
+
32
+ symbol: str = Field(
33
+ description="A Security identifier (Ticker, FIGI, ISIN, CUSIP, Intrinio ID)."
34
+ )
35
+ source: SOURCES = Field(default="iex", description="Source of the data.")
36
+
37
+
38
+ class IntrinioEquityQuoteData(EquityQuoteData):
39
+ """Intrinio Equity Quote Data."""
40
+
41
+ __alias_dict__ = {
42
+ "exchange": "listing_venue",
43
+ "market_center": "market_center_code",
44
+ "bid": "bid_price",
45
+ "ask": "ask_price",
46
+ "open": "open_price",
47
+ "close": "close_price",
48
+ "low": "low_price",
49
+ "high": "high_price",
50
+ "last_timestamp": "last_time",
51
+ "volume": "market_volume",
52
+ }
53
+ is_darkpool: Optional[bool] = Field(
54
+ default=None, description="Whether or not the current trade is from a darkpool."
55
+ )
56
+ source: Optional[str] = Field(
57
+ default=None, description="Source of the Intrinio data."
58
+ )
59
+ updated_on: datetime = Field(
60
+ description="Date and Time when the data was last updated."
61
+ )
62
+ security: Optional[IntrinioSecurity] = Field(
63
+ default=None, description="Security details related to the quote."
64
+ )
65
+
66
+ @field_validator("last_time", "updated_on", mode="before", check_fields=False)
67
+ @classmethod
68
+ def date_validate(cls, v): # pylint: disable=E0213
69
+ """Return the date as a datetime object."""
70
+ return (
71
+ datetime.fromisoformat(v.replace("Z", "+00:00"))
72
+ if v.endswith(("Z", "+00:00"))
73
+ else datetime.fromisoformat(v)
74
+ )
75
+
76
+ @field_validator("sales_conditions", mode="before", check_fields=False)
77
+ @classmethod
78
+ def validate_sales_conditions(cls, v):
79
+ """Validate sales conditions and remove empty strings."""
80
+ if v:
81
+ control_char_re = re.compile(r"[\x00-\x1f\x7f-\x9f]")
82
+ v = control_char_re.sub("", v).strip()
83
+ v = None if v == "" else v
84
+ return v if v else None
85
+
86
+ @field_validator("exchange", "market_center", mode="before", check_fields=False)
87
+ @classmethod
88
+ def validate_listing_venue(cls, v):
89
+ """Validate listing venue and remove empty strings."""
90
+ if v:
91
+ return VENUES.get(v, v)
92
+ return None
93
+
94
+
95
+ class IntrinioEquityQuoteFetcher(
96
+ Fetcher[
97
+ IntrinioEquityQuoteQueryParams,
98
+ List[IntrinioEquityQuoteData],
99
+ ]
100
+ ):
101
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
102
+
103
+ @staticmethod
104
+ def transform_query(params: Dict[str, Any]) -> IntrinioEquityQuoteQueryParams:
105
+ """Transform the query params."""
106
+ return IntrinioEquityQuoteQueryParams(**params)
107
+
108
+ @staticmethod
109
+ async def aextract_data(
110
+ query: IntrinioEquityQuoteQueryParams,
111
+ credentials: Optional[Dict[str, str]],
112
+ **kwargs: Any,
113
+ ) -> List[Dict]:
114
+ """Return the raw data from the Intrinio endpoint."""
115
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
116
+
117
+ base_url = "https://api-v2.intrinio.com"
118
+
119
+ async def callback(response: ClientResponse, _: Any) -> dict:
120
+ """Return the response."""
121
+ if response.status != 200:
122
+ return {}
123
+
124
+ response_data = await response.json()
125
+ response_data["symbol"] = response_data["security"].get("ticker", None) # type: ignore
126
+ if "messages" in response_data and response_data.get("messages"): # type: ignore
127
+ _message = list(response_data.pop("messages")) # type: ignore
128
+ _warn(str(",".join(_message)))
129
+ return response_data # type: ignore
130
+
131
+ urls = [
132
+ f"{base_url}/securities/{s.strip()}/prices/realtime?source={query.source}&api_key={api_key}"
133
+ for s in query.symbol.split(",")
134
+ ]
135
+ return await amake_requests(urls, callback, **kwargs)
136
+
137
+ @staticmethod
138
+ def transform_data(
139
+ query: IntrinioEquityQuoteQueryParams, data: List[Dict], **kwargs: Any
140
+ ) -> List[IntrinioEquityQuoteData]:
141
+ """Return the transformed data."""
142
+ return [IntrinioEquityQuoteData.model_validate(d) for d in data]
openbb_platform/providers/intrinio/openbb_intrinio/models/equity_search.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Equity Search Model."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from openbb_core.provider.abstract.fetcher import Fetcher
6
+ from openbb_core.provider.standard_models.equity_search import (
7
+ EquitySearchData,
8
+ EquitySearchQueryParams,
9
+ )
10
+ from openbb_core.provider.utils.descriptions import (
11
+ DATA_DESCRIPTIONS,
12
+ QUERY_DESCRIPTIONS,
13
+ )
14
+ from openbb_core.provider.utils.helpers import get_querystring
15
+ from openbb_intrinio.utils.helpers import get_data_one
16
+ from pydantic import Field
17
+
18
+
19
+ class IntrinioEquitySearchQueryParams(EquitySearchQueryParams):
20
+ """Intrinio Equity Search Query.
21
+
22
+ Source: https://docs.intrinio.com/documentation/web_api/search_companies_v2
23
+ """
24
+
25
+ __alias_dict__ = {
26
+ "limit": "page_size",
27
+ }
28
+
29
+ active: bool = Field(
30
+ default=True,
31
+ description="When true, return companies that are actively traded (having stock prices within the past 14 days)."
32
+ + " When false, return companies that are not actively traded or never have been traded.",
33
+ )
34
+ limit: Optional[int] = Field(
35
+ default=10000,
36
+ description=QUERY_DESCRIPTIONS.get("limit", ""),
37
+ )
38
+
39
+
40
+ class IntrinioEquitySearchData(EquitySearchData):
41
+ """Intrinio Equity Search Data."""
42
+
43
+ __alias_dict__ = {
44
+ "intrinio_id": "id",
45
+ "symbol": "ticker",
46
+ }
47
+
48
+ cik: Optional[str] = Field(description=DATA_DESCRIPTIONS.get("CIK", ""))
49
+ lei: Optional[str] = Field(
50
+ description="The Legal Entity Identifier (LEI) of the company."
51
+ )
52
+ intrinio_id: str = Field(description="The Intrinio ID of the company.")
53
+
54
+
55
+ class IntrinioEquitySearchFetcher(
56
+ Fetcher[
57
+ IntrinioEquitySearchQueryParams,
58
+ List[IntrinioEquitySearchData],
59
+ ]
60
+ ):
61
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
62
+
63
+ @staticmethod
64
+ def transform_query(params: Dict[str, Any]) -> IntrinioEquitySearchQueryParams:
65
+ """Transform the query."""
66
+ return IntrinioEquitySearchQueryParams(**params)
67
+
68
+ @staticmethod
69
+ async def aextract_data(
70
+ query: IntrinioEquitySearchQueryParams, # pylint: disable=unused-argument
71
+ credentials: Optional[Dict[str, str]],
72
+ **kwargs: Any,
73
+ ) -> List[Dict]:
74
+ """Return the raw data from the Intrinio endpoint."""
75
+
76
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
77
+ query_str = get_querystring(query.model_dump(by_alias=True), ["is_symbol"])
78
+ base_url = "https://api-v2.intrinio.com/companies/search?"
79
+ url = f"{base_url}{query_str}&api_key={api_key}"
80
+ data = await get_data_one(url, **kwargs)
81
+ return data
82
+
83
+ @staticmethod
84
+ def transform_data(
85
+ query: IntrinioEquitySearchQueryParams, data: Dict, **kwargs: Any
86
+ ) -> List[IntrinioEquitySearchData]:
87
+ """Transform the data to the standard format."""
88
+
89
+ return [IntrinioEquitySearchData.model_validate(d) for d in data["companies"]]
openbb_platform/providers/intrinio/openbb_intrinio/models/etf_holdings.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio ETF Holdings Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from datetime import date as dateType
6
+ from typing import Any, Dict, List, Optional, Union
7
+
8
+ from openbb_core.app.model.abstract.error import OpenBBError
9
+ from openbb_core.provider.abstract.fetcher import Fetcher
10
+ from openbb_core.provider.standard_models.etf_holdings import (
11
+ EtfHoldingsData,
12
+ EtfHoldingsQueryParams,
13
+ )
14
+ from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
15
+ from openbb_core.provider.utils.helpers import (
16
+ ClientResponse,
17
+ ClientSession,
18
+ amake_request,
19
+ )
20
+ from pydantic import Field, model_validator
21
+
22
+
23
+ class IntrinioEtfHoldingsQueryParams(EtfHoldingsQueryParams):
24
+ """
25
+ Intrinio ETF Holdings Query Params.
26
+
27
+ Source: https://docs.intrinio.com/documentation/web_api/get_etf_holdings_v2
28
+ """
29
+
30
+ __alias_dict__ = {"date": "as_of_date"}
31
+
32
+ date: Optional[dateType] = Field(
33
+ default=None, description=QUERY_DESCRIPTIONS.get("date", "")
34
+ )
35
+
36
+
37
+ class IntrinioEtfHoldingsData(EtfHoldingsData):
38
+ """Intrinio ETF Holdings Data."""
39
+
40
+ __alias_dict__ = {
41
+ "symbol": "ticker",
42
+ "security_type": "type",
43
+ "unit": "quantity_units",
44
+ "face_value": "face",
45
+ "balance": "quantity_held",
46
+ "value": "market_value_held",
47
+ "derivatives_value": "notional_value",
48
+ "units_per_share": "quantity_per_share",
49
+ "weight": "weighting",
50
+ "updated": "as_of_date",
51
+ "country": "location",
52
+ "maturity_date": "maturity",
53
+ }
54
+
55
+ name: Optional[str] = Field(
56
+ default=None,
57
+ description="The common name for the holding.",
58
+ )
59
+ security_type: Optional[str] = Field(
60
+ default=None,
61
+ description="The type of instrument for this holding. Examples(Bond='BOND', Equity='EQUI')",
62
+ )
63
+ isin: Optional[str] = Field(
64
+ default=None,
65
+ description="The International Securities Identification Number.",
66
+ )
67
+ ric: Optional[str] = Field(
68
+ default=None,
69
+ description="The Reuters Instrument Code.",
70
+ )
71
+ sedol: Optional[str] = Field(
72
+ default=None,
73
+ description="The Stock Exchange Daily Official List.",
74
+ )
75
+ share_class_figi: Optional[str] = Field(
76
+ default=None,
77
+ description="The OpenFIGI symbol for the holding.",
78
+ )
79
+ country: Optional[str] = Field(
80
+ default=None,
81
+ description="The country or region of the holding.",
82
+ )
83
+ maturity_date: Optional[dateType] = Field(
84
+ default=None,
85
+ description="The maturity date for the debt security, if available.",
86
+ )
87
+ contract_expiry_date: Optional[dateType] = Field(
88
+ default=None,
89
+ description="Expiry date for the futures contract held, if available.",
90
+ )
91
+ coupon: Optional[float] = Field(
92
+ default=None,
93
+ description="The coupon rate of the debt security, if available.",
94
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
95
+ )
96
+ balance: Optional[Union[int, float]] = Field(
97
+ default=None,
98
+ description="The number of units of the security held, if available.",
99
+ )
100
+ unit: Optional[str] = Field(
101
+ default=None,
102
+ description="The units of the 'balance' field.",
103
+ )
104
+ units_per_share: Optional[float] = Field(
105
+ default=None,
106
+ description="Number of units of the security held per share outstanding of the ETF, if available.",
107
+ )
108
+ face_value: Optional[float] = Field(
109
+ default=None,
110
+ description="The face value of the debt security, if available.",
111
+ json_schema_extra={"x-unit_measurement": "currency"},
112
+ )
113
+ derivatives_value: Optional[float] = Field(
114
+ default=None,
115
+ description="The notional value of derivatives contracts held.",
116
+ json_schema_extra={"x-unit_measurement": "currency"},
117
+ )
118
+ value: Optional[float] = Field(
119
+ default=None,
120
+ description="The market value of the holding, on the 'as_of' date.",
121
+ json_schema_extra={"x-unit_measurement": "currency"},
122
+ )
123
+ weight: Optional[float] = Field(
124
+ default=None,
125
+ description="The weight of the holding, as a normalized percent.",
126
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
127
+ )
128
+ updated: Optional[dateType] = Field(
129
+ default=None,
130
+ description="The 'as_of' date for the holding.",
131
+ )
132
+
133
+ @model_validator(mode="before")
134
+ @classmethod
135
+ def replace_zero(cls, values):
136
+ """Check for zero values and replace with None."""
137
+ return (
138
+ {k: None if v == 0 else v for k, v in values.items()}
139
+ if isinstance(values, dict)
140
+ else values
141
+ )
142
+
143
+
144
+ class IntrinioEtfHoldingsFetcher(
145
+ Fetcher[IntrinioEtfHoldingsQueryParams, List[IntrinioEtfHoldingsData]]
146
+ ):
147
+ """Intrinio ETF Holdings Fetcher."""
148
+
149
+ @staticmethod
150
+ def transform_query(params: Dict[str, Any]) -> IntrinioEtfHoldingsQueryParams:
151
+ """Transform query."""
152
+ return IntrinioEtfHoldingsQueryParams(**params)
153
+
154
+ @staticmethod
155
+ async def aextract_data(
156
+ query: IntrinioEtfHoldingsQueryParams,
157
+ credentials: Optional[Dict[str, str]],
158
+ **kwargs: Any,
159
+ ) -> List[Dict]:
160
+ """Return the raw data from the Intrinio endpoint."""
161
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
162
+ symbol = query.symbol + ":US" if ":" not in query.symbol else query.symbol
163
+ URL = f"https://api-v2.intrinio.com/etfs/{symbol}/holdings?page_size=10000&api_key={api_key}"
164
+ if query.date:
165
+ URL += f"&as_of_date={query.date}"
166
+ data: List = []
167
+
168
+ async def response_callback(response: ClientResponse, session: ClientSession):
169
+ """Async response callback."""
170
+ results = await response.json()
171
+
172
+ if results.get("error"): # type: ignore
173
+ return results
174
+
175
+ if results.get("holdings") and len(results.get("holdings")) > 0: # type: ignore
176
+ data.extend(results.get("holdings")) # type: ignore
177
+ while results.get("next_page"): # type: ignore
178
+ next_page = results["next_page"] # type: ignore
179
+ next_url = f"{URL}&next_page={next_page}"
180
+ results = await amake_request(next_url, session=session, **kwargs)
181
+ if (
182
+ "holdings" in results
183
+ and len(results.get("holdings")) > 0 # type: ignore
184
+ ):
185
+ data.extend(results.get("holdings")) # type: ignore
186
+ return data
187
+
188
+ return await amake_request(URL, response_callback=response_callback, **kwargs) # type: ignore
189
+
190
+ @staticmethod
191
+ def transform_data(
192
+ query: IntrinioEtfHoldingsQueryParams,
193
+ data: List[Dict],
194
+ **kwargs: Any,
195
+ ) -> List[IntrinioEtfHoldingsData]:
196
+ """Transform data."""
197
+ if not data or isinstance(data, dict) and data.get("error"):
198
+ if isinstance(data, list) and data == []:
199
+ raise OpenBBError(
200
+ str(
201
+ f"No holdings were found for {query.symbol}, and the response from Intrinio was empty."
202
+ )
203
+ )
204
+ raise OpenBBError(str(f"{data.get('message')} {query.symbol}: {data['error']}")) # type: ignore
205
+
206
+ results: List[IntrinioEtfHoldingsData] = []
207
+ for d in sorted(data, key=lambda x: x["weighting"], reverse=True):
208
+ # This field is deprecated and is dupilcated in the response.
209
+ _ = d.pop("composite_figi", None)
210
+ if d.get("coupon"):
211
+ d["coupon"] = d["coupon"] / 100
212
+ results.append(IntrinioEtfHoldingsData.model_validate(d))
213
+
214
+ return results
openbb_platform/providers/intrinio/openbb_intrinio/models/etf_info.py ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio ETF Info Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from datetime import date as dateType
6
+ from typing import Any, Dict, List, Optional
7
+ from warnings import warn
8
+
9
+ from openbb_core.provider.abstract.fetcher import Fetcher
10
+ from openbb_core.provider.standard_models.etf_info import (
11
+ EtfInfoData,
12
+ EtfInfoQueryParams,
13
+ )
14
+ from openbb_core.provider.utils.errors import EmptyDataError
15
+ from openbb_core.provider.utils.helpers import amake_requests
16
+ from pydantic import Field
17
+
18
+
19
+ class IntrinioEtfInfoQueryParams(EtfInfoQueryParams):
20
+ """
21
+ Intrinio ETF Info Query Params.
22
+
23
+ Source: https://docs.intrinio.com/documentation/web_api/get_etf_v2
24
+ """
25
+
26
+ __json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
27
+
28
+
29
+ class IntrinioEtfInfoData(EtfInfoData):
30
+ """Intrinio ETF Info Data."""
31
+
32
+ __alias_dict__ = {
33
+ "symbol": "ticker",
34
+ "exchange": "exchange_mic",
35
+ "issuer": "sponsor",
36
+ "investment_style": "type",
37
+ "industry_group": "sub_industry",
38
+ "holds_mlp": "holds_ml_ps",
39
+ "holds_adr": "holds_ad_rs",
40
+ "index_symbol": "index_ticker",
41
+ "figi_symbol": "figi_ticker",
42
+ "intrinio_id": "id",
43
+ "is_listed": "is_live_listed",
44
+ "beta_type": "smartvs_traditional_beta",
45
+ "beta_details": "smartvs_traditional_beta_level2",
46
+ "listing_country": "primary_ticker_country_code",
47
+ "listing_region": "primary_listing_region",
48
+ "primary_symbol": "primary_ticker",
49
+ "intraday_nav_symbol": "intraday_nav_ticker",
50
+ "issuer_country": "issuing_entity_country_code",
51
+ "livestock_type": "livestock",
52
+ }
53
+
54
+ fund_listing_date: Optional[dateType] = Field(
55
+ default=None,
56
+ description="The date on which the Exchange Traded Product (ETP)"
57
+ + " or share class of the ETP is listed on a specific exchange.",
58
+ )
59
+ data_change_date: Optional[dateType] = Field(
60
+ default=None,
61
+ description="The last date on which there was a change in a classifications data field for this ETF.",
62
+ )
63
+ etn_maturity_date: Optional[dateType] = Field(
64
+ default=None,
65
+ description="If the product is an ETN, this field identifies the maturity date for the ETN.",
66
+ )
67
+ is_listed: Optional[bool] = Field(
68
+ default=None,
69
+ description="If true, the ETF is still listed on an exchange.",
70
+ )
71
+ close_date: Optional[dateType] = Field(
72
+ default=None,
73
+ description="The date on which the ETF was de-listed if it is no longer listed.",
74
+ )
75
+ exchange: Optional[str] = Field(
76
+ default=None,
77
+ description="The exchange Market Identifier Code (MIC).",
78
+ )
79
+ isin: Optional[str] = Field(
80
+ default=None,
81
+ description="International Securities Identification Number (ISIN).",
82
+ )
83
+ ric: Optional[str] = Field(
84
+ default=None,
85
+ description="Reuters Instrument Code (RIC).",
86
+ )
87
+ sedol: Optional[str] = Field(
88
+ default=None,
89
+ description="Stock Exchange Daily Official List (SEDOL).",
90
+ )
91
+ figi_symbol: Optional[str] = Field(
92
+ default=None,
93
+ description="Financial Instrument Global Identifier (FIGI) symbol.",
94
+ )
95
+ share_class_figi: Optional[str] = Field(
96
+ default=None,
97
+ description="Financial Instrument Global Identifier (FIGI).",
98
+ )
99
+ firstbridge_id: Optional[str] = Field(
100
+ default=None,
101
+ description="The FirstBridge unique identifier for the Exchange Traded Fund (ETF).",
102
+ )
103
+ firstbridge_parent_id: Optional[str] = Field(
104
+ default=None,
105
+ description="The FirstBridge unique identifier for the parent Exchange Traded Fund (ETF), if applicable.",
106
+ )
107
+ intrinio_id: Optional[str] = Field(
108
+ default=None,
109
+ description="Intrinio unique identifier for the security.",
110
+ )
111
+ intraday_nav_symbol: Optional[str] = Field(
112
+ default=None,
113
+ description="Intraday Net Asset Value (NAV) symbol.",
114
+ )
115
+ primary_symbol: Optional[str] = Field(
116
+ default=None,
117
+ description="The primary ticker field is used for Exchange Traded Products (ETPs)"
118
+ + " that have multiple listings and share classes."
119
+ + " If an ETP has multiple listings or share classes,"
120
+ + " the same primary ticker is assigned to all the listings and share classes.",
121
+ )
122
+ etp_structure_type: Optional[str] = Field(
123
+ default=None,
124
+ description="Classifies Exchange Traded Products (ETPs) into very broad categories based on its legal structure.",
125
+ )
126
+ legal_structure: Optional[str] = Field(
127
+ default=None,
128
+ description="Legal structure of the fund.",
129
+ )
130
+ issuer: Optional[str] = Field(
131
+ default=None,
132
+ description="Issuer of the ETF.",
133
+ )
134
+ etn_issuing_bank: Optional[str] = Field(
135
+ default=None,
136
+ description="If the product is an Exchange Traded Note (ETN), this field identifies the issuing bank.",
137
+ )
138
+ fund_family: Optional[str] = Field(
139
+ default=None,
140
+ description="This field identifies the fund family to which the ETF belongs, as categorized by the ETF Sponsor.",
141
+ )
142
+ investment_style: Optional[str] = Field(
143
+ default=None,
144
+ description="Investment style of the ETF.",
145
+ )
146
+ derivatives_based: Optional[str] = Field(
147
+ default=None,
148
+ description="This field is populated if the ETF holds either"
149
+ + " listed or over-the-counter derivatives in its portfolio.",
150
+ )
151
+ income_category: Optional[str] = Field(
152
+ default=None,
153
+ description="Identifies if an Exchange Traded Fund (ETF) falls into a category"
154
+ + " that is specifically designed to provide a high yield or income",
155
+ )
156
+ asset_class: Optional[str] = Field(
157
+ default=None,
158
+ description="Captures the underlying nature of the securities in the Exchanged Traded Product (ETP).",
159
+ )
160
+ other_asset_types: Optional[str] = Field(
161
+ default=None,
162
+ description="If 'asset_class' field is classified as 'Other Asset Types'"
163
+ + " this field captures the specific category of the underlying assets.",
164
+ )
165
+ single_category_designation: Optional[str] = Field(
166
+ default=None,
167
+ description="This categorization is created for those users who want every ETF to be 'forced'"
168
+ + " into a single bucket, so that the assets for all categories will always sum to the total market.",
169
+ )
170
+ beta_type: Optional[str] = Field(
171
+ default=None,
172
+ description="This field identifies whether an ETF provides 'Traditional' beta exposure or 'Smart' beta exposure."
173
+ + " ETFs that are active (i.e. non-indexed), leveraged / inverse or have a proprietary quant model"
174
+ + " (i.e. that don't provide indexed exposure to a targeted factor) are classified separately.",
175
+ )
176
+ beta_details: Optional[str] = Field(
177
+ default=None,
178
+ description="This field provides further detail within the traditional and smart beta categories.",
179
+ )
180
+ market_cap_range: Optional[str] = Field(
181
+ default=None,
182
+ description="Equity ETFs are classified as falling into categories"
183
+ + " based on the description of their investment strategy in the prospectus."
184
+ + " Examples ('Mega Cap', 'Large Cap', 'Mid Cap', etc.)",
185
+ )
186
+ market_cap_weighting_type: Optional[str] = Field(
187
+ default=None,
188
+ description="For ETFs that take the value 'Market Cap Weighted' in the 'index_weighting_scheme' field,"
189
+ + " this field provides detail on the market cap weighting type.",
190
+ )
191
+ index_weighting_scheme: Optional[str] = Field(
192
+ default=None,
193
+ description="For ETFs that track an underlying index,"
194
+ + " this field provides detail on the index weighting type.",
195
+ )
196
+ index_linked: Optional[str] = Field(
197
+ default=None,
198
+ description="This field identifies whether an ETF is index linked or active.",
199
+ )
200
+ index_name: Optional[str] = Field(
201
+ default=None,
202
+ description="This field identifies the name of the underlying index tracked by the ETF, if applicable.",
203
+ )
204
+ index_symbol: Optional[str] = Field(
205
+ default=None,
206
+ description="This field identifies the OpenFIGI ticker for the Index underlying the ETF.",
207
+ )
208
+ parent_index: Optional[str] = Field(
209
+ default=None,
210
+ description="This field identifies the name of the parent index, which represents"
211
+ + " the broader universe from which the index underlying the ETF is created, if applicable.",
212
+ )
213
+ index_family: Optional[str] = Field(
214
+ default=None,
215
+ description="This field identifies the index family to which the index underlying the ETF belongs."
216
+ + " The index family is represented as categorized by the index provider.",
217
+ )
218
+ broader_index_family: Optional[str] = Field(
219
+ default=None,
220
+ description="This field identifies the broader index family to which the index underlying the ETF belongs."
221
+ + " The broader index family is represented as categorized by the index provider.",
222
+ )
223
+ index_provider: Optional[str] = Field(
224
+ default=None,
225
+ description="This field identifies the Index provider for the index underlying the ETF, if applicable.",
226
+ )
227
+ index_provider_code: Optional[str] = Field(
228
+ default=None,
229
+ description="This field provides the First Bridge code for each Index provider,"
230
+ + " corresponding to the index underlying the ETF if applicable.",
231
+ )
232
+ replication_structure: Optional[str] = Field(
233
+ default=None,
234
+ description="The replication structure of the Exchange Traded Product (ETP).",
235
+ )
236
+ growth_value_tilt: Optional[str] = Field(
237
+ default=None,
238
+ description="Classifies equity ETFs as either 'Growth' or Value' based on the stated style tilt"
239
+ + " in the ETF prospectus. Equity ETFs that do not have a stated style tilt are classified as 'Core / Blend'.",
240
+ )
241
+ growth_type: Optional[str] = Field(
242
+ default=None,
243
+ description="For ETFs that are classified as 'Growth' in 'growth_value_tilt',"
244
+ + " this field further identifies those where the stocks in the"
245
+ + " ETF are both selected and weighted based on their growth (style factor) scores.",
246
+ )
247
+ value_type: Optional[str] = Field(
248
+ default=None,
249
+ description="For ETFs that are classified as 'Value' in 'growth_value_tilt',"
250
+ + " this field further identifies those where the stocks in the"
251
+ + " ETF are both selected and weighted based on their value (style factor) scores.",
252
+ )
253
+ sector: Optional[str] = Field(
254
+ default=None,
255
+ description="For equity ETFs that aim to provide targeted exposure to a sector or industry,"
256
+ + " this field identifies the Sector that it provides the exposure to.",
257
+ )
258
+ industry: Optional[str] = Field(
259
+ default=None,
260
+ description="For equity ETFs that aim to provide targeted exposure to an industry,"
261
+ + " this field identifies the Industry that it provides the exposure to.",
262
+ )
263
+ industry_group: Optional[str] = Field(
264
+ default=None,
265
+ description="For equity ETFs that aim to provide targeted exposure to a sub-industry,"
266
+ + " this field identifies the sub-Industry that it provides the exposure to.",
267
+ )
268
+ cross_sector_theme: Optional[str] = Field(
269
+ default=None,
270
+ description="For equity ETFs that aim to provide targeted exposure to a specific investment theme"
271
+ + " that cuts across GICS sectors, this field identifies the specific cross-sector theme."
272
+ + " Examples ('Agri-business', 'Natural Resources', 'Green Investing', etc.)",
273
+ )
274
+ natural_resources_type: Optional[str] = Field(
275
+ default=None,
276
+ description="For ETFs that are classified as 'Natural Resources' in the 'cross_sector_theme' field,"
277
+ + " this field provides further detail on the type of Natural Resources exposure.",
278
+ )
279
+ us_or_excludes_us: Optional[str] = Field(
280
+ default=None,
281
+ description="Takes the value of 'Domestic' for US exposure,"
282
+ + " 'International' for non-US exposure and 'Global' for exposure that includes all regions including the US.",
283
+ )
284
+ developed_emerging: Optional[str] = Field(
285
+ default=None,
286
+ description="This field identifies the stage of development of the markets that the ETF provides exposure to.",
287
+ )
288
+ specialized_region: Optional[str] = Field(
289
+ default=None,
290
+ description="This field is populated if the ETF provides targeted"
291
+ + " exposure to a specific type of geography-based grouping"
292
+ + " that does not fall into a specific country or continent grouping."
293
+ + " Examples ('BRIC', 'Chindia', etc.)",
294
+ )
295
+ continent: Optional[str] = Field(
296
+ default=None,
297
+ description="This field is populated if the ETF provides targeted exposure"
298
+ + " to a specific continent or country within that Continent.",
299
+ )
300
+ latin_america_sub_group: Optional[str] = Field(
301
+ default=None,
302
+ description="For ETFs that are classified as 'Latin America' in the 'continent' field,"
303
+ + " this field provides further detail on the type of regional exposure.",
304
+ )
305
+ europe_sub_group: Optional[str] = Field(
306
+ default=None,
307
+ description="For ETFs that are classified as 'Europe' in the 'continent' field,"
308
+ + " this field provides further detail on the type of regional exposure.",
309
+ )
310
+ asia_sub_group: Optional[str] = Field(
311
+ default=None,
312
+ description="For ETFs that are classified as 'Asia' in the 'continent' field,"
313
+ + " this field provides further detail on the type of regional exposure.",
314
+ )
315
+ specific_country: Optional[str] = Field(
316
+ default=None,
317
+ description="This field is populated if the ETF provides targeted exposure to a specific country.",
318
+ )
319
+ china_listing_location: Optional[str] = Field(
320
+ default=None,
321
+ description="For ETFs that are classified as 'China' in the 'country' field,"
322
+ + " this field provides further detail on the type of exposure in the underlying securities.",
323
+ )
324
+ us_state: Optional[str] = Field(
325
+ default=None,
326
+ description="Takes the value of a US state if the ETF provides"
327
+ + " targeted exposure to the municipal bonds or equities of companies.",
328
+ )
329
+ real_estate: Optional[str] = Field(
330
+ default=None,
331
+ description="For ETFs that provide targeted real estate exposure,"
332
+ + " this field is populated if the ETF provides targeted"
333
+ + " exposure to a specific segment of the real estate market.",
334
+ )
335
+
336
+ fundamental_weighting_type: Optional[str] = Field(
337
+ default=None,
338
+ description="For ETFs that take the value 'Fundamental Weighted' in the 'index_weighting_scheme' field,"
339
+ + " this field provides detail on the fundamental weighting methodology.",
340
+ )
341
+ dividend_weighting_type: Optional[str] = Field(
342
+ default=None,
343
+ description="For ETFs that take the value 'Dividend Weighted' in the 'index_weighting_scheme' field,"
344
+ + " this field provides detail on the dividend weighting methodology.",
345
+ )
346
+ bond_type: Optional[str] = Field(
347
+ default=None,
348
+ description="For ETFs where 'asset_class_type' is 'Bonds',"
349
+ + " this field provides detail on the type of bonds held in the ETF.",
350
+ )
351
+ government_bond_types: Optional[str] = Field(
352
+ default=None,
353
+ description="For bond ETFs that take the value 'Treasury & Government' in 'bond_type',"
354
+ + " this field provides detail on the exposure.",
355
+ )
356
+ municipal_bond_region: Optional[str] = Field(
357
+ default=None,
358
+ description="For bond ETFs that take the value 'Municipal' in 'bond_type',"
359
+ + " this field provides additional detail on the geographic exposure.",
360
+ )
361
+ municipal_vrdo: Optional[bool] = Field(
362
+ default=None,
363
+ description="For bond ETFs that take the value 'Municipal' in 'bond_type',"
364
+ + " this field identifies those ETFs that specifically provide exposure to Variable Rate Demand Obligations.",
365
+ )
366
+ mortgage_bond_types: Optional[str] = Field(
367
+ default=None,
368
+ description="For bond ETFs that take the value 'Mortgage' in 'bond_type',"
369
+ + " this field provides additional detail on the type of underlying securities.",
370
+ )
371
+ bond_tax_status: Optional[str] = Field(
372
+ default=None,
373
+ description="For all US bond ETFs, this field provides additional"
374
+ + " detail on the tax treatment of the underlying securities.",
375
+ )
376
+ credit_quality: Optional[str] = Field(
377
+ default=None,
378
+ description="For all bond ETFs, this field helps to identify if the ETF"
379
+ + " provides targeted exposure to securities of a specific credit quality range.",
380
+ )
381
+ average_maturity: Optional[str] = Field(
382
+ default=None,
383
+ description="For all bond ETFs, this field helps to identify if the ETF"
384
+ + " provides targeted exposure to securities of a specific maturity range.",
385
+ )
386
+ specific_maturity_year: Optional[int] = Field(
387
+ default=None,
388
+ description="For all bond ETFs that take the value 'Specific Maturity Year' in the 'average_maturity' field,"
389
+ + " this field specifies the calendar year.",
390
+ )
391
+ commodity_types: Optional[str] = Field(
392
+ default=None,
393
+ description="For ETFs where 'asset_class_type' is 'Commodities',"
394
+ + " this field provides detail on the type of commodities held in the ETF.",
395
+ )
396
+ energy_type: Optional[str] = Field(
397
+ default=None,
398
+ description="For ETFs where 'commodity_type' is 'Energy',"
399
+ + " this field provides detail on the type of energy exposure provided by the ETF.",
400
+ )
401
+ agricultural_type: Optional[str] = Field(
402
+ default=None,
403
+ description="For ETFs where 'commodity_type' is 'Agricultural',"
404
+ + " this field provides detail on the type of agricultural exposure provided by the ETF.",
405
+ )
406
+ livestock_type: Optional[str] = Field(
407
+ default=None,
408
+ description="For ETFs where 'commodity_type' is 'Livestock',"
409
+ + " this field provides detail on the type of livestock exposure provided by the ETF.",
410
+ )
411
+ metal_type: Optional[str] = Field(
412
+ default=None,
413
+ description="For ETFs where 'commodity_type' is 'Gold & Metals',"
414
+ + " this field provides detail on the type of exposure provided by the ETF.",
415
+ )
416
+ inverse_leveraged: Optional[str] = Field(
417
+ default=None,
418
+ description="This field is populated if the ETF provides inverse or leveraged exposure.",
419
+ )
420
+ target_date_multi_asset_type: Optional[str] = Field(
421
+ default=None,
422
+ description="For ETFs where 'asset_class_type' is 'Target Date / MultiAsset',"
423
+ + " this field provides detail on the type of commodities held in the ETF.",
424
+ )
425
+ currency_pair: Optional[str] = Field(
426
+ default=None,
427
+ description="This field is populated if the ETF's strategy involves providing exposure to"
428
+ + " the movements of a currency or involves hedging currency exposure.",
429
+ )
430
+ social_environmental_type: Optional[str] = Field(
431
+ default=None,
432
+ description="This field is populated if the ETF's strategy involves providing"
433
+ + " exposure to a specific social or environmental theme.",
434
+ )
435
+ clean_energy_type: Optional[str] = Field(
436
+ default=None,
437
+ description="This field is populated if the ETF has a value of 'Clean Energy'"
438
+ + " in the 'social_environmental_type' field.",
439
+ )
440
+ dividend_type: Optional[str] = Field(
441
+ default=None,
442
+ description="This field is populated if the ETF has an intended"
443
+ + " investment objective of holding dividend-oriented stocks as stated in the prospectus.",
444
+ )
445
+ regular_dividend_payor_type: Optional[str] = Field(
446
+ default=None,
447
+ description="This field is populated if the ETF has a value of"
448
+ + "'Dividend - Regular Payors' in the 'dividend_type' field.",
449
+ )
450
+ quant_strategies_type: Optional[str] = Field(
451
+ default=None,
452
+ description="This field is populated if the ETF has either an index-linked"
453
+ + " or active strategy that is based on a proprietary quantitative strategy.",
454
+ )
455
+ other_quant_models: Optional[str] = Field(
456
+ default=None,
457
+ description="For ETFs where 'quant_strategies_type' is 'Other Quant Model',"
458
+ + " this field provides the name of the specific proprietary quant model"
459
+ + " used as the underlying strategy for the ETF.",
460
+ )
461
+ hedge_fund_type: Optional[str] = Field(
462
+ default=None,
463
+ description="For ETFs where 'other_asset_types' is 'Hedge Fund Replication',"
464
+ + " this field provides detail on the type of hedge fund replication strategy.",
465
+ )
466
+ excludes_financials: Optional[bool] = Field(
467
+ default=None,
468
+ description="For equity ETFs, identifies those ETFs"
469
+ + " where the underlying fund holdings will not hold financials stocks,"
470
+ + " based on the funds intended objective.",
471
+ )
472
+ excludes_technology: Optional[bool] = Field(
473
+ default=None,
474
+ description="For equity ETFs, identifies those ETFs"
475
+ + " where the underlying fund holdings will not hold technology stocks,"
476
+ + " based on the funds intended objective.",
477
+ )
478
+ holds_only_nyse_stocks: Optional[bool] = Field(
479
+ default=None,
480
+ description="If true, the ETF is an equity ETF and holds only stocks listed on NYSE.",
481
+ )
482
+ holds_only_nasdaq_stocks: Optional[bool] = Field(
483
+ default=None,
484
+ description="If true, the ETF is an equity ETF and holds only stocks listed on Nasdaq.",
485
+ )
486
+ holds_mlp: Optional[bool] = Field(
487
+ default=None,
488
+ description="If true, the ETF's investment objective explicitly specifies"
489
+ + " that it holds MLPs as an intended part of its investment strategy.",
490
+ )
491
+ holds_preferred_stock: Optional[bool] = Field(
492
+ default=None,
493
+ description="If true, the ETF's investment objective explicitly specifies"
494
+ + " that it holds preferred stock as an intended part of its investment strategy.",
495
+ )
496
+ holds_closed_end_funds: Optional[bool] = Field(
497
+ default=None,
498
+ description="If true, the ETF's investment objective explicitly specifies"
499
+ + " that it holds closed end funds as an intended part of its investment strategy.",
500
+ )
501
+ holds_adr: Optional[bool] = Field(
502
+ default=None,
503
+ description="If true, he ETF's investment objective explicitly specifies that it holds"
504
+ + " American Depositary Receipts (ADRs) as an intended part of its investment strategy.",
505
+ )
506
+ laddered: Optional[bool] = Field(
507
+ default=None,
508
+ description="For bond ETFs, this field identifies those ETFs that"
509
+ + " specifically hold bonds in a laddered structure,"
510
+ + " where the bonds are scheduled to mature in an annual, sequential structure.",
511
+ )
512
+ zero_coupon: Optional[bool] = Field(
513
+ default=None,
514
+ description="For bond ETFs, this field identifies those ETFs that specifically hold zero coupon Treasury Bills.",
515
+ )
516
+ floating_rate: Optional[bool] = Field(
517
+ default=None,
518
+ description="For bond ETFs, this field identifies those ETFs that specifically hold floating rate bonds.",
519
+ )
520
+ build_america_bonds: Optional[bool] = Field(
521
+ default=None,
522
+ description="For municipal bond ETFs, this field identifies those"
523
+ + " ETFs that specifically hold Build America Bonds.",
524
+ )
525
+ dynamic_futures_roll: Optional[bool] = Field(
526
+ default=None,
527
+ description="If the product holds futures contracts, this field identifies those products where the roll strategy"
528
+ + " is dynamic (rather than entirely rules based), so as to minimize roll costs.",
529
+ )
530
+ currency_hedged: Optional[bool] = Field(
531
+ default=None,
532
+ description="This field is populated if the ETF's strategy involves hedging currency exposure.",
533
+ )
534
+ includes_short_exposure: Optional[bool] = Field(
535
+ default=None,
536
+ description="This field is populated if the ETF has short exposure"
537
+ + " in any of its holdings e.g. in a long/short or inverse ETF.",
538
+ )
539
+ ucits: Optional[bool] = Field(
540
+ default=None,
541
+ description="If true, the Exchange Traded Product (ETP) is Undertakings for the Collective Investment"
542
+ + " in Transferable Securities (UCITS) compliant",
543
+ )
544
+ registered_countries: Optional[str] = Field(
545
+ default=None,
546
+ description="The list of countries where the ETF is legally registered for sale."
547
+ + " This may differ from where the ETF is domiciled or traded, particularly in Europe.",
548
+ )
549
+ issuer_country: Optional[str] = Field(
550
+ default=None,
551
+ description="2 letter ISO country code for the country where the issuer is located.",
552
+ )
553
+ domicile: Optional[str] = Field(
554
+ default=None,
555
+ description="2 letter ISO country code for the country where the ETP is domiciled.",
556
+ )
557
+ listing_country: Optional[str] = Field(
558
+ default=None,
559
+ description="2 letter ISO country code for the country of the primary listing.",
560
+ alias="listing_country_code",
561
+ )
562
+ listing_region: Optional[str] = Field(
563
+ default=None,
564
+ description="Geographic region in the country of the primary listing falls.",
565
+ )
566
+ bond_currency_denomination: Optional[str] = Field(
567
+ default=None,
568
+ description="For all bond ETFs, this field provides additional"
569
+ + " detail on the currency denomination of the underlying securities.",
570
+ )
571
+ base_currency: Optional[str] = Field(
572
+ default=None,
573
+ description="Base currency in which NAV is reported.",
574
+ )
575
+ listing_currency: Optional[str] = Field(
576
+ default=None,
577
+ description="Listing currency of the Exchange Traded Product (ETP) in which it is traded."
578
+ + " Reported using the 3-digit ISO currency code.",
579
+ )
580
+ number_of_holdings: Optional[int] = Field(
581
+ default=None,
582
+ description="The number of holdings in the ETF.",
583
+ )
584
+ month_end_assets: Optional[float] = Field(
585
+ default=None,
586
+ description="Net assets in millions of dollars as of the most recent month end.",
587
+ )
588
+ net_expense_ratio: Optional[float] = Field(
589
+ default=None,
590
+ description="Gross expense net of Fee Waivers, as a percentage of net assets"
591
+ + " as published by the ETF issuer.",
592
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
593
+ )
594
+ etf_portfolio_turnover: Optional[float] = Field(
595
+ default=None,
596
+ description="The percentage of positions turned over in the last 12 months.",
597
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
598
+ )
599
+
600
+
601
+ class IntrinioEtfInfoFetcher(
602
+ Fetcher[IntrinioEtfInfoQueryParams, List[IntrinioEtfInfoData]]
603
+ ):
604
+ """Intrinio ETF Info Fetcher."""
605
+
606
+ @staticmethod
607
+ def transform_query(params: Dict[str, Any]) -> IntrinioEtfInfoQueryParams:
608
+ """Transform query."""
609
+ return IntrinioEtfInfoQueryParams(**params)
610
+
611
+ @staticmethod
612
+ async def aextract_data(
613
+ query: IntrinioEtfInfoQueryParams,
614
+ credentials: Optional[Dict[str, str]],
615
+ **kwargs: Any,
616
+ ) -> List[Dict]:
617
+ """Return the raw data from the Intrinio endpoint."""
618
+
619
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
620
+ base_url = "https://api-v2.intrinio.com/etfs/"
621
+ symbols = query.symbol.split(",")
622
+ symbols = [
623
+ symbol + ":US" if ":" not in symbol else symbol for symbol in symbols
624
+ ]
625
+ urls = [f"{base_url}{symbol}?api_key={api_key}" for symbol in symbols]
626
+
627
+ results = []
628
+
629
+ async def response_callback(response, _):
630
+ """Response callback."""
631
+ result = await response.json()
632
+ if "error" in result:
633
+ warn(f"Symbol Error: {result['error']} for {response.url.parts[-1]}")
634
+ return
635
+ _ = result.pop("messages", None)
636
+ results.append(result)
637
+
638
+ await amake_requests(urls, response_callback, **kwargs) # type: ignore
639
+
640
+ if not results:
641
+ raise EmptyDataError("No data was returned.")
642
+
643
+ return sorted(
644
+ results,
645
+ key=(lambda item: (symbols.index(item.get("figi_ticker", len(symbols))))),
646
+ )
647
+
648
+ @staticmethod
649
+ def transform_data(
650
+ query: IntrinioEtfInfoQueryParams,
651
+ data: List[Dict],
652
+ **kwargs: Any,
653
+ ) -> List[IntrinioEtfInfoData]:
654
+ """Transform data."""
655
+ return [IntrinioEtfInfoData.model_validate(d) for d in data]
openbb_platform/providers/intrinio/openbb_intrinio/models/etf_price_performance.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio ETF Performance Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ import asyncio
6
+ from datetime import date as dateType
7
+ from typing import Any, Dict, List, Literal, Optional
8
+ from warnings import warn
9
+
10
+ from openbb_core.provider.abstract.fetcher import Fetcher
11
+ from openbb_core.provider.standard_models.recent_performance import (
12
+ RecentPerformanceData,
13
+ RecentPerformanceQueryParams,
14
+ )
15
+ from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS
16
+ from openbb_core.provider.utils.errors import EmptyDataError
17
+ from openbb_core.provider.utils.helpers import amake_request
18
+ from openbb_intrinio.utils.references import ETF_PERFORMANCE_MAP
19
+ from pydantic import Field
20
+
21
+
22
+ class IntrinioEtfPricePerformanceQueryParams(RecentPerformanceQueryParams):
23
+ """
24
+ Intrinio ETF Performance Query Params.
25
+
26
+ Source: https://docs.intrinio.com/documentation/web_api/get_etf_stats_v2
27
+ Source: https://docs.intrinio.com/documentation/web_api/get_etf_analytics_v2
28
+ """
29
+
30
+ __json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
31
+
32
+ return_type: Literal["trailing", "calendar"] = Field(
33
+ default="trailing",
34
+ description="The type of returns to return, a trailing or calendar window.",
35
+ )
36
+ adjustment: Literal["splits_only", "splits_and_dividends"] = Field(
37
+ default="splits_and_dividends",
38
+ description="The adjustment factor, 'splits_only' will return pure price performance.",
39
+ )
40
+
41
+
42
+ class IntrinioEtfPricePerformanceData(RecentPerformanceData):
43
+ """Intrinio ETF Performance Data."""
44
+
45
+ __alias_dict__ = {
46
+ "updated": "date",
47
+ "year_high": "fifty_two_week_high",
48
+ "year_low": "fifty_two_week_low",
49
+ "volume": "volume_traded",
50
+ "volume_avg_30": "average_daily_volume_one_month",
51
+ "volume_avg_90": "average_daily_volume_three_month",
52
+ "volume_avg_180": "average_daily_volume_six_month",
53
+ }
54
+
55
+ max_annualized: Optional[float] = Field(
56
+ default=None,
57
+ description="Annualized rate of return from inception.",
58
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
59
+ )
60
+ volatility_one_year: Optional[float] = Field(
61
+ default=None,
62
+ description="Trailing one-year annualized volatility.",
63
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
64
+ )
65
+ volatility_three_year: Optional[float] = Field(
66
+ default=None,
67
+ description="Trailing three-year annualized volatility.",
68
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
69
+ )
70
+ volatility_five_year: Optional[float] = Field(
71
+ default=None,
72
+ description="Trailing five-year annualized volatility.",
73
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
74
+ )
75
+ volume: Optional[int] = Field(
76
+ default=None,
77
+ description=DATA_DESCRIPTIONS.get("volume", ""),
78
+ )
79
+ volume_avg_30: Optional[float] = Field(
80
+ default=None,
81
+ description="The one-month average daily volume.",
82
+ )
83
+ volume_avg_90: Optional[float] = Field(
84
+ default=None,
85
+ description="The three-month average daily volume.",
86
+ )
87
+ volume_avg_180: Optional[float] = Field(
88
+ default=None,
89
+ description="The six-month average daily volume.",
90
+ )
91
+ beta: Optional[float] = Field(
92
+ default=None,
93
+ description="Beta compared to the S&P 500.",
94
+ )
95
+ nav: Optional[float] = Field(
96
+ default=None,
97
+ description="Net asset value per share.",
98
+ )
99
+ year_high: Optional[float] = Field(
100
+ default=None,
101
+ description="The 52-week high price.",
102
+ )
103
+ year_low: Optional[float] = Field(
104
+ default=None,
105
+ description="The 52-week low price.",
106
+ )
107
+
108
+ market_cap: Optional[float] = Field(
109
+ default=None,
110
+ description="The market capitalization.",
111
+ )
112
+ shares_outstanding: Optional[int] = Field(
113
+ default=None,
114
+ description="The number of shares outstanding.",
115
+ )
116
+ updated: Optional[dateType] = Field(
117
+ default=None,
118
+ description=DATA_DESCRIPTIONS.get("date", ""),
119
+ )
120
+
121
+
122
+ class IntrinioEtfPricePerformanceFetcher(
123
+ Fetcher[
124
+ IntrinioEtfPricePerformanceQueryParams, List[IntrinioEtfPricePerformanceData]
125
+ ]
126
+ ):
127
+ """Intrinio ETF Performance Fetcher."""
128
+
129
+ @staticmethod
130
+ def transform_query(
131
+ params: Dict[str, Any]
132
+ ) -> IntrinioEtfPricePerformanceQueryParams:
133
+ """Transform query."""
134
+ return IntrinioEtfPricePerformanceQueryParams(**params)
135
+
136
+ @staticmethod
137
+ async def aextract_data(
138
+ query: IntrinioEtfPricePerformanceQueryParams,
139
+ credentials: Optional[Dict[str, str]],
140
+ **kwargs: Any,
141
+ ) -> List[Dict]:
142
+ """Return the raw data from the Intrinio endpoint."""
143
+
144
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
145
+ base_url = "https://api-v2.intrinio.com/etfs/"
146
+ symbols = query.symbol.split(",")
147
+ symbols = [
148
+ symbol + ":US" if ":" not in symbol else symbol for symbol in symbols
149
+ ]
150
+
151
+ adjustment = (
152
+ "split_only"
153
+ if query.adjustment == "splits_and_dividends"
154
+ else "split_and_dividend"
155
+ )
156
+
157
+ return_type = "trailing" if query.return_type == "calendar" else "calendar"
158
+
159
+ results = []
160
+
161
+ async def get_one(symbol: str, **kwargs):
162
+ """Get data for one symbol."""
163
+
164
+ url = f"{base_url}{symbol}/stats?api_key={api_key}"
165
+ result = await amake_request(url, **kwargs)
166
+
167
+ if "message" in result and result["message"] != []: # type: ignore
168
+ warn(f"Symbol Error: {symbol} - {result['message']}") # type: ignore
169
+ return
170
+ _ = result.pop("message", None) # type: ignore
171
+ _ = result.pop("messages", None) # type: ignore
172
+
173
+ data = {}
174
+ etf = result.pop("etf", {}) # type: ignore
175
+ data["symbol"] = etf.get("ticker")
176
+ # These items will be kept regardless of the adjustment and return_type.
177
+ keep = ["volatility", "month", "year_to_date"]
178
+ for k, v in result.copy().items(): # type: ignore
179
+ if not any(substring in k for substring in keep):
180
+ _ = result.pop(k, None) if adjustment in k else None # type: ignore
181
+ _ = result.pop(k, None) if return_type in k else None # type: ignore
182
+ if k in result:
183
+ data[ETF_PERFORMANCE_MAP.get(k, k)] = v
184
+ # Get an additional set of data to combine with the first set.
185
+ analytics_url = (
186
+ f"https://api-v2.intrinio.com/etfs/{symbol}/analytics?api_key={api_key}"
187
+ )
188
+ if data:
189
+ analytics = await amake_request(analytics_url, **kwargs)
190
+ if "messages" in analytics and analytics["messages"] != []: # type: ignore
191
+ warn(
192
+ f"Symbol Error: {analytics['messages']}" # type: ignore
193
+ + f"for {etf.get('ticker')}" # type: ignore
194
+ )
195
+ return
196
+ # Remove the duplicate data from the analytics response.
197
+ _ = analytics.pop("messages", None) # type: ignore
198
+ _ = analytics.pop("etf", None) # type: ignore
199
+ _ = analytics.pop("date", None) # type: ignore
200
+
201
+ data.update(analytics) # type: ignore
202
+
203
+ results.append(data)
204
+
205
+ tasks = [get_one(symbol, **kwargs) for symbol in symbols]
206
+
207
+ await asyncio.gather(*tasks)
208
+
209
+ if not results:
210
+ raise EmptyDataError("No data was returned.")
211
+
212
+ # Undo any formatting changes made to the symbols before sorting.
213
+ symbols = query.symbol.replace(":US", "").split(",")
214
+
215
+ return sorted(
216
+ results,
217
+ key=(lambda item: (symbols.index(item.get("symbol", len(symbols))))),
218
+ )
219
+
220
+ @staticmethod
221
+ def transform_data(
222
+ query: IntrinioEtfPricePerformanceQueryParams,
223
+ data: List[Dict],
224
+ **kwargs: Any,
225
+ ) -> List[IntrinioEtfPricePerformanceData]:
226
+ """Transform data."""
227
+ return [IntrinioEtfPricePerformanceData.model_validate(d) for d in data]
openbb_platform/providers/intrinio/openbb_intrinio/models/etf_search.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio ETF Search Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from typing import Any, Dict, List, Optional, Union
6
+
7
+ from openbb_core.app.model.abstract.error import OpenBBError
8
+ from openbb_core.provider.abstract.fetcher import Fetcher
9
+ from openbb_core.provider.standard_models.etf_search import (
10
+ EtfSearchData,
11
+ EtfSearchQueryParams,
12
+ )
13
+ from openbb_core.provider.utils.errors import EmptyDataError
14
+ from openbb_intrinio.utils.references import ETF_EXCHANGES
15
+ from pydantic import Field
16
+
17
+
18
+ class IntrinioEtfSearchQueryParams(EtfSearchQueryParams):
19
+ """
20
+ Intrinio ETF Search Query Params.
21
+
22
+ Source: https://docs.intrinio.com/documentation/web_api/search_etfs_v2
23
+ """
24
+
25
+ exchange: Union[None, ETF_EXCHANGES] = Field(
26
+ default=None,
27
+ description="Target a specific exchange by providing the MIC code.",
28
+ )
29
+
30
+
31
+ class IntrinioEtfSearchData(EtfSearchData):
32
+ """Intrinio ETF Search Data."""
33
+
34
+ __alias_dict__ = {
35
+ "intrinio_id": "id",
36
+ "symbol": "ticker",
37
+ "exchange": "exchange_mic",
38
+ }
39
+
40
+ exchange: Optional[str] = Field(
41
+ default=None,
42
+ description="The exchange MIC code.",
43
+ )
44
+ figi_ticker: Optional[str] = Field(
45
+ None,
46
+ description="The OpenFIGI ticker.",
47
+ )
48
+ ric: Optional[str] = Field(
49
+ None,
50
+ description="The Reuters Instrument Code.",
51
+ )
52
+ isin: Optional[str] = Field(
53
+ None,
54
+ description="The International Securities Identification Number.",
55
+ )
56
+ sedol: Optional[str] = Field(
57
+ None,
58
+ description="The Stock Exchange Daily Official List.",
59
+ )
60
+ intrinio_id: Optional[str] = Field(
61
+ None,
62
+ description="The unique Intrinio ID for the security.",
63
+ )
64
+
65
+
66
+ class IntrinioEtfSearchFetcher(
67
+ Fetcher[IntrinioEtfSearchQueryParams, List[IntrinioEtfSearchData]]
68
+ ):
69
+ """Intrinio ETF Search Fetcher."""
70
+
71
+ @staticmethod
72
+ def transform_query(params: Dict[str, Any]) -> IntrinioEtfSearchQueryParams:
73
+ """Transform query."""
74
+ return IntrinioEtfSearchQueryParams(**params)
75
+
76
+ @staticmethod
77
+ async def aextract_data(
78
+ query: IntrinioEtfSearchQueryParams,
79
+ credentials: Optional[Dict[str, str]],
80
+ **kwargs: Any,
81
+ ) -> List[Dict]:
82
+ """Return the raw data from the Intrinio endpoint."""
83
+ # pylint: disable=import-outside-toplevel
84
+ from openbb_core.provider.utils.helpers import (
85
+ ClientResponse,
86
+ ClientSession,
87
+ amake_request,
88
+ )
89
+
90
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
91
+ BASE = "https://api-v2.intrinio.com/etfs"
92
+ if query.exchange is not None:
93
+ url = f"{BASE}?exchange={query.exchange.upper()}&page_size=10000&api_key={api_key}"
94
+ elif query.query:
95
+ url = f"{BASE}/search?query={query.query}&page_size=10000&api_key={api_key}"
96
+ else:
97
+ url = f"{BASE}?page_size=10000&api_key={api_key}"
98
+
99
+ data: List = []
100
+
101
+ async def response_callback(response: ClientResponse, session: ClientSession):
102
+ """Async response callback."""
103
+ results = await response.json()
104
+
105
+ if results.get("messages"): # type: ignore
106
+ messages = results.get("messages") # type: ignore
107
+ raise OpenBBError(str(messages))
108
+
109
+ if results.get("etfs") and len(results.get("etfs")) > 0: # type: ignore
110
+ data.extend(results.get("etfs")) # type: ignore
111
+ while results.get("next_page"): # type: ignore
112
+ next_page = results["next_page"] # type: ignore
113
+ next_url = f"{url}&next_page={next_page}"
114
+ results = await amake_request(next_url, session=session, **kwargs)
115
+ if (
116
+ "etfs" in results
117
+ and len(results.get("etfs")) > 0 # type: ignore
118
+ ):
119
+ data.extend(results.get("etfs")) # type: ignore
120
+ return data
121
+
122
+ return await amake_request(url, response_callback=response_callback, **kwargs) # type: ignore
123
+
124
+ @staticmethod
125
+ def transform_data(
126
+ query: IntrinioEtfSearchQueryParams,
127
+ data: List[Dict],
128
+ **kwargs: Any,
129
+ ) -> List[IntrinioEtfSearchData]:
130
+ """Transform data."""
131
+ # pylint: disable=import-outside-toplevel
132
+ import re # noqa
133
+ from pandas import DataFrame # noqa
134
+
135
+ if not data:
136
+ raise EmptyDataError("No data found.")
137
+
138
+ results = DataFrame(data)
139
+ if query.query:
140
+ pattern = f".*{re.escape(query.query)}.*"
141
+ results = results[
142
+ results["name"].str.contains(pattern, case=False, regex=True)
143
+ ]
144
+
145
+ return [
146
+ IntrinioEtfSearchData.model_validate(d)
147
+ for d in results.to_dict(orient="records")
148
+ ]
openbb_platform/providers/intrinio/openbb_intrinio/models/financial_attributes.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Financial Attributes Model."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from dateutil.relativedelta import relativedelta
7
+ from openbb_core.provider.abstract.fetcher import Fetcher
8
+ from openbb_core.provider.standard_models.financial_attributes import (
9
+ FinancialAttributesData,
10
+ FinancialAttributesQueryParams,
11
+ )
12
+ from openbb_core.provider.utils.helpers import get_querystring
13
+ from openbb_intrinio.utils.helpers import get_data_many
14
+
15
+
16
+ class IntrinioFinancialAttributesQueryParams(FinancialAttributesQueryParams):
17
+ """Intrinio Financial Attributes Query."""
18
+
19
+ __alias_dict__ = {"sort": "sort_order", "limit": "page_size"}
20
+
21
+
22
+ class IntrinioFinancialAttributesData(FinancialAttributesData):
23
+ """Intrinio Financial Attributes Data."""
24
+
25
+
26
+ class IntrinioFinancialAttributesFetcher(
27
+ Fetcher[
28
+ IntrinioFinancialAttributesQueryParams,
29
+ List[IntrinioFinancialAttributesData],
30
+ ]
31
+ ):
32
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
33
+
34
+ @staticmethod
35
+ def transform_query(
36
+ params: Dict[str, Any]
37
+ ) -> IntrinioFinancialAttributesQueryParams:
38
+ """Transform the query params."""
39
+ transformed_params = params
40
+
41
+ now = datetime.now().date()
42
+ if params.get("start_date") is None:
43
+ transformed_params["start_date"] = now - relativedelta(years=5)
44
+
45
+ if params.get("end_date") is None:
46
+ transformed_params["end_date"] = now
47
+
48
+ return IntrinioFinancialAttributesQueryParams(**transformed_params)
49
+
50
+ @staticmethod
51
+ async def aextract_data(
52
+ query: IntrinioFinancialAttributesQueryParams, # pylint: disable=unused-argument
53
+ credentials: Optional[Dict[str, str]],
54
+ **kwargs: Any,
55
+ ) -> List[Dict]:
56
+ """Return the raw data from the Intrinio endpoint."""
57
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
58
+ frequency = "yearly" if query.period == "annual" else "quarterly"
59
+ data: List[Dict] = []
60
+
61
+ base_url = "https://api-v2.intrinio.com"
62
+ query_str = get_querystring(query.model_dump(by_alias=True), ["frequency"])
63
+ query_str = f"{query_str}&frequency={frequency}"
64
+
65
+ url = f"{base_url}/historical_data/{query.symbol}/{query.tag}?{query_str}&api_key={api_key}"
66
+ # data = get_data_one(url).get("historical_data", [])
67
+ data = await get_data_many(url, "historical_data")
68
+
69
+ return data
70
+
71
+ @staticmethod
72
+ def transform_data(
73
+ query: IntrinioFinancialAttributesQueryParams,
74
+ data: List[Dict],
75
+ **kwargs: Any,
76
+ ) -> List[IntrinioFinancialAttributesData]:
77
+ """Return the transformed data."""
78
+ return [IntrinioFinancialAttributesData.model_validate(item) for item in data]
openbb_platform/providers/intrinio/openbb_intrinio/models/financial_ratios.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Financial Ratios Model."""
2
+
3
+ from typing import Any, Dict, List, Literal, Optional
4
+ from warnings import warn
5
+
6
+ from openbb_core.app.model.abstract.error import OpenBBError
7
+ from openbb_core.provider.abstract.fetcher import Fetcher
8
+ from openbb_core.provider.standard_models.financial_ratios import (
9
+ FinancialRatiosData,
10
+ FinancialRatiosQueryParams,
11
+ )
12
+ from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
13
+ from openbb_core.provider.utils.helpers import ClientResponse, amake_requests
14
+ from openbb_intrinio.utils.helpers import get_data_one
15
+ from pydantic import Field, field_validator
16
+
17
+
18
+ class IntrinioFinancialRatiosQueryParams(FinancialRatiosQueryParams):
19
+ """Intrinio Financial Ratios Query.
20
+
21
+ Source: https://docs.intrinio.com/documentation/web_api/get_company_fundamentals_v2
22
+ Source: https://docs.intrinio.com/documentation/web_api/get_fundamental_standardized_financials_v2
23
+ """
24
+
25
+ __json_schema_extra__ = {
26
+ "period": {
27
+ "choices": ["annual", "quarter", "ttm", "ytd"],
28
+ }
29
+ }
30
+ period: Literal["annual", "quarter", "ttm", "ytd"] = Field(
31
+ default="annual",
32
+ description=QUERY_DESCRIPTIONS.get("period", ""),
33
+ )
34
+ fiscal_year: Optional[int] = Field(
35
+ default=None,
36
+ description="The specific fiscal year. Reports do not go beyond 2008.",
37
+ )
38
+
39
+ @field_validator("symbol", mode="after", check_fields=False)
40
+ @classmethod
41
+ def handle_symbol(cls, v) -> str:
42
+ """Handle symbols with a dash and replace it with a dot for Intrinio."""
43
+ return v.replace("-", ".")
44
+
45
+
46
+ class IntrinioFinancialRatiosData(FinancialRatiosData):
47
+ """Intrinio Financial Ratios Data."""
48
+
49
+ __alias_dict__ = {
50
+ "net_operating_profit_after_tax_margin": "nopatmargin",
51
+ "invested_capital_turnover": "investedcapitalturnover",
52
+ "book_value_per_share": "bookvaluepershare",
53
+ "tangible_book_value": "tangiblebookvaluepershare",
54
+ "price_to_book_ratio": "pricetobook",
55
+ "price_to_tangible_book_ratio": "pricetotangiblebook",
56
+ "price_to_revenue": "pricetorevenue",
57
+ "price_to_earnings": "pricetoearnings",
58
+ "dividend_yield": "dividendyield",
59
+ "earnings_yield": "earningsyield",
60
+ "ev_to_invested_capital": "evtoinvestedcapital",
61
+ "ev_to_sales": "evtorevenue",
62
+ "ev_to_ebitda": "evtoebitda",
63
+ "ev_to_ebit": "evtoebit",
64
+ "ev_to_nopat": "evtonopat",
65
+ "ev_to_operating_cash_flow": "evtoocf",
66
+ "ev_to_free_cash_flow": "evtofcff",
67
+ "gross_margin": "grossmargin",
68
+ "ebitda_margin": "ebitdamargin",
69
+ "operating_margin": "operatingmargin",
70
+ "ebit_margin": "ebitmargin",
71
+ "net_profit_margin": "profitmargin",
72
+ "cost_of_rev_to_revenue": "costofrevtorevenue",
73
+ "sga_expense_to_revenue": "sgaextorevenue",
74
+ "rd_expense_to_revenue": "rdextorevenue",
75
+ "op_expense_to_revenue": "opextorevenue",
76
+ "tax_burden_percent": "taxburdenpct",
77
+ "interest_burden_percent": "interestburdenpct",
78
+ "effective_tax_rate": "efftaxrate",
79
+ "asset_turnover": "assetturnover",
80
+ "receivables_turnover": "arturnover",
81
+ "inventory_turnover": "invturnover",
82
+ "fixed_asset_turnover": "faturnover",
83
+ "payables_turnover": "apturnover",
84
+ "days_of_sales_outstanding": "dso",
85
+ "days_of_inventory_outstanding": "dio",
86
+ "days_payable_outstanding": "dpo",
87
+ "cash_conversion_cycle": "ccc",
88
+ "financial_leverage": "finleverage",
89
+ "leverage_ratio": "leverageratio",
90
+ "compound_leverage_factor": "compoundleveragefactor",
91
+ "long_term_debt_equity_ratio": "ltdebttoequity",
92
+ "debt_equity_ratio": "debttoequity",
93
+ "return_on_invested_capital": "roic",
94
+ "net_non_operating_expense_percent": "nnep",
95
+ "roic_nnep_spread": "roicnnepspread",
96
+ "return_on_net_non_operating_assets": "rnnoa",
97
+ "return_on_equity": "roe",
98
+ "cash_returned_on_invested_capitals": "croic",
99
+ "operating_return_on_assets": "oroa",
100
+ "return_on_assets": "roa",
101
+ "non_controlling_interest_sharing_ratio": "noncontrollinginterestsharingratio",
102
+ "return_on_common_equity": "roce",
103
+ "dividend_payout_ratio": "divpayoutratio",
104
+ "augmented_payout_ratio": "augmentedpayoutratio",
105
+ "operating_cash_flow_to_capex": "ocftocapex",
106
+ "short_term_debt_to_capitalization": "stdebttocap",
107
+ "long_term_debt_to_capitalization": "ltdebttocap",
108
+ "debt_to_capitalization": "debttototalcapital",
109
+ "preferred_equity_to_capitalization": "preferredtocap",
110
+ "non_controlling_interests_to_capitalization": "noncontrolinttocap",
111
+ "equity_to_capitalization": "commontocap",
112
+ "debt_to_ebitda": "debttoebitda",
113
+ "net_debt_to_ebitda": "netdebttoebitda",
114
+ "long_term_debt_to_ebita": "ltdebttoebitda",
115
+ "debt_to_nopat": "debttonopat",
116
+ "net_debt_to_nopat": "netdebttonopat",
117
+ "long_term_debt_to_nopat": "ltdebttonopat",
118
+ "altman_z_score": "altmanzscore",
119
+ "ebit_to_interest_expense": "ebittointerestex",
120
+ "nopat_to_intersest_expense": "nopattointerestex",
121
+ "ebit_less_capex_to_interest_expense": "ebitlesscapextointerestex",
122
+ "nopat_less_capex_to_interest_expense": "nopatlesscapextointex",
123
+ "operating_cash_flow_to_interest_expense": "ocftointerestex",
124
+ "operating_cash_flow_less_capex_to_interest_expense": "ocflesscapextointerestex",
125
+ "free_cash_flow_to_interest_expense": "fcfftointerestex",
126
+ "current_ratio": "curratio",
127
+ "quick_ratio": "quickratio",
128
+ "debt_free_cash_free_nwc_to_revenue": "dfcfnwctorev",
129
+ "debt_free_nwc_to_revenue": "dfnwctorev",
130
+ "net_working_capital_to_revenue": "nwctorev",
131
+ "normalized_nopat_margin": "normalizednopatmargin",
132
+ "pre_tax_income_margin": "pretaxincomemargin",
133
+ "adjusted_basic_eps": "adjbasiceps",
134
+ "adjusted_diluted_eps": "adjdilutedeps",
135
+ "adjusted_basic_diluted_eps": "adjbasicdilutedeps",
136
+ "return_on_equity_simple": "roe_simple",
137
+ }
138
+
139
+
140
+ class IntrinioFinancialRatiosFetcher(
141
+ Fetcher[
142
+ IntrinioFinancialRatiosQueryParams,
143
+ List[IntrinioFinancialRatiosData],
144
+ ]
145
+ ):
146
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
147
+
148
+ @staticmethod
149
+ def transform_query(params: Dict[str, Any]) -> IntrinioFinancialRatiosQueryParams:
150
+ """Transform the query params."""
151
+ return IntrinioFinancialRatiosQueryParams(**params)
152
+
153
+ @staticmethod
154
+ async def aextract_data(
155
+ query: IntrinioFinancialRatiosQueryParams,
156
+ credentials: Optional[Dict[str, str]],
157
+ **kwargs: Any,
158
+ ) -> List[Dict]:
159
+ """Return the raw data from the Intrinio endpoint."""
160
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
161
+ statement_code = "calculations"
162
+ if query.period in ["quarter", "annual"]:
163
+ period_type = "FY" if query.period == "annual" else "QTR"
164
+ elif query.period in ["ttm", "ytd"]:
165
+ period_type = query.period.upper()
166
+ else:
167
+ raise OpenBBError(f"Period '{query.period}' not supported.")
168
+
169
+ fundamentals_data: Dict = {}
170
+
171
+ base_url = "https://api-v2.intrinio.com"
172
+ fundamentals_url = (
173
+ f"{base_url}/companies/{query.symbol}/fundamentals?"
174
+ f"statement_code={statement_code}&type={period_type}"
175
+ )
176
+ if query.fiscal_year is not None:
177
+ if query.fiscal_year < 2008:
178
+ warn("Financials data is only available from 2008 and later.")
179
+ query.fiscal_year = 2008
180
+ fundamentals_url = fundamentals_url + f"&fiscal_year={query.fiscal_year}"
181
+ fundamentals_url = fundamentals_url + f"&api_key={api_key}"
182
+ fundamentals_data = (await get_data_one(fundamentals_url, **kwargs)).get(
183
+ "fundamentals", []
184
+ )
185
+ ids = [item["id"] for item in fundamentals_data]
186
+ ids = ids[: query.limit]
187
+
188
+ async def callback(response: ClientResponse, _: Any) -> Dict:
189
+ """Return the response."""
190
+ statement_data = await response.json()
191
+ return {
192
+ "period_ending": statement_data["fundamental"]["end_date"], # type: ignore
193
+ "fiscal_year": statement_data["fundamental"]["fiscal_year"], # type: ignore
194
+ "fiscal_period": statement_data["fundamental"]["fiscal_period"], # type: ignore
195
+ "calculations": statement_data["standardized_financials"], # type: ignore
196
+ }
197
+
198
+ urls = [
199
+ f"https://api-v2.intrinio.com/fundamentals/{id}/standardized_financials?api_key={api_key}"
200
+ for id in ids
201
+ ]
202
+
203
+ return await amake_requests(urls, callback, **kwargs) # type: ignore
204
+
205
+ @staticmethod
206
+ def transform_data(
207
+ query: IntrinioFinancialRatiosQueryParams, data: List[Dict], **kwargs: Any
208
+ ) -> List[IntrinioFinancialRatiosData]:
209
+ """Return the transformed data."""
210
+ transformed_data: List[IntrinioFinancialRatiosData] = []
211
+
212
+ tags = [
213
+ "nopatmargin",
214
+ "investedcapitalturnover",
215
+ "bookvaluepershare",
216
+ "tangiblebookvaluepershare",
217
+ "pricetobook",
218
+ "pricetotangiblebook",
219
+ "pricetorevenue",
220
+ "pricetoearnings",
221
+ "dividendyield",
222
+ "earningsyield",
223
+ "evtoinvestedcapital",
224
+ "evtorevenue",
225
+ "evtoebitda",
226
+ "evtoebit",
227
+ "evtonopat",
228
+ "evtoocf",
229
+ "evtofcff",
230
+ "grossmargin",
231
+ "ebitdamargin",
232
+ "operatingmargin",
233
+ "ebitmargin",
234
+ "profitmargin",
235
+ "costofrevtorevenue",
236
+ "sgaextorevenue",
237
+ "rdextorevenue",
238
+ "opextorevenue",
239
+ "taxburdenpct",
240
+ "interestburdenpct",
241
+ "efftaxrate",
242
+ "assetturnover",
243
+ "arturnover",
244
+ "invturnover",
245
+ "faturnover",
246
+ "apturnover",
247
+ "dso",
248
+ "dio",
249
+ "dpo",
250
+ "ccc",
251
+ "finleverage",
252
+ "leverageratio",
253
+ "compoundleveragefactor",
254
+ "ltdebttoequity",
255
+ "debttoequity",
256
+ "roic",
257
+ "nnep",
258
+ "roicnnepspread",
259
+ "rnnoa",
260
+ "roe",
261
+ "croic",
262
+ "oroa",
263
+ "roa",
264
+ "noncontrollinginterestsharingratio",
265
+ "roce",
266
+ "divpayoutratio",
267
+ "augmentedpayoutratio",
268
+ "ocftocapex",
269
+ "stdebttocap",
270
+ "ltdebttocap",
271
+ "debttototalcapital",
272
+ "preferredtocap",
273
+ "noncontrolinttocap",
274
+ "commontocap",
275
+ "debttoebitda",
276
+ "netdebttoebitda",
277
+ "ltdebttoebitda",
278
+ "debttonopat",
279
+ "netdebttonopat",
280
+ "ltdebttonopat",
281
+ "altmanzscore",
282
+ "ebittointerestex",
283
+ "nopattointerestex",
284
+ "ebitlesscapextointerestex",
285
+ "nopatlesscapextointex",
286
+ "ocftointerestex",
287
+ "ocflesscapextointerestex",
288
+ "fcfftointerestex",
289
+ "curratio",
290
+ "quickratio",
291
+ "dfcfnwctorev",
292
+ "dfnwctorev",
293
+ "nwctorev",
294
+ "normalizednopatmargin",
295
+ "pretaxincomemargin",
296
+ "adjbasiceps",
297
+ "adjdilutedeps",
298
+ "adjbasicdilutedeps",
299
+ "roe_simple",
300
+ ]
301
+
302
+ for item in data:
303
+ sub_dict: Dict[str, Any] = {}
304
+
305
+ for sub_item in item["calculations"]:
306
+ field_name = sub_item["data_tag"]["tag"]
307
+ if field_name in tags:
308
+ sub_dict[field_name] = (
309
+ float(sub_item["value"])
310
+ if sub_item["value"] and sub_item["value"] != 0
311
+ else None
312
+ )
313
+
314
+ sub_dict["period_ending"] = item["period_ending"]
315
+ sub_dict["fiscal_year"] = item["fiscal_year"]
316
+ sub_dict["fiscal_period"] = item["fiscal_period"]
317
+
318
+ transformed_data.append(IntrinioFinancialRatiosData(**sub_dict))
319
+
320
+ return transformed_data
openbb_platform/providers/intrinio/openbb_intrinio/models/forward_ebitda_estimates.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Forward EBITDA Estimates Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ import asyncio
6
+ from typing import Any, Dict, List, Literal, Optional
7
+ from warnings import warn
8
+
9
+ from openbb_core.app.model.abstract.error import OpenBBError
10
+ from openbb_core.provider.abstract.fetcher import Fetcher
11
+ from openbb_core.provider.standard_models.forward_ebitda_estimates import (
12
+ ForwardEbitdaEstimatesData,
13
+ ForwardEbitdaEstimatesQueryParams,
14
+ )
15
+ from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
16
+ from openbb_core.provider.utils.helpers import (
17
+ amake_request,
18
+ get_querystring,
19
+ )
20
+ from openbb_intrinio.utils.helpers import response_callback
21
+ from pydantic import Field
22
+
23
+
24
+ class IntrinioForwardEbitdaEstimatesQueryParams(ForwardEbitdaEstimatesQueryParams):
25
+ """Intrinio Forward EBITDA Estimates Query.
26
+
27
+ https://docs.intrinio.com/documentation/web_api/get_zacks_sales_estimates_v2
28
+ """
29
+
30
+ __json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
31
+ __alias_dict__ = {"estimate_type": "type"}
32
+
33
+ fiscal_period: Optional[Literal["annual", "quarter"]] = Field(
34
+ default=None, description="Filter for only full-year or quarterly estimates."
35
+ )
36
+ estimate_type: Optional[
37
+ Literal[
38
+ "ebitda",
39
+ "ebit",
40
+ "enterprise_value",
41
+ "cash_flow_per_share",
42
+ "pretax_income",
43
+ ]
44
+ ] = Field(
45
+ default=None,
46
+ description="Limit the EBITDA estimates to this type.",
47
+ )
48
+
49
+
50
+ class IntrinioForwardEbitdaEstimatesData(ForwardEbitdaEstimatesData):
51
+ """Intrinio Forward EBITDA Estimates Data."""
52
+
53
+ __alias_dict__ = {
54
+ "last_updated": "updated_date",
55
+ "symbol": "ticker",
56
+ "calendar_period": "estimate_month",
57
+ "name": "company_name",
58
+ "fiscal_year": "estimate_year",
59
+ "fiscal_period": "period",
60
+ "low_estimate": "low",
61
+ "high_estimate": "high",
62
+ "number_of_analysts": "estimate_count",
63
+ "standard_deviation": "std_dev",
64
+ }
65
+
66
+ conensus_type: Optional[
67
+ Literal[
68
+ "ebitda",
69
+ "ebitda",
70
+ "ebit",
71
+ "enterprise_value",
72
+ "cash_flow_per_share",
73
+ "pretax_income",
74
+ ]
75
+ ] = Field(
76
+ default=None,
77
+ description="The type of estimate.",
78
+ )
79
+
80
+
81
+ class IntrinioForwardEbitdaEstimatesFetcher(
82
+ Fetcher[
83
+ IntrinioForwardEbitdaEstimatesQueryParams,
84
+ List[IntrinioForwardEbitdaEstimatesData],
85
+ ]
86
+ ):
87
+ """Intrinio Forward EBITDA Estimates Fetcher."""
88
+
89
+ @staticmethod
90
+ def transform_query(
91
+ params: Dict[str, Any]
92
+ ) -> IntrinioForwardEbitdaEstimatesQueryParams:
93
+ """Transform the query params."""
94
+ return IntrinioForwardEbitdaEstimatesQueryParams(**params)
95
+
96
+ @staticmethod
97
+ async def aextract_data(
98
+ query: IntrinioForwardEbitdaEstimatesQueryParams,
99
+ credentials: Optional[Dict[str, str]],
100
+ **kwargs: Any,
101
+ ) -> List[Dict]:
102
+ """Return the raw data from the Intrinio endpoint."""
103
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
104
+ BASE_URL = (
105
+ "https://api-v2.intrinio.com/zacks/ebitda_consensus?"
106
+ + f"page_size=10000&api_key={api_key}"
107
+ )
108
+ symbols = query.symbol.split(",") if query.symbol else None
109
+ query_str = get_querystring(query.model_dump(by_alias=True), ["symbol"])
110
+ results: List[Dict] = []
111
+
112
+ async def get_one(symbol):
113
+ """Get the data for one symbol."""
114
+ url = f"{BASE_URL}&identifier={symbol}"
115
+ url = url + f"&{query_str}" if query_str else url
116
+ data = await amake_request(
117
+ url, response_callback=response_callback, **kwargs
118
+ )
119
+ consensus = (
120
+ data.get("ebitda_consensus")
121
+ if isinstance(data, dict) and "ebitda_consensus" in data
122
+ else []
123
+ )
124
+ if not data or not consensus:
125
+ warn(f"Symbol Error: No data found for {symbol}")
126
+ if consensus:
127
+ results.extend(consensus)
128
+
129
+ if symbols:
130
+ await asyncio.gather(*[get_one(symbol) for symbol in symbols])
131
+ if not results:
132
+ raise EmptyDataError(f"No results were found. -> {query.symbol}")
133
+ return results
134
+
135
+ async def fetch_callback(response, session):
136
+ """Use callback for pagination."""
137
+ data = await response.json()
138
+ error = data.get("error", None)
139
+ if error:
140
+ message = data.get("message", "")
141
+ if "api key" in message.lower():
142
+ raise UnauthorizedError(
143
+ f"Unauthorized Intrinio request -> {message}"
144
+ )
145
+ raise OpenBBError(f"Error: {error} -> {message}")
146
+
147
+ estimates = data.get("ebitda_consensus", []) # type: ignore
148
+ if estimates and len(estimates) > 0:
149
+ results.extend(estimates)
150
+ while data.get("next_page"): # type: ignore
151
+ next_page = data["next_page"] # type: ignore
152
+ next_url = f"{url}&next_page={next_page}"
153
+ data = await amake_request(next_url, session=session, **kwargs)
154
+ consensus = (
155
+ data.get("ebitda_consensus")
156
+ if isinstance(data, dict) and "ebitda_consensus" in data
157
+ else []
158
+ )
159
+ if consensus:
160
+ results.extend(consensus) # type: ignore
161
+ return results
162
+
163
+ url = f"{BASE_URL}&{query_str}" if query_str else BASE_URL
164
+
165
+ results = await amake_request(url, response_callback=fetch_callback, **kwargs) # type: ignore
166
+
167
+ if not results:
168
+ raise EmptyDataError("The request was successful but was returned empty.")
169
+
170
+ return results
171
+
172
+ @staticmethod
173
+ def transform_data(
174
+ query: IntrinioForwardEbitdaEstimatesQueryParams,
175
+ data: List[Dict],
176
+ **kwargs: Any,
177
+ ) -> List[IntrinioForwardEbitdaEstimatesData]:
178
+ """Transform the raw data into the standard format."""
179
+ if not data:
180
+ raise EmptyDataError()
181
+ results: List[IntrinioForwardEbitdaEstimatesData] = []
182
+ fiscal_period = None
183
+ if query.fiscal_period is not None:
184
+ fiscal_period = "fy" if query.fiscal_period == "annual" else "fq"
185
+ for item in data:
186
+ estimate_count = item.get("estimate_count")
187
+ if (
188
+ not estimate_count
189
+ or estimate_count == 0
190
+ or not item.get("updated_date")
191
+ ):
192
+ continue
193
+ if fiscal_period and item.get("period") != fiscal_period:
194
+ continue
195
+ results.append(IntrinioForwardEbitdaEstimatesData.model_validate(item))
196
+ if not results:
197
+ raise EmptyDataError()
198
+
199
+ return sorted(
200
+ results, key=lambda x: (x.fiscal_year, x.last_updated), reverse=True
201
+ )
openbb_platform/providers/intrinio/openbb_intrinio/models/forward_eps_estimates.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Forward EPS Estimates Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ import asyncio
6
+ from datetime import datetime
7
+ from typing import Any, Dict, List, Literal, Optional, Union
8
+ from warnings import warn
9
+
10
+ from openbb_core.app.model.abstract.error import OpenBBError
11
+ from openbb_core.provider.abstract.fetcher import Fetcher
12
+ from openbb_core.provider.standard_models.forward_eps_estimates import (
13
+ ForwardEpsEstimatesData,
14
+ ForwardEpsEstimatesQueryParams,
15
+ )
16
+ from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
17
+ from openbb_core.provider.utils.helpers import (
18
+ amake_request,
19
+ get_querystring,
20
+ )
21
+ from openbb_intrinio.utils.helpers import response_callback
22
+ from pydantic import Field, field_validator, model_validator
23
+
24
+
25
+ class IntrinioForwardEpsEstimatesQueryParams(ForwardEpsEstimatesQueryParams):
26
+ """Intrinio Forward EPS Estimates Query.
27
+
28
+ https://docs.intrinio.com/documentation/web_api/get_zacks_sales_estimates_v2
29
+ """
30
+
31
+ __json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
32
+
33
+ fiscal_year: Optional[int] = Field(
34
+ default=None,
35
+ description="The future fiscal year to retrieve estimates for."
36
+ + " When no symbol and year is supplied the current calendar year is used.",
37
+ )
38
+ fiscal_period: Optional[Literal["fy", "q1", "q2", "q3", "q4"]] = Field(
39
+ default=None,
40
+ description="The future fiscal period to retrieve estimates for.",
41
+ )
42
+ calendar_year: Optional[int] = Field(
43
+ default=None,
44
+ description="The future calendar year to retrieve estimates for."
45
+ + " When no symbol and year is supplied the current calendar year is used.",
46
+ )
47
+ calendar_period: Optional[Literal["q1", "q2", "q3", "q4"]] = Field(
48
+ default=None,
49
+ description="The future calendar period to retrieve estimates for.",
50
+ )
51
+
52
+ @model_validator(mode="after")
53
+ @classmethod
54
+ def validate_choices(cls, values):
55
+ """Validate the model and set a safe default state."""
56
+ if values.symbol is None and (
57
+ values.calendar_year is None and values.fiscal_year is None
58
+ ):
59
+ values.calendar_year = datetime.now().year
60
+ return values
61
+
62
+
63
+ class IntrinioForwardEpsEstimatesData(ForwardEpsEstimatesData):
64
+ """Intrinio Forward EPS Estimates Data."""
65
+
66
+ __alias_dict__ = {
67
+ "high_estimate": "high",
68
+ "low_estimate": "low",
69
+ "number_of_analysts": "count",
70
+ "mean": "estimated_sales_mean",
71
+ "revisions_change_percent": "percent_change",
72
+ "mean_1w": "mean_7_days_ago",
73
+ "mean_1m": "mean_30_days_ago",
74
+ "mean_2m": "mean_60_days_ago",
75
+ "mean_3m": "mean_90_days_ago",
76
+ }
77
+
78
+ revisions_change_percent: Optional[float] = Field(
79
+ default=None,
80
+ description="The earnings per share (EPS) percent change in estimate for the period.",
81
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
82
+ )
83
+ mean_1w: Optional[float] = Field(
84
+ default=None,
85
+ description="The mean estimate for the period one week ago.",
86
+ )
87
+ mean_1m: Optional[float] = Field(
88
+ default=None,
89
+ description="The mean estimate for the period one month ago.",
90
+ )
91
+ mean_2m: Optional[float] = Field(
92
+ default=None,
93
+ description="The mean estimate for the period two months ago.",
94
+ )
95
+ mean_3m: Optional[float] = Field(
96
+ default=None,
97
+ description="The mean estimate for the period three months ago.",
98
+ )
99
+
100
+ @field_validator(
101
+ "revisions_change_percent",
102
+ mode="before",
103
+ check_fields=False,
104
+ )
105
+ @classmethod
106
+ def normalize_percent(
107
+ cls, v: Optional[Union[int, float]]
108
+ ) -> Optional[Union[int, float]]:
109
+ """Normalize percent values."""
110
+ return v / 100 if v else None
111
+
112
+
113
+ class IntrinioForwardEpsEstimatesFetcher(
114
+ Fetcher[
115
+ IntrinioForwardEpsEstimatesQueryParams, List[IntrinioForwardEpsEstimatesData]
116
+ ]
117
+ ):
118
+ """Intrinio Forward EPS Estimates Fetcher."""
119
+
120
+ @staticmethod
121
+ def transform_query(
122
+ params: Dict[str, Any]
123
+ ) -> IntrinioForwardEpsEstimatesQueryParams:
124
+ """Transform the query params."""
125
+ return IntrinioForwardEpsEstimatesQueryParams(**params)
126
+
127
+ @staticmethod
128
+ async def aextract_data(
129
+ query: IntrinioForwardEpsEstimatesQueryParams,
130
+ credentials: Optional[Dict[str, str]],
131
+ **kwargs: Any,
132
+ ) -> List[Dict]:
133
+ """Return the raw data from the Intrinio endpoint."""
134
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
135
+
136
+ BASE_URL = "https://api-v2.intrinio.com/zacks/eps_estimates?page_size=10000"
137
+
138
+ symbols = query.symbol.split(",") if query.symbol else None
139
+
140
+ query_str = get_querystring(
141
+ query.model_dump(by_alias=True),
142
+ ["symbol", "calendar_period", "fiscal_period", "limit"],
143
+ )
144
+
145
+ results: List[Dict] = []
146
+
147
+ async def get_one(symbol):
148
+ """Get the data for one symbol."""
149
+ url = f"{BASE_URL}&identifier={symbol}&{query_str}&api_key={api_key}"
150
+ new_data: List[Dict] = []
151
+ data = await amake_request(
152
+ url, response_callback=response_callback, **kwargs
153
+ )
154
+ if not data or not isinstance(data, dict) or not data.get("estimates"):
155
+ warn(f"Symbol Error: No data found for {symbol}")
156
+ if isinstance(data, dict) and data.get("estimates"):
157
+ new_data = data.get("estimates") # type: ignore
158
+ if new_data:
159
+ results.extend(new_data)
160
+
161
+ if symbols:
162
+ await asyncio.gather(*[get_one(symbol) for symbol in symbols])
163
+ return results
164
+
165
+ async def fetch_callback(response, session):
166
+ """Use callback for pagination."""
167
+ data = await response.json()
168
+ error = data.get("error", None)
169
+ if error:
170
+ message = data.get("message", "")
171
+ if "api key" in message.lower():
172
+ raise UnauthorizedError(
173
+ f"Unauthorized Intrinio request -> {message}"
174
+ )
175
+ raise OpenBBError(f"Error: {error} -> {message}")
176
+
177
+ if data.get("estimates") and len(data.get("estimates")) > 0: # type: ignore
178
+ results.extend(data.get("estimates")) # type: ignore
179
+ while data.get("next_page"): # type: ignore
180
+ next_page = data["next_page"] # type: ignore
181
+ next_url = f"{url}&next_page={next_page}"
182
+ data = await amake_request(next_url, session=session, **kwargs)
183
+ if (
184
+ "estimates" in data
185
+ and len(data.get("estimates")) > 0 # type: ignore
186
+ ):
187
+ results.extend(data.get("estimates")) # type: ignore
188
+ return results
189
+
190
+ url = f"{BASE_URL}&{query_str}&api_key={api_key}"
191
+
192
+ results = await amake_request(url, response_callback=fetch_callback, **kwargs) # type: ignore
193
+
194
+ if not results:
195
+ raise EmptyDataError("The request was successful but was returned empty.")
196
+
197
+ return results
198
+
199
+ @staticmethod
200
+ def transform_data(
201
+ query: IntrinioForwardEpsEstimatesQueryParams,
202
+ data: List[Dict],
203
+ **kwargs: Any,
204
+ ) -> List[IntrinioForwardEpsEstimatesData]:
205
+ """Transform the raw data into the standard format."""
206
+ symbols = query.symbol.split(",") if query.symbol else []
207
+ results: List[IntrinioForwardEpsEstimatesData] = []
208
+ for item in sorted(
209
+ data,
210
+ key=lambda item: ( # type: ignore
211
+ (
212
+ symbols.index(item.get("symbol")) if item.get("symbol") in symbols else len(symbols), # type: ignore
213
+ item.get("date"),
214
+ )
215
+ if symbols
216
+ else item.get("date")
217
+ ),
218
+ ):
219
+ temp: Dict[str, Any] = {}
220
+ company = item.pop("company")
221
+ if company.get("ticker") is None:
222
+ continue
223
+ temp["symbol"] = company.get("ticker")
224
+ temp["name"] = company.get("name")
225
+ if query.fiscal_period and query.fiscal_period.upper() != item.get(
226
+ "fiscal_period"
227
+ ):
228
+ continue
229
+ if query.calendar_period and query.calendar_period.upper() != item.get(
230
+ "calendar_period"
231
+ ):
232
+ continue
233
+ temp.update(item)
234
+ results.append(IntrinioForwardEpsEstimatesData.model_validate(temp))
235
+
236
+ return results
openbb_platform/providers/intrinio/openbb_intrinio/models/forward_pe_estimates.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Forward PE Estimates Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ from datetime import date as dateType
6
+ from typing import Any, Optional
7
+
8
+ from openbb_core.app.model.abstract.error import OpenBBError
9
+ from openbb_core.provider.abstract.fetcher import Fetcher
10
+ from openbb_core.provider.standard_models.forward_pe_estimates import (
11
+ ForwardPeEstimatesData,
12
+ ForwardPeEstimatesQueryParams,
13
+ )
14
+ from pydantic import Field
15
+
16
+
17
+ class IntrinioForwardPeEstimatesQueryParams(ForwardPeEstimatesQueryParams):
18
+ """Intrinio Forward PE Estimates Query.
19
+
20
+ https://api-v2.intrinio.com/zacks/forward_pe?
21
+ """
22
+
23
+ __json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
24
+
25
+
26
+ class IntrinioForwardPeEstimatesData(ForwardPeEstimatesData):
27
+ """Intrinio Forward PE Estimates Data."""
28
+
29
+ __alias_dict__ = {
30
+ "symbol": "ticker",
31
+ "name": "company_name",
32
+ "year1": "forward_pe_year1",
33
+ "year2": "forward_pe_year2",
34
+ "year3": "forward_pe_year3",
35
+ "year4": "forward_pe_year4",
36
+ "year5": "forward_pe_year5",
37
+ "peg_ratio_year1": "forward_peg_ratio_year1",
38
+ "eps_ttm": "latest_ttm_eps",
39
+ "last_updated": "updated_date",
40
+ }
41
+
42
+ peg_ratio_year1: Optional[float] = Field(
43
+ default=None,
44
+ description="Estimated Forward PEG ratio for the next fiscal year.",
45
+ )
46
+ eps_ttm: Optional[float] = Field(
47
+ default=None,
48
+ description="The latest trailing twelve months earnings per share.",
49
+ )
50
+ last_updated: Optional[dateType] = Field(
51
+ default=None,
52
+ description="The date the data was last updated.",
53
+ )
54
+
55
+
56
+ class IntrinioForwardPeEstimatesFetcher(
57
+ Fetcher[IntrinioForwardPeEstimatesQueryParams, list[IntrinioForwardPeEstimatesData]]
58
+ ):
59
+ """Intrinio Forward PE Estimates Fetcher."""
60
+
61
+ @staticmethod
62
+ def transform_query(
63
+ params: dict[str, Any],
64
+ ) -> IntrinioForwardPeEstimatesQueryParams:
65
+ """Transform the query params."""
66
+ return IntrinioForwardPeEstimatesQueryParams(**params)
67
+
68
+ @staticmethod
69
+ async def aextract_data(
70
+ query: IntrinioForwardPeEstimatesQueryParams,
71
+ credentials: Optional[dict[str, str]],
72
+ **kwargs: Any,
73
+ ) -> list[dict]:
74
+ """Return the raw data from the Intrinio endpoint."""
75
+ # pylint: disable=import-outside-toplevel
76
+ import asyncio # noqa
77
+ from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
78
+ from openbb_core.provider.utils.helpers import amake_request
79
+ from openbb_intrinio.utils.helpers import response_callback
80
+
81
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
82
+ BASE_URL = "https://api-v2.intrinio.com/zacks/forward_pe"
83
+ symbols = query.symbol.split(",") if query.symbol else None
84
+ results: list[dict] = []
85
+
86
+ async def get_one(symbol):
87
+ """Get the data for one symbol."""
88
+ url = f"{BASE_URL}/{symbol}?api_key={api_key}"
89
+ try:
90
+ data = await amake_request(
91
+ url, response_callback=response_callback, **kwargs
92
+ )
93
+ except Exception as e:
94
+ raise OpenBBError(e) from e
95
+
96
+ if data:
97
+ results.append(data) # type: ignore
98
+
99
+ if symbols:
100
+ try:
101
+ gather_results = await asyncio.gather(
102
+ *[get_one(symbol) for symbol in symbols], return_exceptions=True
103
+ )
104
+
105
+ for result in gather_results:
106
+ if isinstance(result, UnauthorizedError):
107
+ raise result
108
+ if isinstance(result, OpenBBError):
109
+ raise result
110
+
111
+ if not results:
112
+ raise EmptyDataError(
113
+ f"There were no results found for any of the given symbols. -> {symbols}"
114
+ )
115
+ return results
116
+ except Exception as e:
117
+ raise OpenBBError(
118
+ f"Error in Intrinio request -> {e} -> {symbols}"
119
+ ) from e
120
+
121
+ async def fetch_callback(response, session):
122
+ """Use callback for pagination."""
123
+ data = await response.json()
124
+ error = data.get("error", None)
125
+
126
+ if error:
127
+ message = data.get("message", "")
128
+ if "api key" in message.lower() or "view this data" in error.lower():
129
+ raise UnauthorizedError(
130
+ f"Unauthorized Intrinio request -> {message} -> {error}"
131
+ )
132
+ raise OpenBBError(f"Error: {error} -> {message}")
133
+
134
+ forward_pe = data.get("forward_pe")
135
+
136
+ if forward_pe and len(forward_pe) > 0: # type: ignore
137
+ results.extend(forward_pe) # type: ignore
138
+
139
+ return results
140
+
141
+ url = f"{BASE_URL}?page_size=10000&api_key={api_key}"
142
+ results = await amake_request(url, response_callback=fetch_callback, **kwargs) # type: ignore
143
+
144
+ if not results:
145
+ raise EmptyDataError("The request was successful but was returned empty.")
146
+
147
+ return results
148
+
149
+ @staticmethod
150
+ def transform_data(
151
+ query: IntrinioForwardPeEstimatesQueryParams,
152
+ data: list[dict],
153
+ **kwargs: Any,
154
+ ) -> list[IntrinioForwardPeEstimatesData]:
155
+ """Transform the raw data into the standard format."""
156
+ symbols = query.symbol.split(",") if query.symbol else []
157
+ if symbols:
158
+ data.sort(
159
+ key=lambda item: (
160
+ symbols.index(item.get("ticker")) # type: ignore
161
+ if item.get("ticker") in symbols
162
+ else len(symbols)
163
+ )
164
+ )
165
+ return [IntrinioForwardPeEstimatesData.model_validate(d) for d in data]
openbb_platform/providers/intrinio/openbb_intrinio/models/forward_sales_estimates.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Forward Sales Estimates Model."""
2
+
3
+ # pylint: disable=unused-argument
4
+
5
+ import asyncio
6
+ from datetime import datetime
7
+ from typing import Any, Dict, List, Literal, Optional, Union
8
+ from warnings import warn
9
+
10
+ from openbb_core.app.model.abstract.error import OpenBBError
11
+ from openbb_core.provider.abstract.fetcher import Fetcher
12
+ from openbb_core.provider.standard_models.forward_sales_estimates import (
13
+ ForwardSalesEstimatesData,
14
+ ForwardSalesEstimatesQueryParams,
15
+ )
16
+ from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
17
+ from openbb_core.provider.utils.helpers import (
18
+ amake_request,
19
+ get_querystring,
20
+ )
21
+ from openbb_intrinio.utils.helpers import response_callback
22
+ from pydantic import Field, field_validator, model_validator
23
+
24
+
25
+ class IntrinioForwardSalesEstimatesQueryParams(ForwardSalesEstimatesQueryParams):
26
+ """Intrinio Forward Sales Estimates Query.
27
+
28
+ https://docs.intrinio.com/documentation/web_api/get_zacks_sales_estimates_v2
29
+ """
30
+
31
+ __json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
32
+
33
+ fiscal_year: Optional[int] = Field(
34
+ default=None,
35
+ description="The future fiscal year to retrieve estimates for."
36
+ + " When no symbol and year is supplied the current calendar year is used.",
37
+ )
38
+ fiscal_period: Optional[Literal["fy", "q1", "q2", "q3", "q4"]] = Field(
39
+ default=None,
40
+ description="The future fiscal period to retrieve estimates for.",
41
+ )
42
+ calendar_year: Optional[int] = Field(
43
+ default=None,
44
+ description="The future calendar year to retrieve estimates for."
45
+ + " When no symbol and year is supplied the current calendar year is used.",
46
+ )
47
+ calendar_period: Optional[Literal["q1", "q2", "q3", "q4"]] = Field(
48
+ default=None,
49
+ description="The future calendar period to retrieve estimates for.",
50
+ )
51
+
52
+ @model_validator(mode="after")
53
+ @classmethod
54
+ def validate_choices(cls, values):
55
+ """Validate the model and set a safe default state."""
56
+ if values.symbol is None and (
57
+ values.calendar_year is None and values.fiscal_year is None
58
+ ):
59
+ values.calendar_year = datetime.now().year
60
+ return values
61
+
62
+
63
+ class IntrinioForwardSalesEstimatesData(ForwardSalesEstimatesData):
64
+ """Intrinio Forward Sales Estimates Data."""
65
+
66
+ __alias_dict__ = {
67
+ "low_estimate": "low",
68
+ "high_estimate": "high",
69
+ "number_of_analysts": "count",
70
+ "mean": "estimated_sales_mean",
71
+ "revisions_1w_up": "analyst_revisions_up_1w",
72
+ "revisions_1w_down": "analyst_revisions_down_1w",
73
+ "revisions_1w_change_percent": "analyst_revisions_percent_change_1w",
74
+ "revisions_1m_up": "analyst_revisions_up_1m",
75
+ "revisions_1m_down": "analyst_revisions_down_1m",
76
+ "revisions_1m_change_percent": "analyst_revisions_percent_change_1m",
77
+ "revisions_3m_up": "analyst_revisions_up_3m",
78
+ "revisions_3m_down": "analyst_revisions_down_3m",
79
+ "revisions_3m_change_percent": "analyst_revisions_percent_change_3m",
80
+ }
81
+
82
+ revisions_1w_up: Optional[int] = Field(
83
+ default=None, description="Number of revisions up in the last week."
84
+ )
85
+ revisions_1w_down: Optional[int] = Field(
86
+ default=None, description="Number of revisions down in the last week."
87
+ )
88
+ revisions_1w_change_percent: Optional[float] = Field(
89
+ default=None,
90
+ description="The analyst revisions percent change in estimate for the period of 1 week.",
91
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
92
+ )
93
+ revisions_1m_up: Optional[int] = Field(
94
+ default=None, description="Number of revisions up in the last month."
95
+ )
96
+ revisions_1m_down: Optional[int] = Field(
97
+ default=None, description="Number of revisions down in the last month."
98
+ )
99
+ revisions_1m_change_percent: Optional[float] = Field(
100
+ default=None,
101
+ description="The analyst revisions percent change in estimate for the period of 1 month.",
102
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
103
+ )
104
+ revisions_3m_up: Optional[int] = Field(
105
+ default=None, description="Number of revisions up in the last 3 months."
106
+ )
107
+ revisions_3m_down: Optional[int] = Field(
108
+ default=None, description="Number of revisions down in the last 3 months."
109
+ )
110
+ revisions_3m_change_percent: Optional[float] = Field(
111
+ default=None,
112
+ description="The analyst revisions percent change in estimate for the period of 3 months.",
113
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
114
+ )
115
+
116
+ @field_validator(
117
+ "revisions_1w_change_percent",
118
+ "revisions_1m_change_percent",
119
+ "revisions_3m_change_percent",
120
+ mode="before",
121
+ check_fields=False,
122
+ )
123
+ @classmethod
124
+ def normalize_percent(
125
+ cls, v: Optional[Union[int, float]]
126
+ ) -> Optional[Union[int, float]]:
127
+ """Normalize percent values."""
128
+ return v / 100 if v else None
129
+
130
+
131
+ class IntrinioForwardSalesEstimatesFetcher(
132
+ Fetcher[
133
+ IntrinioForwardSalesEstimatesQueryParams,
134
+ List[IntrinioForwardSalesEstimatesData],
135
+ ]
136
+ ):
137
+ """Intrinio Forward Sales Estimates Fetcher."""
138
+
139
+ @staticmethod
140
+ def transform_query(
141
+ params: Dict[str, Any]
142
+ ) -> IntrinioForwardSalesEstimatesQueryParams:
143
+ """Transform the query params."""
144
+ return IntrinioForwardSalesEstimatesQueryParams(**params)
145
+
146
+ @staticmethod
147
+ async def aextract_data(
148
+ query: IntrinioForwardSalesEstimatesQueryParams,
149
+ credentials: Optional[Dict[str, str]],
150
+ **kwargs: Any,
151
+ ) -> List[Dict]:
152
+ """Return the raw data from the Intrinio endpoint."""
153
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
154
+
155
+ BASE_URL = "https://api-v2.intrinio.com/zacks/sales_estimates?page_size=10000"
156
+
157
+ symbols = query.symbol.split(",") if query.symbol else None
158
+
159
+ query_str = get_querystring(
160
+ query.model_dump(by_alias=True),
161
+ ["symbol", "calendar_period", "fiscal_period", "limit"],
162
+ )
163
+
164
+ results: List[Dict] = []
165
+
166
+ async def get_one(symbol):
167
+ """Get the data for one symbol."""
168
+ url = f"{BASE_URL}&identifier={symbol}&{query_str}&api_key={api_key}"
169
+ new_data: List[Dict] = []
170
+ data = await amake_request(
171
+ url, response_callback=response_callback, **kwargs
172
+ )
173
+ if not data or not isinstance(data, dict) or not data.get("estimates"):
174
+ warn(f"Symbol Error: No data found for {symbol}")
175
+ if isinstance(data, dict) and data.get("estimates"):
176
+ new_data = data.get("estimates") # type: ignore
177
+ if new_data:
178
+ results.extend(new_data)
179
+
180
+ if symbols:
181
+ await asyncio.gather(*[get_one(symbol) for symbol in symbols])
182
+ return results
183
+
184
+ async def fetch_callback(response, session):
185
+ """Use callback for pagination."""
186
+ data = await response.json()
187
+ error = data.get("error", None)
188
+ if error:
189
+ message = data.get("message", "")
190
+ if "api key" in message.lower():
191
+ raise UnauthorizedError(
192
+ f"Unauthorized Intrinio request -> {message}"
193
+ )
194
+ raise OpenBBError(f"Error: {error} -> {message}")
195
+ if data.get("estimates") and len(data.get("estimates")) > 0: # type: ignore
196
+ results.extend(data.get("estimates")) # type: ignore
197
+ while data.get("next_page"): # type: ignore
198
+ next_page = data["next_page"] # type: ignore
199
+ next_url = f"{url}&next_page={next_page}"
200
+ data = await amake_request(next_url, session=session, **kwargs)
201
+ if (
202
+ "estimates" in data
203
+ and len(data.get("estimates")) > 0 # type: ignore
204
+ ):
205
+ results.extend(data.get("estimates")) # type: ignore
206
+ return results
207
+
208
+ url = f"{BASE_URL}&{query_str}&api_key={api_key}"
209
+
210
+ results = await amake_request(url, response_callback=fetch_callback, **kwargs) # type: ignore
211
+
212
+ if not results:
213
+ raise EmptyDataError("The request was successful but was returned empty.")
214
+
215
+ return results
216
+
217
+ @staticmethod
218
+ def transform_data(
219
+ query: IntrinioForwardSalesEstimatesQueryParams,
220
+ data: List[Dict],
221
+ **kwargs: Any,
222
+ ) -> List[IntrinioForwardSalesEstimatesData]:
223
+ """Transform the raw data into the standard format."""
224
+ symbols = query.symbol.split(",") if query.symbol else []
225
+ results: List[IntrinioForwardSalesEstimatesData] = []
226
+ for item in sorted(
227
+ data,
228
+ key=lambda item: ( # type: ignore
229
+ (
230
+ symbols.index(item.get("symbol")) if item.get("symbol") in symbols else len(symbols), # type: ignore
231
+ item.get("date"),
232
+ )
233
+ if symbols
234
+ else item.get("date")
235
+ ),
236
+ ):
237
+ temp: Dict[str, Any] = {}
238
+ company = item.pop("company")
239
+ if company.get("ticker") is None:
240
+ continue
241
+ temp["symbol"] = company.get("ticker")
242
+ temp["name"] = company.get("name")
243
+ if query.fiscal_period and query.fiscal_period.upper() != item.get(
244
+ "fiscal_period"
245
+ ):
246
+ continue
247
+ if query.calendar_period and query.calendar_period.upper() != item.get(
248
+ "calendar_period"
249
+ ):
250
+ continue
251
+ temp.update(item)
252
+ results.append(IntrinioForwardSalesEstimatesData.model_validate(temp))
253
+
254
+ return results
openbb_platform/providers/intrinio/openbb_intrinio/models/fred_series.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio FRED Series Model."""
2
+
3
+ import asyncio
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from dateutil.relativedelta import relativedelta
8
+ from openbb_core.provider.abstract.fetcher import Fetcher
9
+ from openbb_core.provider.standard_models.fred_series import (
10
+ SeriesData,
11
+ SeriesQueryParams,
12
+ )
13
+ from openbb_core.provider.utils.helpers import (
14
+ ClientResponse,
15
+ ClientSession,
16
+ amake_requests,
17
+ get_querystring,
18
+ )
19
+ from pydantic import Field
20
+
21
+
22
+ class IntrinioFredSeriesQueryParams(SeriesQueryParams):
23
+ """Intrinio FRED Series Query.
24
+
25
+ Source: https://docs.intrinio.com/documentation/web_api/get_economic_index_historical_data_v2
26
+ """
27
+
28
+ __alias_dict__ = {"limit": "page_size"}
29
+
30
+ all_pages: Optional[bool] = Field(
31
+ default=False,
32
+ description="Returns all pages of data from the API call at once.",
33
+ )
34
+ sleep: Optional[float] = Field(
35
+ default=1.0,
36
+ description="Time to sleep between requests to avoid rate limiting.",
37
+ )
38
+
39
+
40
+ class IntrinioFredSeriesData(SeriesData):
41
+ """Intrinio FRED Series Data."""
42
+
43
+ value: Optional[float] = Field(default=None, description="Value of the index.")
44
+
45
+
46
+ class IntrinioFredSeriesFetcher(
47
+ Fetcher[
48
+ IntrinioFredSeriesQueryParams,
49
+ List[IntrinioFredSeriesData],
50
+ ]
51
+ ):
52
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
53
+
54
+ @staticmethod
55
+ def transform_query(params: Dict[str, Any]) -> IntrinioFredSeriesQueryParams:
56
+ """Transform the query params."""
57
+ transformed_params = params
58
+
59
+ now = datetime.now().date()
60
+ if params.get("start_date") is None:
61
+ transformed_params["start_date"] = now - relativedelta(years=1)
62
+
63
+ if params.get("end_date") is None:
64
+ transformed_params["end_date"] = now
65
+
66
+ return IntrinioFredSeriesQueryParams(**transformed_params)
67
+
68
+ @staticmethod
69
+ async def aextract_data(
70
+ query: IntrinioFredSeriesQueryParams,
71
+ credentials: Optional[Dict[str, str]],
72
+ **kwargs: Any,
73
+ ) -> List[Dict]:
74
+ """Return the raw data from the Intrinio endpoint."""
75
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
76
+
77
+ base_url = "https://api-v2.intrinio.com"
78
+ query_str = get_querystring(
79
+ query.model_dump(), ["symbol", "all_pages", "sleep"]
80
+ )
81
+
82
+ url = (
83
+ f"{base_url}/indices/economic/${query.symbol.replace('$', '')}/historical_data/level"
84
+ f"?{query_str}&api_key={api_key}"
85
+ )
86
+
87
+ async def callback(response: ClientResponse, session: ClientSession) -> dict:
88
+ """Return the response."""
89
+ init_response = await response.json()
90
+
91
+ all_data: list = init_response.get("historical_data", [])
92
+
93
+ if query.all_pages:
94
+ next_page = init_response.get("next_page", None)
95
+ while next_page:
96
+ if query.limit > 100:
97
+ await asyncio.sleep(query.sleep)
98
+
99
+ url = response.url.update_query(next_page=next_page).human_repr()
100
+ response_data = await session.get_json(url)
101
+
102
+ all_data.extend(response_data.get("historical_data", []))
103
+ next_page = response_data.get("next_page", None)
104
+
105
+ return all_data
106
+
107
+ return await amake_requests([url], callback, **kwargs)
108
+
109
+ @staticmethod
110
+ def transform_data(
111
+ query: IntrinioFredSeriesQueryParams, data: List[Dict], **kwargs: Any
112
+ ) -> List[IntrinioFredSeriesData]:
113
+ """Return the transformed data."""
114
+ return [IntrinioFredSeriesData.model_validate(d) for d in data]
openbb_platform/providers/intrinio/openbb_intrinio/models/historical_attributes.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Intrinio Historical Attributes Model."""
2
+
3
+ # pylint: disable = unused-argument
4
+
5
+ import warnings
6
+ from datetime import datetime
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from dateutil.relativedelta import relativedelta
10
+ from openbb_core.app.model.abstract.warning import OpenBBWarning
11
+ from openbb_core.provider.abstract.fetcher import Fetcher
12
+ from openbb_core.provider.standard_models.historical_attributes import (
13
+ HistoricalAttributesData,
14
+ HistoricalAttributesQueryParams,
15
+ )
16
+ from openbb_core.provider.utils.helpers import (
17
+ ClientResponse,
18
+ ClientSession,
19
+ amake_requests,
20
+ get_querystring,
21
+ )
22
+
23
+
24
+ class IntrinioHistoricalAttributesQueryParams(HistoricalAttributesQueryParams):
25
+ """Intrinio Historical Attributes Query.
26
+
27
+ Source: https://docs.intrinio.com/documentation/web_api/get_historical_data_v2
28
+ """
29
+
30
+ __alias_dict__ = {"sort": "sort_order", "limit": "page_size", "tag_type": "type"}
31
+ __json_schema_extra__ = {
32
+ "tag": {"multiple_items_allowed": True},
33
+ "symbol": {"multiple_items_allowed": True},
34
+ }
35
+
36
+
37
+ class IntrinioHistoricalAttributesData(HistoricalAttributesData):
38
+ """Intrinio Historical Attributes Data."""
39
+
40
+
41
+ class IntrinioHistoricalAttributesFetcher(
42
+ Fetcher[
43
+ IntrinioHistoricalAttributesQueryParams,
44
+ List[IntrinioHistoricalAttributesData],
45
+ ]
46
+ ):
47
+ """Transform the query, extract and transform the data from the Intrinio endpoints."""
48
+
49
+ @staticmethod
50
+ def transform_query(
51
+ params: Dict[str, Any]
52
+ ) -> IntrinioHistoricalAttributesQueryParams:
53
+ """Transform the query params."""
54
+ transformed_params = params
55
+
56
+ now = datetime.now().date()
57
+ if params.get("start_date") is None:
58
+ transformed_params["start_date"] = now - relativedelta(years=5)
59
+
60
+ if params.get("end_date") is None:
61
+ transformed_params["end_date"] = now
62
+
63
+ return IntrinioHistoricalAttributesQueryParams(**transformed_params)
64
+
65
+ @staticmethod
66
+ async def aextract_data(
67
+ query: IntrinioHistoricalAttributesQueryParams,
68
+ credentials: Optional[Dict[str, str]],
69
+ **kwargs: Any,
70
+ ) -> List[Dict]:
71
+ """Return the raw data from the Intrinio endpoint."""
72
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
73
+
74
+ base_url = "https://api-v2.intrinio.com"
75
+ query_str = get_querystring(query.model_dump(by_alias=True), ["symbol", "tag"])
76
+
77
+ def generate_url(symbol: str, tag: str) -> str:
78
+ """Return the url for the given symbol and tag."""
79
+ url_params = f"{symbol}/{tag}?{query_str}&api_key={api_key}"
80
+ url = f"{base_url}/historical_data/{url_params}"
81
+ return url
82
+
83
+ async def callback(
84
+ response: ClientResponse, session: ClientSession
85
+ ) -> List[Dict]:
86
+ """Return the response."""
87
+ init_response = await response.json()
88
+
89
+ if message := init_response.get( # type: ignore
90
+ "error"
91
+ ) or init_response.get( # type: ignore
92
+ "message"
93
+ ):
94
+ warnings.warn(message=str(message), category=OpenBBWarning)
95
+ return []
96
+
97
+ symbol = response.url.parts[-2] # type: ignore
98
+ tag = response.url.parts[-1] # type: ignore
99
+
100
+ all_data: List = init_response.get("historical_data", []) # type: ignore
101
+ all_data = [{**item, "symbol": symbol, "tag": tag} for item in all_data]
102
+
103
+ next_page = init_response.get("next_page", None) # type: ignore
104
+ while next_page:
105
+ url = response.url.update_query( # type: ignore
106
+ next_page=next_page
107
+ ).human_repr()
108
+ response_data = await session.get_json(url)
109
+
110
+ if message := response_data.get("error") or response_data.get( # type: ignore
111
+ "message"
112
+ ):
113
+ warnings.warn(message=message, category=OpenBBWarning)
114
+ return []
115
+
116
+ symbol = response.url.parts[-2] # type: ignore
117
+ tag = response_data.url.parts[-1] # type: ignore
118
+
119
+ response_data = response_data.get("historical_data", []) # type: ignore
120
+ response_data = [
121
+ {**item, "symbol": symbol, "tag": tag} for item in response_data
122
+ ]
123
+
124
+ all_data.extend(response_data)
125
+ next_page = response_data.get("next_page", None) # type: ignore
126
+
127
+ return all_data
128
+
129
+ urls = [
130
+ generate_url(symbol, tag)
131
+ for symbol in query.symbol.split(",")
132
+ for tag in query.tag.split(",")
133
+ ]
134
+
135
+ return await amake_requests(urls, callback, **kwargs)
136
+
137
+ @staticmethod
138
+ def transform_data(
139
+ query: IntrinioHistoricalAttributesQueryParams,
140
+ data: List[Dict],
141
+ **kwargs: Any,
142
+ ) -> List[IntrinioHistoricalAttributesData]:
143
+ """Return the transformed data."""
144
+ return [IntrinioHistoricalAttributesData.model_validate(d) for d in data]