diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e941ab2..4495df6 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: python-version: 3.x - name: Install Dependencies run: | - pip install pytest requests pandas yfinance + pip install pytest requests pandas yfinance psycopg2-binary - name: pytest run: | pytest tests/ \ No newline at end of file diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100755 index 0000000..c15d8f9 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,28 @@ +FROM ubuntu:22.04 + +# set a directory for the app +WORKDIR /usr/src/ + +# copy all the files to the container +COPY . . + +# install dependencies +RUN apt-get update +RUN apt-get install -y python3 +RUN apt install -y python3-pip +RUN pip install --no-cache-dir -r requirements.txt +RUN pip install flask + +# Copy the shell script into the image +COPY entrypoint.sh /usr/local/bin/entrypoint.sh + +# Make the shell script executable +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Set the entrypoint +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] + +# EXPOSE 8000 + +# CMD cd django-stock-tracker && python3 manage.py runserver 0.0.0.0:8000 Can run this in the terminal +# but need to specify docker run -it -p 8888:8000 stock \ No newline at end of file diff --git a/src/entrypoint.sh b/src/entrypoint.sh new file mode 100644 index 0000000..ddbb67c --- /dev/null +++ b/src/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd .. +python3 src/flask_endpoints.py diff --git a/src/flask_endpoints.py b/src/flask_endpoints.py new file mode 100644 index 0000000..eead358 --- /dev/null +++ b/src/flask_endpoints.py @@ -0,0 +1,105 @@ +"""This module creates the endpoints to be called by streamlit.""" + +from typing import Union, Any +from flask import Flask, jsonify, request # pylint: disable=E0401 +import pandas as pd +from src.model import Model +from src.live_price_display import LivePriceDisplay # type: ignore[import-untyped] +from src.news_display import NewsDisplay +import sys # pylint: disable=C0411 +import os # pylint: disable=C0411 +sys.path.append(os.getcwd()) + +models: Model = Model() +news_disp: NewsDisplay = NewsDisplay() +price_disp: LivePriceDisplay = LivePriceDisplay() +all_data: Union[pd.DataFrame, Any] = models.process_data() + +app = Flask(__name__) + +def update_graph(company: str) -> str: + """ + This method gets the data from the selected company. + + Args: + company: The ticker symbol of the company + + Returns: + chart_data: A DataFrame containing required information of all companies + """ + raw_data: pd.Series = all_data[company] + data: dict = { + "date": raw_data["trade_date"], + "close": raw_data["close"] + } + df: pd.DataFrame = pd.DataFrame(data) # pylint: disable=C0103 + chart_data: str = df.to_json() + return chart_data + +def update_price(company: str) -> Union[float, str]: + """ + Returns the price of the selected company using core modules. + + Args: + company_name: The ticker symbol of the company. + + Returns: + The most recent price. + """ + price: Union[float, str] = price_disp.display_final_price_yf(company) + return price + +def update_news(company: str) -> list: + """ + Get the formatted news. + + Args: + company: The ticker symbol of the company + + Returns: + news: The most recent five articles + """ + news: list = news_disp.format_news_django(company) + return news + +@app.route("/model/generate_company_list", methods=["GET"]) +def generate_company_list(): + """ + Returns the list of companies. + + Args: + None. + + Returns: + ticker_list: The list of companies in json format. + """ + ticker_list: list + ticker_list, _ = models.generate_company_list() + return jsonify(ticker_list) + +@app.route("/update_data", methods=["GET", "POST"]) +def update_data(): + """ + Returns the data of the selected company. + + Args: + None. + + Returns: + response: The data for the selected company in json. + """ + response: dict = {} + data: dict = request.get_json() + company: str = data["company"] + print(f"Received company: {company}") + processed_price: Union[float, str] = update_price(company) + processed_news: list = update_news(company) + processed_chart_data: str = update_graph(company) + + response["price"] = processed_price + response["news"] = processed_news + response["graph"] = processed_chart_data + return jsonify(response) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/src/live_price_display.py b/src/live_price_display.py index 89ea218..27b1788 100755 --- a/src/live_price_display.py +++ b/src/live_price_display.py @@ -22,10 +22,10 @@ def display_final_price_av(company_name: str) -> Union[str, dict, Any]: Returns a the price using Alpha Vantage. Args: - company_name: The ticker symbol of the company + company_name: The ticker symbol of the company. Returns: - The most recent price in string + The most recent price. """ try: # Gets last available price by default @@ -57,13 +57,13 @@ def display_final_price_av(company_name: str) -> Union[str, dict, Any]: @staticmethod def display_final_price_yf(company_name: str) -> Union[float, str]: """ - Returns a the price using Yahoo Finance. + Returns the price of the selected company using Yahoo Finance. Args: - company_name: The ticker symbol of the company + company_name: The ticker symbol of the company. Returns: - The most recent price in string + The most recent price. """ # Uncomment below for full company names in selection rather than ticker symbols. # conn = psycopg2.connect(database = "stocks", user='postgres', password='123456') diff --git a/src/model.py b/src/model.py index a3bf243..b2ee8bf 100755 --- a/src/model.py +++ b/src/model.py @@ -22,7 +22,7 @@ def generate_company_list() -> Tuple[list, list]: :return: (list) A list of companies. """ conn = psycopg2.connect( - host="172.19.0.2", + host="stocks-postgres", database="stocks", user="postgres", password="123456", @@ -89,7 +89,11 @@ def process_data(self) -> Union[pd.DataFrame, str]: companies_list: Tuple[list, list] = self.generate_company_list() companies_data: dict = {} conn: psycopg2.extensions.connection = psycopg2.connect( - database="stocks", user="postgres", password="123456" + host="stocks-postgres", + database="stocks", + user="postgres", + password="123456", + port="5432" ) number_of_companies: int = len(companies_list[0]) for company_idx in range(1, number_of_companies + 1): diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100755 index 0000000..0f0160b --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,49 @@ +appdirs==1.4.4 +asgiref==3.7.2 +beautifulsoup4==4.12.3 +certifi==2023.5.7 +charset-normalizer==3.1.0 +coverage==7.4.3 +Django==4.1.12 +django-admin-volt==1.0.10 +exceptiongroup==1.1.1 +frozendict==2.4.0 +gunicorn==21.2.0 +html5lib==1.1 +idna==3.4 +iniconfig==2.0.0 +install==1.3.5 +jmespath==1.0.1 +lxml==4.9.3 +multitasking==0.0.11 +mypy==1.8.0 +mypy-extensions==1.0.0 +numpy==1.26.4 +packaging==23.1 +pandas==2.2.1 +pandas-stubs==2.2.0.240218 +peewee==3.17.1 +pluggy==1.0.0 +psycopg2-binary==2.9.9 +pytest==7.3.1 +pytest-cov==4.1.0 +python-dateutil==2.8.2 +python-dotenv==1.0.0 +pytz==2023.3 +requests==2.31.0 +ruff==0.3.0 +six==1.16.0 +soupsieve==2.5 +sqlparse==0.4.4 +tomli==2.0.1 +types-openpyxl==3.1.0.20240301 +types-psycopg2==2.9.21.20240311 +types-pytz==2024.1.0.20240203 +types-requests==2.31.0.20240218 +typing_extensions==4.8.0 +tzdata==2023.3 +urllib3==2.0.2 +utils==1.0.1 +webencodings==0.5.1 +whitenoise==6.5.0 +yfinance==0.2.37 diff --git a/streamlit/Dockerfile b/streamlit/Dockerfile new file mode 100644 index 0000000..0b394d9 --- /dev/null +++ b/streamlit/Dockerfile @@ -0,0 +1,22 @@ +# app/Dockerfile + +FROM python:3.10src/requirements.txt-slim + +WORKDIR /app + +COPY . . + +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + software-properties-common \ + git \ + && rm -rf /var/lib/apt/lists/* + +RUN pip3 install -r requirements.txt + +EXPOSE 8501 + +HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health + +ENTRYPOINT ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"] \ No newline at end of file diff --git a/streamlit/requirements.txt b/streamlit/requirements.txt new file mode 100755 index 0000000..bd70e41 --- /dev/null +++ b/streamlit/requirements.txt @@ -0,0 +1,42 @@ +appdirs==1.4.4 +asgiref==3.7.2 +beautifulsoup4==4.12.3 +certifi==2023.5.7 +charset-normalizer==3.1.0 +coverage==7.4.3 +exceptiongroup==1.1.1 +frozendict==2.4.0 +gunicorn==21.2.0 +html5lib==1.1 +idna==3.4 +iniconfig==2.0.0 +install==1.3.5 +jmespath==1.0.1 +lxml==4.9.3 +multitasking==0.0.11 +numpy==1.26.4 +packaging==23.1 +pandas==2.2.1 +pandas-stubs==2.2.0.240218 +peewee==3.17.1 +plotly==5.20.0 +pluggy==1.0.0 +python-dateutil==2.8.2 +python-dotenv==1.0.0 +pytz==2023.3 +requests==2.31.0 +ruff==0.3.0 +six==1.16.0 +soupsieve==2.5 +sqlparse==0.4.4 +streamlit==1.33.0 +tomli==2.0.1 +types-openpyxl==3.1.0.20240301 +types-pytz==2024.1.0.20240203 +types-requests==2.31.0.20240218 +typing_extensions==4.8.0 +tzdata==2023.3 +urllib3==2.0.2 +utils==1.0.1 +webencodings==0.5.1 +whitenoise==6.5.0 diff --git a/streamlit/streamlit_app.py b/streamlit/streamlit_app.py new file mode 100644 index 0000000..42bcb0d --- /dev/null +++ b/streamlit/streamlit_app.py @@ -0,0 +1,46 @@ +"""This module configures the streamlit web app.""" +import json +import requests +import plotly.express as px # type: ignore[import-untyped] # pylint: disable=E0401 +import pandas as pd +import streamlit as st # type: ignore[import-untyped] # pylint: disable=E0401 + + +company_list_response: requests.Response = requests.get( + "http://core-modules:5000/model/generate_company_list" + ) +company_list: list = company_list_response.json() +st.write("Hello, let's learn more about a company together!") +company = st.selectbox("Pick a company", [None] + company_list) +st.write("You selected:", company) + +if company: + payload: dict = {"company": company} + update_response: requests.Response = requests.post( + "http://core-modules:5000/update_data", json=payload + ) + price: float = update_response.json()["price"] + news: list = update_response.json()["news"] + st.sidebar.write(f"{company}'s most recent price: {price}") + + news_container = st.sidebar.container() + for article in news: + news_container.markdown(f"- [{article['title']}]({article['url']})") + + chart_data: dict = json.loads(update_response.json()["graph"]) + date: dict + close: dict + date, close = chart_data["date"].values(), chart_data["close"].values() + df = pd.DataFrame(close, date) + + fig = px.line(df) + + fig.update_layout( + title=company, + xaxis_title='Date', + yaxis_title='Close', + width=800, + height=600 + ) + + st.plotly_chart(fig)