Skip to content

Commit 8071e2d

Browse files
improve fetchers
1 parent 55420e7 commit 8071e2d

File tree

5 files changed

+136
-124
lines changed

5 files changed

+136
-124
lines changed

apps/stocks/fetcher/__init__.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
from typing import Callable
22
from apps.stocks.fetcher.marketstack import (
3-
fetch_price_with_marketstack_fetcher,
4-
fetch_prices_with_marketstack_fetchers,
3+
MarketstackFetcher,
54
)
65
from apps.stocks.fetcher.selenium import (
7-
fetch_price_with_selenium_fetcher,
8-
fetch_prices_with_selenium_fetchers,
6+
SeleniumFetcher,
97
)
108
from apps.stocks.fetcher.website import (
119
WebsiteFetcher,
@@ -16,6 +14,22 @@
1614
from typing import Callable
1715
from django.utils import timezone
1816
from apps.stocks.models import Price, PriceFetcher, Stock
17+
from django.utils import timezone
18+
from apps.stocks.models import Stock
19+
from apps.stocks.forms import PriceForm
20+
21+
22+
def save_price(price: float, stock: Stock) -> None:
23+
price = PriceForm(
24+
{
25+
"ticker": stock.ticker,
26+
"exchange": stock.exchange,
27+
"date": timezone.now(),
28+
"price": price,
29+
}
30+
)
31+
if price.is_valid():
32+
price.save()
1933

2034

2135
FETCHER_FUNCTION = Callable[[PriceFetcher], tuple[bool, str]]
@@ -32,25 +46,47 @@ def get_stocks_to_be_fetched() -> list[Stock]:
3246
return stocks_to_be_fetched
3347

3448

35-
def get_fetchers_to_be_run(type: str) -> list[PriceFetcher]:
49+
def get_fetchers_to_be_run(type: str) -> dict[str, dict[str, int | str]]:
3650
fetchers_to_be_run = []
3751
for fetcher in list(PriceFetcher.objects.filter(type=type)):
3852
if Price.objects.filter(
3953
ticker=fetcher.stock.ticker, date__gt=timezone.now() - timedelta(days=1)
4054
).exists():
4155
continue
4256
fetchers_to_be_run.append(fetcher)
43-
return fetchers_to_be_run
57+
return {str(fetcher.pk): fetcher.data for fetcher in fetchers_to_be_run}
58+
59+
60+
def turn_fetchers_to_data(fetchers: list[PriceFetcher]) -> dict[str, dict]:
61+
data = {}
62+
for fetcher in fetchers:
63+
data[str(fetcher.pk)] = fetcher.data
64+
return data
65+
66+
67+
def save_prices(results: dict[str, tuple[bool, str | float]]):
68+
for fetcher, result in results:
69+
fetcher = PriceFetcher.objects.get(pk=fetcher)
70+
if result[0]:
71+
save_price(fetcher.stock, result[1])
4472

4573

4674
FETCHERS: dict[str, FETCHER_FUNCTION] = {
4775
"WEBSITE": WebsiteFetcher.fetch_single,
48-
"SELENIUM": fetch_price_with_selenium_fetcher,
49-
"MARKETSTACK": fetch_price_with_marketstack_fetcher,
76+
"SELENIUM": SeleniumFetcher.fetch_single,
77+
"MARKETSTACK": MarketstackFetcher.fetch_single,
5078
}
5179

5280

5381
def fetch_prices():
54-
WebsiteFetcher.fetch_multiple(get_fetchers_to_be_run("WEBSITE"))
55-
fetch_prices_with_selenium_fetchers(get_stocks_to_be_fetched())
56-
fetch_prices_with_marketstack_fetchers(get_stocks_to_be_fetched())
82+
fetchers = get_fetchers_to_be_run("WEBSITE")
83+
results = WebsiteFetcher.fetch_multiple(fetchers)
84+
save_prices(results)
85+
86+
fetchers = get_fetchers_to_be_run("SELENIUM")
87+
results = SeleniumFetcher.fetch_multiple(fetchers)
88+
save_prices(results)
89+
90+
fetchers = get_fetchers_to_be_run("MARKETSTACK")
91+
results = MarketstackFetcher.fetch_multiple(fetchers)
92+
save_prices(results)

apps/stocks/fetcher/base.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import abc
22

3-
from apps.stocks.models import PriceFetcher, Stock
4-
53

64
class Fetcher(abc.ABC):
75
@abc.abstractmethod
8-
def fetch_single(self, stock: PriceFetcher) -> tuple[bool, str | float]:
6+
def fetch_single(self, *args, **kwargs) -> tuple[bool, str | float]:
97
raise NotImplementedError()
10-
8+
119
@abc.abstractmethod
12-
def fetch_multiple(self, stocks: list[PriceFetcher]) -> dict[Stock, tuple[bool, str | float]]:
10+
def fetch_multiple(
11+
self, *args, **kwargs
12+
) -> dict[str, tuple[bool, str | float]]:
1313
raise NotImplementedError()

apps/stocks/fetcher/marketstack.py

Lines changed: 37 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,39 @@
11
from django.conf import settings
22
import requests
3-
from apps.stocks.fetcher.price import save_price
4-
from apps.stocks.models import PriceFetcher, Stock
5-
6-
7-
def fetch_prices_from_marketstack(symbols: list[str]) -> tuple[bool, str]:
8-
symbols = ",".join(symbols)
9-
params = {"access_key": settings.MARKETSTACK_API_KEY}
10-
url = "http://api.marketstack.com/v1/eod/latest?symbols={}".format(symbols)
11-
api_result = requests.get(url, params)
12-
api_response = api_result.json()
13-
if "error" in api_response:
14-
return (
15-
False,
16-
f"Could not fetch prices from marketstack: '{api_response['error']['message']}'.",
17-
)
18-
19-
for price in api_response["data"]:
20-
save_price(
21-
round(price["close"], 2), Stock.objects.get(ticker=[price["symbol"]])
22-
)
23-
24-
return True, ""
25-
26-
27-
def fetch_price_with_marketstack_fetcher(fetcher: PriceFetcher) -> tuple[bool, str]:
28-
return fetch_prices_from_marketstack([fetcher.data["symbol"]])
29-
30-
31-
def fetch_prices_with_marketstack_fetchers(
32-
stocks: list[Stock], messages: list[str] | None = None
33-
):
34-
symbols = []
35-
for stock in stocks:
36-
fetcher = stock.price_fetchers.filter(type="MARKETSTACK").first()
37-
if fetcher is None:
38-
if messages is not None:
39-
messages.append(f"Could not find a price fetcher for {stock.ticker}.")
40-
continue
41-
symbols.append(fetcher.data["symbol"])
42-
43-
fetch_prices_from_marketstack(symbols)
3+
from apps.stocks.fetcher.base import Fetcher
4+
5+
6+
class MarketstackFetcher(Fetcher):
7+
def fetch_single(self, symbol: str) -> tuple[bool, str | float]:
8+
return self.fetch_multiple({"_": {"symbol": symbol}})["_"]
9+
10+
def fetch_multiple(
11+
self, data: dict[str, dict[str, str | int]]
12+
) -> dict[str, tuple[bool, str | float]]:
13+
symbols = []
14+
for fetcher, input in data.items():
15+
symbols.append(input["symbol"])
16+
17+
symbols = ",".join(symbols)
18+
params = {"access_key": settings.MARKETSTACK_API_KEY}
19+
url = "http://api.marketstack.com/v1/eod/latest?symbols={}".format(symbols)
20+
api_result = requests.get(url, params)
21+
api_response = api_result.json()
22+
23+
results = {}
24+
25+
if "error" in api_response:
26+
for fetcher in data:
27+
results[fetcher] = (
28+
False,
29+
f"Could not fetch prices from marketstack: '{api_response['error']['message']}'.",
30+
)
31+
return results
32+
33+
for fetcher, input in data.items():
34+
symbol = input["symbol"]
35+
results[fetcher] = (False, "Price not found in response.")
36+
for price in api_response["data"]:
37+
if price["symbol"] == symbol:
38+
results[fetcher] = (True, round(price["close"], 2))
39+
break

apps/stocks/fetcher/selenium.py

Lines changed: 40 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,43 @@
22
import time
33
from bs4 import BeautifulSoup
44
from apps.core.selenium import get_chrome_driver
5-
from apps.stocks.fetcher.price import save_price
6-
from apps.stocks.models import PriceFetcher, Stock
7-
8-
9-
def fetch_price_with_selenium_fetcher(fetcher: PriceFetcher) -> tuple[bool, str]:
10-
stock = fetcher.stock
11-
website = fetcher.data["website"]
12-
target = fetcher.data["target"]
13-
14-
browser = get_chrome_driver()
15-
try:
16-
browser.get(website)
17-
time.sleep(5) # wait for the api requests to finish
18-
html = browser.page_source
19-
except Exception as e:
20-
return False, f"An error occured while trying to connect to {website}: {e}."
21-
finally:
22-
browser.quit()
23-
24-
soup = BeautifulSoup(html, features="html.parser")
25-
selection = soup.select_one(target)
26-
27-
if not selection:
28-
return (
29-
False,
30-
f"Could not find a price for {stock.ticker} on {website} with {target}.",
31-
)
32-
33-
result = re.search("\d{1,5}[.,]\d{2}", str(selection))
34-
35-
if not result:
36-
return False, f"Could not find a price inside '{selection}'."
37-
38-
price = result.group()
39-
price = price.replace(",", ".")
40-
price = float(price)
41-
save_price(price, stock)
42-
return True, ""
43-
44-
45-
def fetch_prices_with_selenium_fetchers(
46-
stocks: list[Stock], messages: list[str] | None = None
47-
):
48-
i = 0
49-
for stock in stocks:
50-
fetchers = list(stock.price_fetchers.filter(type="SELENIUM"))
51-
for fetcher in fetchers:
52-
success, message = fetch_price_with_selenium_fetcher(fetcher)
53-
if success:
54-
break
55-
if messages is not None:
56-
messages.append(message)
57-
time.sleep(i)
58-
i += 1
5+
from apps.stocks.fetcher.base import Fetcher
6+
7+
8+
class SeleniumFetcher(Fetcher):
9+
def fetch_single(self, website: str, target: str) -> tuple[bool, str | float]:
10+
browser = get_chrome_driver()
11+
try:
12+
browser.get(website)
13+
time.sleep(5) # wait for the api requests to finish
14+
html = browser.page_source
15+
except Exception as e:
16+
return False, f"An error occured while trying to connect to {website}: {e}."
17+
finally:
18+
browser.quit()
19+
20+
soup = BeautifulSoup(html, features="html.parser")
21+
selection = soup.select_one(target)
22+
23+
if not selection:
24+
return (
25+
False,
26+
f"Could not find a price for on {website} with {target}.",
27+
)
28+
29+
result = re.search("\d{1,5}[.,]\d{2}", str(selection))
30+
31+
if not result:
32+
return False, f"Could not find a price inside '{selection}'."
33+
34+
price = result.group()
35+
price = price.replace(",", ".")
36+
price = float(price)
37+
return True, price
38+
39+
def fetch_multiple(self, data: dict[str, dict[str, str | int]]) -> dict[str, tuple[bool, str | float]]:
40+
results = {}
41+
for fetcher, input in data.items():
42+
result = self.fetch_single(**input)
43+
results[fetcher] = result
44+
return results

apps/stocks/fetcher/website.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from bs4 import BeautifulSoup
44
import requests
55
from apps.stocks.fetcher.base import Fetcher
6-
from apps.stocks.models import PriceFetcher, Stock
76

87

98
headers = {
@@ -22,11 +21,7 @@
2221

2322

2423
class WebsiteFetcher(Fetcher):
25-
def fetch_single(self, fetcher: PriceFetcher) -> tuple[bool, str | float]:
26-
stock = fetcher.stock
27-
website = fetcher.data["website"]
28-
target = fetcher.data["target"]
29-
24+
def fetch_single(self, website: str, target: str) -> tuple[bool, str | float]:
3025
try:
3126
resp = requests.get(website, headers=headers)
3227
html = resp.text
@@ -45,7 +40,7 @@ def fetch_single(self, fetcher: PriceFetcher) -> tuple[bool, str | float]:
4540
if not selection:
4641
return (
4742
False,
48-
f"Could not find a price for {stock.ticker} on {website} with {target}.",
43+
f"Could not find a price on {website} with {target}.",
4944
)
5045

5146
result = re.search("\d{1,5}[.,]\d{2}", str(selection))
@@ -59,14 +54,13 @@ def fetch_single(self, fetcher: PriceFetcher) -> tuple[bool, str | float]:
5954
return True, price
6055

6156
def fetch_multiple(
62-
self, fetchers: list[PriceFetcher]
63-
) -> dict[Stock, tuple[bool, str | float]]:
57+
self, data: dict[str, dict[str, str | int]]
58+
) -> dict[str, tuple[bool, str | float]]:
6459
i = 0
6560
results = {}
66-
for fetcher in fetchers:
67-
assert fetcher.type == "WEBSITE"
68-
result = self.fetch_single(fetcher)
69-
results[fetcher.stock] = result
61+
for fetcher, input in data.items():
62+
result = self.fetch_single(**input)
63+
results[fetcher] = result
7064
time.sleep(i)
7165
i += 1
7266
return results

0 commit comments

Comments
 (0)