finviz-data 1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,223 @@
1
+ from datetime import datetime
2
+
3
+ from bs4 import BeautifulSoup
4
+ from curl_cffi import requests
5
+
6
+
7
+ class FinvizRequestError(Exception):
8
+ """Raised when Finviz returns an unsuccessful HTTP response."""
9
+
10
+
11
+ def get_soup(ticker) -> BeautifulSoup:
12
+ url = f"https://finviz.com/stock?t={ticker}&p=d"
13
+ response = requests.get(url, impersonate="chrome")
14
+ status_code = getattr(response, "status_code", None)
15
+ if isinstance(status_code, int) and status_code >= 400:
16
+ raise FinvizRequestError(
17
+ f"Finviz request failed for ticker {ticker!r}: "
18
+ f"HTTP {status_code} ({url})"
19
+ )
20
+
21
+ html = response.text
22
+ soup = BeautifulSoup(html, "html.parser")
23
+ return soup
24
+
25
+
26
+ def get_fundamentals(soup: BeautifulSoup) -> dict:
27
+ # Finviz may render the snapshot as one table or split it into several
28
+ # responsive columns, all with the same class.
29
+ tables = soup.find_all("table", {"class": "snapshot-table2"})
30
+
31
+ # Initialize a dictionary to store the key-value pairs
32
+ financial_data = {}
33
+
34
+ for table in tables:
35
+ # Iterate through all rows of the table
36
+ for row in table.find_all("tr"):
37
+ # Each cell in the row
38
+ cells = row.find_all("td")
39
+ # Step by two because the cells are key/value pairs.
40
+ for i in range(0, len(cells), 2):
41
+ key = cells[i].get_text().strip()
42
+ value = cells[i + 1].get_text().strip()
43
+ financial_data[key] = value
44
+
45
+ return financial_data
46
+
47
+
48
+ def get_fundamentals_float(soup: BeautifulSoup) -> dict:
49
+ fundamentals = get_fundamentals(soup)
50
+ fundamentals_float = _convert_to_floats(fundamentals)
51
+
52
+ return fundamentals_float
53
+
54
+
55
+ def get_company_info(soup: BeautifulSoup) -> dict:
56
+ base_info = {}
57
+
58
+ # Get ticker. Text ontent of quote-header_ticker-wrapper_ticker
59
+ ticker = soup.find("h1", class_="quote-header_ticker-wrapper_ticker").text
60
+ base_info["Ticker"] = ticker
61
+
62
+ # Get company name. Text content of h2.quote-header_ticker-wrapper_company a
63
+ company_name = (
64
+ soup.find("h2", class_="quote-header_ticker-wrapper_company").text
65
+ )
66
+ base_info["Company"] = company_name.strip()
67
+
68
+ categories = soup.find("div", class_="quote-header_categories")
69
+ if categories is None:
70
+ categories = soup.find("div", class_="quote-links")
71
+ links = categories.find_all("a") if categories else []
72
+
73
+ for link in links:
74
+ href = link.get("href", "")
75
+ if "f=sec_" in href:
76
+ base_info["Sector"] = link.text
77
+ elif "f=ind_" in href:
78
+ base_info["Industry"] = link.text
79
+ elif "f=geo_" in href:
80
+ base_info["Country"] = link.text
81
+ elif "f=exch_" in href:
82
+ base_info["Exchange"] = link.text
83
+
84
+ return base_info
85
+
86
+
87
+ def _is_float_like(val: str) -> bool:
88
+ try:
89
+ float(val)
90
+ return True
91
+ except ValueError:
92
+ return False
93
+
94
+
95
+ def _convert_value(value: str):
96
+ if value is None:
97
+ return None
98
+
99
+ value = value.strip()
100
+
101
+ if value in ("", "-"):
102
+ return None
103
+ elif value.endswith("%") and _is_float_like(value.strip("%")):
104
+ return float(value.strip("%")) / 100
105
+ elif value.endswith("M") and _is_float_like(value.strip("M")):
106
+ return float(value.strip("M")) * 1e6
107
+ elif value.endswith("B") and _is_float_like(value.strip("B")):
108
+ return float(value.strip("B")) * 1e9
109
+ elif value.endswith("T") and _is_float_like(value.strip("T")):
110
+ return float(value.strip("T")) * 1e12
111
+ elif _is_float_like(value.replace(",", "")):
112
+ return float(value.replace(",", ""))
113
+ else:
114
+ return value
115
+
116
+
117
+ def _convert_date(value: str):
118
+ try:
119
+ return datetime.strptime(value, "%b %d, %Y").date().isoformat()
120
+ except ValueError:
121
+ return _convert_value(value)
122
+
123
+
124
+ def _split_two_values(value: str, first_key: str, second_key: str) -> dict:
125
+ values = value.split()
126
+ if len(values) != 2:
127
+ return {first_key: _convert_value(value), second_key: None}
128
+
129
+ return {
130
+ first_key: _convert_value(values[0]),
131
+ second_key: _convert_value(values[1]),
132
+ }
133
+
134
+
135
+ def _split_dividend(value: str, amount_key: str, yield_key: str) -> dict:
136
+ values = value.replace("(", "").replace(")", "").split()
137
+ if len(values) != 2:
138
+ return {amount_key: _convert_value(value), yield_key: None}
139
+
140
+ return {
141
+ amount_key: _convert_value(values[0]),
142
+ yield_key: _convert_value(values[1]),
143
+ }
144
+
145
+
146
+ def _split_option_short(value: str) -> dict:
147
+ values = [item.strip() for item in value.split("/")]
148
+ if len(values) != 2:
149
+ return {"Optionable": _convert_value(value), "Shortable": None}
150
+
151
+ return {
152
+ "Optionable": values[0] == "Yes",
153
+ "Shortable": values[1] == "Yes",
154
+ }
155
+
156
+
157
+ def _split_earnings(value: str) -> dict:
158
+ values = value.split()
159
+ if len(values) < 3:
160
+ return {"Earnings Date": _convert_value(value), "Earnings Time": None}
161
+
162
+ return {
163
+ "Earnings Date": " ".join(values[:-1]),
164
+ "Earnings Time": values[-1],
165
+ }
166
+
167
+
168
+ def _convert_to_floats(data_dict: dict) -> dict:
169
+ """
170
+ Converts values in a dictionary to floats where applicable.
171
+ - Replaces single dash ('-') with None.
172
+ - Splits values representing a range (e.g., '10.86 - 19.08') into two separate keys.
173
+ - Converts values with '%' to proportionate floats.
174
+ - Converts values ending in 'M', 'B', or 'T' to their numeric equivalents in millions, billions, or trillions.
175
+ - Converts numeric strings with commas (e.g., '30,196,932') into floats.
176
+ """
177
+
178
+ compound_fields = {
179
+ "Volatility": lambda value: _split_two_values(
180
+ value, "Volatility Week", "Volatility Month"
181
+ ),
182
+ "52W High": lambda value: _split_two_values(
183
+ value, "52W High Price", "52W High Change"
184
+ ),
185
+ "52W Low": lambda value: _split_two_values(
186
+ value, "52W Low Price", "52W Low Change"
187
+ ),
188
+ "EPS past 3/5Y": lambda value: _split_two_values(
189
+ value, "EPS past 3Y", "EPS past 5Y"
190
+ ),
191
+ "Sales past 3/5Y": lambda value: _split_two_values(
192
+ value, "Sales past 3Y", "Sales past 5Y"
193
+ ),
194
+ "Dividend Gr. 3/5Y": lambda value: _split_two_values(
195
+ value, "Dividend Gr. 3Y", "Dividend Gr. 5Y"
196
+ ),
197
+ "EPS/Sales Surpr.": lambda value: _split_two_values(
198
+ value, "EPS Surprise", "Sales Surprise"
199
+ ),
200
+ "Dividend Est.": lambda value: _split_dividend(
201
+ value, "Dividend Est. Amount", "Dividend Est. Yield"
202
+ ),
203
+ "Dividend TTM": lambda value: _split_dividend(
204
+ value, "Dividend TTM Amount", "Dividend TTM Yield"
205
+ ),
206
+ "Option/Short": _split_option_short,
207
+ "Earnings": _split_earnings,
208
+ }
209
+
210
+ date_fields = {"IPO", "Dividend Ex-Date"}
211
+
212
+ new_data = {}
213
+ for key, value in data_dict.items():
214
+ value = value.strip() if isinstance(value, str) else value
215
+
216
+ if key in compound_fields:
217
+ new_data.update(compound_fields[key](value))
218
+ elif key in date_fields:
219
+ new_data[key] = _convert_date(value)
220
+ else:
221
+ new_data[key] = _convert_value(value)
222
+
223
+ return new_data
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: finviz-data
3
+ Version: 1.3.1
4
+ Summary: Simple package to get data from finviz.com
5
+ Author-email: Dennis Iversen <dennis.iversen@gmail.com>
6
+ Project-URL: Repository, https://github.com/diversen/finviz-data
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: beautifulsoup4<5,>=4.12
10
+ Requires-Dist: curl-cffi<1,>=0.7
11
+
12
+ # finviz-data
13
+
14
+ A simple package for getting fundamental data from finviz.com for a single ticker.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ uv add git+https://github.com/diversen/finviz-data
20
+ ```
21
+
22
+ ## Development
23
+
24
+ ```bash
25
+ uv sync
26
+ uv run python -m unittest discover
27
+ ```
28
+
29
+ By default, the live Finviz test is skipped. To run the full test suite without
30
+ skipping the live test, enable it with `FINVIZ_LIVE_TESTS=1`:
31
+
32
+ ```bash
33
+ FINVIZ_LIVE_TESTS=1 uv run python -m unittest discover
34
+ ```
35
+
36
+ The live test makes a real request to finviz.com and may still be skipped if
37
+ Finviz temporarily rate limits your IP.
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+
43
+ from finviz_data import finviz_data
44
+
45
+ # Get the html soup for a single ticker
46
+ soup = finviz_data.get_soup('AAPL')
47
+
48
+ # Get the fundamentals for a single ticker
49
+ fundamentals = finviz_data.get_fundamentals(soup)
50
+
51
+ # Get the fundamentals where all is formatted to float values where possible
52
+ fundamentals = finviz_data.get_fundementals_float(soup)
53
+
54
+ # Get basic company info, sector, ticker etc.
55
+ company_info = finviz_data.get_company_info(soup)
56
+ ```
@@ -0,0 +1,6 @@
1
+ finviz_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ finviz_data/finviz_data.py,sha256=o0reXsmdkFR8Basp7zOgY69Pu5ARvCCHHbK3GBv9ntY,7220
3
+ finviz_data-1.3.1.dist-info/METADATA,sha256=Z_4aU1-TXNRzdb87O1RuZV9dYUTdMI6JSflqPdp5ubs,1409
4
+ finviz_data-1.3.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ finviz_data-1.3.1.dist-info/top_level.txt,sha256=ThOlyRUZfe1iYFLpNodzkEuRUK8NvvAw-EqavoNR_m8,12
6
+ finviz_data-1.3.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ finviz_data