From e84371a6791b31ac092cfc558a4e2357db94d284 Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Mon, 11 Nov 2024 01:37:24 -0700 Subject: [PATCH 01/19] Added codes to python files --- .github/workflows/ci_cd.yml | 38 +++++++++++++ Dockerfile | 10 ++++ docker-compose.yml | 8 +++ pyproject.toml | 1 + .../stock_valuation_app/config.yml | 0 src/stock_valuation_app/data/fmp_client.py | 25 +++++++++ src/stock_valuation_app/models/stock.py | 55 +++++++++++++++++++ uv.lock | 28 ++++++++++ 8 files changed, 165 insertions(+) rename config.yml => src/stock_valuation_app/config.yml (100%) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index e69de29..f57a483 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -0,0 +1,38 @@ +name: CI/CD + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.11' + - name: Install dependencies + run: | + pip install uv + uv pip install -e .[dev] + - name: Run tests + run: pytest + + deploy: + needs: test + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v2 + - name: Build and push Docker image + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: | + docker build -t your-docker-repo/stock-valuation-app:latest . + echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin + docker push your-docker-repo/stock-valuation-app:latest \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e69de29..ee05652 100644 --- a/Dockerfile +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY pyproject.toml uv.lock ./ +RUN pip install uv && uv pip install -e . + +COPY src ./src + +CMD ["uvicorn", "stock_valuation_app:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e69de29..2f52801 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + web: + build: . + ports: + - "8000:8000" + environment: + - FMP_API_KEY=your_api_key_here \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1539d18..e786811 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "polars>=1.12.0", "pyarrow>=18.0.0", "pydantic>=2.9.2", + "pyyaml>=6.0.2", ] [project.scripts] diff --git a/config.yml b/src/stock_valuation_app/config.yml similarity index 100% rename from config.yml rename to src/stock_valuation_app/config.yml diff --git a/src/stock_valuation_app/data/fmp_client.py b/src/stock_valuation_app/data/fmp_client.py index e69de29..0c62e40 100644 --- a/src/stock_valuation_app/data/fmp_client.py +++ b/src/stock_valuation_app/data/fmp_client.py @@ -0,0 +1,25 @@ +import httpx +from pydantic import BaseModel + + +class FinancialData(BaseModel): + revenue: float + netIncome: float + freeCashFlow: float + dividendsPaid: float + + +class FMPClient: + def __init__(self, api_key: str): + self.base_url = "https://financialmodelingprep.com/api/v3" + self.api_key = api_key + + async def get_financial_data( + self, symbol: str, limit: int = 10 + ) -> list[FinancialData]: + url = f"{self.base_url}/income-statement/{symbol}?limit={limit}&apikey={self.api_key}" + async with httpx.AsyncClient() as client: + response = await client.get(url) + response.raise_for_status() + data = response.json() + return [FinancialData(**item) for item in data] diff --git a/src/stock_valuation_app/models/stock.py b/src/stock_valuation_app/models/stock.py index e69de29..63e85ef 100644 --- a/src/stock_valuation_app/models/stock.py +++ b/src/stock_valuation_app/models/stock.py @@ -0,0 +1,55 @@ +from pydantic import BaseModel, Field +from typing import Optional, List + + +class FinancialData(BaseModel): + revenue: float + net_income: float + free_cash_flow: float + dividends_paid: float + date: str + + +class GrowthRates(BaseModel): + revenue: dict[str, float] + net_income: dict[str, float] + free_cash_flow: dict[str, float] + dividends_paid: dict[str, float] + + +class StockValuation(BaseModel): + symbol: str + current_price: float + pe_ratio: float + dividend_yield: Optional[float] = None + growth_rates: GrowthRates + is_quality_dividend_growth_stock: bool + is_undervalued: bool + + +class Stock(BaseModel): + symbol: str + company_name: str + sector: Optional[str] = None + industry: Optional[str] = None + financial_data: List[FinancialData] + valuation: Optional[StockValuation] = None + + class Config: + schema_extra = { + "example": { + "symbol": "AAPL", + "company_name": "Apple Inc.", + "sector": "Technology", + "industry": "Consumer Electronics", + "financial_data": [ + { + "revenue": 365817000000, + "net_income": 94680000000, + "free_cash_flow": 90215000000, + "dividends_paid": 14467000000, + "date": "2022-09-30", + } + ], + } + } diff --git a/uv.lock b/uv.lock index 36039e5..6f5b635 100644 --- a/uv.lock +++ b/uv.lock @@ -830,6 +830,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, ] +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + [[package]] name = "pyzmq" version = "26.2.0" @@ -991,6 +1017,7 @@ dependencies = [ { name = "polars" }, { name = "pyarrow" }, { name = "pydantic" }, + { name = "pyyaml" }, ] [package.dev-dependencies] @@ -1010,6 +1037,7 @@ requires-dist = [ { name = "polars", specifier = ">=1.12.0" }, { name = "pyarrow", specifier = ">=18.0.0" }, { name = "pydantic", specifier = ">=2.9.2" }, + { name = "pyyaml", specifier = ">=6.0.2" }, ] [package.metadata.requires-dev] From 8af16ded473ece88d358f426a6b446867424e312 Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Tue, 12 Nov 2024 21:48:23 -0700 Subject: [PATCH 02/19] Modified python code files --- pyproject.toml | 1 + src/stock_valuation_app/__init__.py | 11 +++- src/stock_valuation_app/api/routes.py | 33 ++++++++++++ src/stock_valuation_app/config.yml | 10 +++- src/stock_valuation_app/data/fmp_client.py | 36 ++++++++----- src/stock_valuation_app/main.py | 0 src/stock_valuation_app/services/valuation.py | 36 +++++++++++++ src/stock_valuation_app/ui/dashboard.py | 54 +++++++++++++++++++ src/stock_valuation_app/utils/__init__.py | 0 src/stock_valuation_app/utils/utils.py | 45 ++++++++++++++++ uv.lock | 17 ++++-- 11 files changed, 222 insertions(+), 21 deletions(-) create mode 100644 src/stock_valuation_app/main.py create mode 100644 src/stock_valuation_app/utils/__init__.py create mode 100644 src/stock_valuation_app/utils/utils.py diff --git a/pyproject.toml b/pyproject.toml index e786811..868d288 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "polars>=1.12.0", "pyarrow>=18.0.0", "pydantic>=2.9.2", + "python-dotenv>=1.0.1", "pyyaml>=6.0.2", ] diff --git a/src/stock_valuation_app/__init__.py b/src/stock_valuation_app/__init__.py index e4a8c91..c346459 100644 --- a/src/stock_valuation_app/__init__.py +++ b/src/stock_valuation_app/__init__.py @@ -1,2 +1,9 @@ -def main() -> None: - print("Hello from stock-valuation-app!") +from fastapi import FastAPI +from .api.routes import router +from .ui.dashboard import app as dash_app + +app = FastAPI() +app.include_router(router) + +# Mount Dash app +app.mount("/dashboard", dash_app.server) diff --git a/src/stock_valuation_app/api/routes.py b/src/stock_valuation_app/api/routes.py index e69de29..1fa8ff7 100644 --- a/src/stock_valuation_app/api/routes.py +++ b/src/stock_valuation_app/api/routes.py @@ -0,0 +1,33 @@ +from fastapi import APIRouter, Depends +from ..data.fmp_client import FMPClient +from ..services.valuation import ( + calculate_growth_rates, + is_quality_dividend_growth_stock, + is_undervalued, +) + +router = APIRouter() + + +async def get_fmp_client(): + return FMPClient("YOUR_API_KEY") + + +@router.get("/stock/{symbol}") +async def get_stock_valuation( + symbol: str, fmp_client: FMPClient = Depends(get_fmp_client) +): + financial_data = await fmp_client.get_financial_data(symbol) + growth_rates = calculate_growth_rates(financial_data) + + # Assuming we have a way to get the current P/E ratio + current_pe = 15 # This should be fetched from the API + + return { + "symbol": symbol, + "growth_rates": growth_rates, + "is_quality_dividend_growth_stock": is_quality_dividend_growth_stock( + growth_rates + ), + "is_undervalued": is_undervalued(growth_rates, current_pe), + } diff --git a/src/stock_valuation_app/config.yml b/src/stock_valuation_app/config.yml index 2e7a46a..5d25ea5 100644 --- a/src/stock_valuation_app/config.yml +++ b/src/stock_valuation_app/config.yml @@ -1,5 +1,8 @@ api: base_url: "https://financialmodelingprep.com/api/v3" + annual_ratios: "ratios/" + annual_financial_growth: "financial-growth/" + rating: "rating/" valuation: default_pe_ratio: 15 @@ -15,5 +18,8 @@ database: file: "stock_data.duckdb" logging: - level: "INFO" - file: "app.log" \ No newline at end of file + level: INFO + file: app.log + filemode: "w" + format: "%(asctime)s - %(levelname)s - %(message)s" + datefmt: "%Y-%m-%d %H:%M:%S" \ No newline at end of file diff --git a/src/stock_valuation_app/data/fmp_client.py b/src/stock_valuation_app/data/fmp_client.py index 0c62e40..ef5ba02 100644 --- a/src/stock_valuation_app/data/fmp_client.py +++ b/src/stock_valuation_app/data/fmp_client.py @@ -1,25 +1,33 @@ +from dataclasses import dataclass, field +from urllib.parse import urljoin import httpx +from typing import Any, Optional from pydantic import BaseModel +from stock_valuation_app.utils import utils -class FinancialData(BaseModel): - revenue: float - netIncome: float - freeCashFlow: float - dividendsPaid: float +@dataclass +class FMPClient: + """A client for interacting with the Financial Modeling Prep API.""" + base_url: str = field(default_factory=utils.get_base_url) + api_key: str = field(default_factory=utils.get_api_key) -class FMPClient: - def __init__(self, api_key: str): - self.base_url = "https://financialmodelingprep.com/api/v3" - self.api_key = api_key + async def fetch_data(self, endpoint: str, symbol: str, period: Optional[str] = "annual"): # params: dict[str, Any] + """Fetch data from the Financial Modeling Prep API.""" + base_endpoint = urljoin(self.base_url, endpoint) + if period is None: + url = f"{base_endpoint}/{symbol}?apikey={self.api_key}" + else: + url = f"{base_endpoint}/{symbol}?period={period}&apikey={self.api_key}" - async def get_financial_data( - self, symbol: str, limit: int = 10 - ) -> list[FinancialData]: - url = f"{self.base_url}/income-statement/{symbol}?limit={limit}&apikey={self.api_key}" async with httpx.AsyncClient() as client: response = await client.get(url) response.raise_for_status() data = response.json() - return [FinancialData(**item) for item in data] + return data + +api_client = FMPClient() +rating_endpoint = utils.get_endpoint("rating") +data = api_client.fetch_data(rating_endpoint, "AAPL") +print(data) \ No newline at end of file diff --git a/src/stock_valuation_app/main.py b/src/stock_valuation_app/main.py new file mode 100644 index 0000000..e69de29 diff --git a/src/stock_valuation_app/services/valuation.py b/src/stock_valuation_app/services/valuation.py index e69de29..f6f0c4d 100644 --- a/src/stock_valuation_app/services/valuation.py +++ b/src/stock_valuation_app/services/valuation.py @@ -0,0 +1,36 @@ +import polars as pl +from ..data.fmp_client import FinancialData + + +def calculate_growth_rates(data: list[FinancialData]) -> dict[str, dict[str, float]]: + df = pl.DataFrame([d.dict() for d in data]) + + metrics = ["revenue", "netIncome", "freeCashFlow", "dividendsPaid"] + periods = [3, 5, 10] + + results = {} + for metric in metrics: + metric_results = {} + for period in periods: + if len(df) >= period: + growth_rate = (df[metric][0] / df[metric][period - 1]) ** ( + 1 / period + ) - 1 + metric_results[f"{period}y"] = growth_rate + results[metric] = metric_results + + return results + + +def is_quality_dividend_growth_stock(growth_rates: dict[str, dict[str, float]]) -> bool: + revenue_growth = growth_rates["revenue"]["5y"] + dividend_growth = growth_rates["dividendsPaid"]["5y"] + return revenue_growth > 0.05 and dividend_growth > 0.05 + + +def is_undervalued( + growth_rates: dict[str, dict[str, float]], current_pe: float +) -> bool: + earnings_growth = growth_rates["netIncome"]["5y"] + fair_pe = earnings_growth * 100 # PEG ratio of 1 + return current_pe < fair_pe diff --git a/src/stock_valuation_app/ui/dashboard.py b/src/stock_valuation_app/ui/dashboard.py index e69de29..38fcaf1 100644 --- a/src/stock_valuation_app/ui/dashboard.py +++ b/src/stock_valuation_app/ui/dashboard.py @@ -0,0 +1,54 @@ +import dash +from dash import dcc, html +from dash.dependencies import Input, Output +import plotly.graph_objs as go +import httpx + +app = dash.Dash(__name__) + +app.layout = html.Div( + [ + html.H1("Stock Valuation Dashboard"), + dcc.Input(id="stock-input", type="text", placeholder="Enter stock symbol"), + html.Button("Analyze", id="analyze-button"), + html.Div(id="valuation-output"), + dcc.Graph(id="growth-chart"), + ] +) + + +@app.callback( + [Output("valuation-output", "children"), Output("growth-chart", "figure")], + [Input("analyze-button", "n_clicks")], + [dash.dependencies.State("stock-input", "value")], +) +def update_valuation(n_clicks, symbol): + if n_clicks is None or not symbol: + return dash.no_update, dash.no_update + + # Fetch data from our FastAPI endpoint + response = httpx.get(f"http://localhost:8000/stock/{symbol}") + data = response.json() + + # Create valuation output + valuation_output = [ + html.P( + f"Is quality dividend growth stock: {data['is_quality_dividend_growth_stock']}" + ), + html.P(f"Is undervalued: {data['is_undervalued']}"), + ] + + # Create growth chart + traces = [] + for metric, rates in data["growth_rates"].items(): + trace = go.Bar(x=list(rates.keys()), y=list(rates.values()), name=metric) + traces.append(trace) + + layout = go.Layout(title="Growth Rates", barmode="group") + figure = go.Figure(data=traces, layout=layout) + + return valuation_output, figure + + +if __name__ == "__main__": + app.run_server(debug=True) diff --git a/src/stock_valuation_app/utils/__init__.py b/src/stock_valuation_app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/stock_valuation_app/utils/utils.py b/src/stock_valuation_app/utils/utils.py new file mode 100644 index 0000000..ce6587b --- /dev/null +++ b/src/stock_valuation_app/utils/utils.py @@ -0,0 +1,45 @@ +import os +from pathlib import Path +from dotenv import load_dotenv +import yaml + +# Load environment variables from .env file +load_dotenv() +# Define the path to the config.yml file +config_file = Path("../config.yml") + +def load_config(): + """Load configuration from a YAML file.""" + with open(f"{config_file}", "r", encoding="utf-8") as file: + config = yaml.safe_load(file) + return config + + +def get_section_config(section: str): + """Get configuration for a specific section.""" + match section: + case "api": + return load_config().get("api") + case "valuation": + return load_config().get("valuation") + case "dashboard": + return load_config().get("dashboard") + case "database": + return load_config().get("database") + case "logging": + return load_config().get("logging") + case _: + raise ValueError(f"Invalid section: {section}") + +def get_endpoint(url: str) -> str: + """Get endpoints from the configuration.""" + api_config = get_section_config("api") + return api_config.get(f"{url}") + +def get_base_url() -> str: + """Get the base URL from the configuration.""" + return get_endpoint("base_url") + +def get_api_key() -> str: + """Get the API key from the .env file.""" + return os.getenv("FMP_API_KEY", "") \ No newline at end of file diff --git a/uv.lock b/uv.lock index 6f5b635..aebb64c 100644 --- a/uv.lock +++ b/uv.lock @@ -817,6 +817,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + [[package]] name = "pywin32" version = "308" @@ -954,11 +963,11 @@ wheels = [ [[package]] name = "setuptools" -version = "75.3.0" +version = "75.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/22/a438e0caa4576f8c383fa4d35f1cc01655a46c75be358960d815bfbb12bd/setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686", size = 1351577 } +sdist = { url = "https://files.pythonhosted.org/packages/e2/73/c1ccf3e057ef6331cc6861412905dc218203bde46dfe8262c1631aa7fb11/setuptools-75.4.0.tar.gz", hash = "sha256:1dc484f5cf56fd3fe7216d7b8df820802e7246cfb534a1db2aa64f14fcb9cdcb", size = 1336593 } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/12/282ee9bce8b58130cb762fbc9beabd531549952cac11fc56add11dcb7ea0/setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd", size = 1251070 }, + { url = "https://files.pythonhosted.org/packages/21/df/7c6bb83dcb45b35dc35b310d752f254211cde0bcd2a35290ea6e2862b2a9/setuptools-75.4.0-py3-none-any.whl", hash = "sha256:b3c5d862f98500b06ffdf7cc4499b48c46c317d8d56cb30b5c8bce4d88f5c216", size = 1223131 }, ] [[package]] @@ -1017,6 +1026,7 @@ dependencies = [ { name = "polars" }, { name = "pyarrow" }, { name = "pydantic" }, + { name = "python-dotenv" }, { name = "pyyaml" }, ] @@ -1037,6 +1047,7 @@ requires-dist = [ { name = "polars", specifier = ">=1.12.0" }, { name = "pyarrow", specifier = ">=18.0.0" }, { name = "pydantic", specifier = ">=2.9.2" }, + { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "pyyaml", specifier = ">=6.0.2" }, ] From b0b52c53a798f611045b12cf206ff4a0a0f2eed9 Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Tue, 12 Nov 2024 23:23:48 -0700 Subject: [PATCH 03/19] Cleaned up files --- src/stock_valuation_app/api/__init__.py | 0 src/stock_valuation_app/api/routes.py | 33 ---------- src/stock_valuation_app/data/fmp_client.py | 8 +-- src/stock_valuation_app/main.py | 60 +++++++++++++++++++ src/stock_valuation_app/models/__init__.py | 0 src/stock_valuation_app/models/stock.py | 55 ----------------- src/stock_valuation_app/services/__init__.py | 0 src/stock_valuation_app/services/valuation.py | 36 ----------- src/stock_valuation_app/ui/__init__.py | 0 src/stock_valuation_app/ui/dashboard.py | 54 ----------------- src/stock_valuation_app/utils/__init__.py | 0 src/stock_valuation_app/utils/utils.py | 45 -------------- 12 files changed, 61 insertions(+), 230 deletions(-) delete mode 100644 src/stock_valuation_app/api/__init__.py delete mode 100644 src/stock_valuation_app/api/routes.py delete mode 100644 src/stock_valuation_app/models/__init__.py delete mode 100644 src/stock_valuation_app/models/stock.py delete mode 100644 src/stock_valuation_app/services/__init__.py delete mode 100644 src/stock_valuation_app/services/valuation.py delete mode 100644 src/stock_valuation_app/ui/__init__.py delete mode 100644 src/stock_valuation_app/ui/dashboard.py delete mode 100644 src/stock_valuation_app/utils/__init__.py delete mode 100644 src/stock_valuation_app/utils/utils.py diff --git a/src/stock_valuation_app/api/__init__.py b/src/stock_valuation_app/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/stock_valuation_app/api/routes.py b/src/stock_valuation_app/api/routes.py deleted file mode 100644 index 1fa8ff7..0000000 --- a/src/stock_valuation_app/api/routes.py +++ /dev/null @@ -1,33 +0,0 @@ -from fastapi import APIRouter, Depends -from ..data.fmp_client import FMPClient -from ..services.valuation import ( - calculate_growth_rates, - is_quality_dividend_growth_stock, - is_undervalued, -) - -router = APIRouter() - - -async def get_fmp_client(): - return FMPClient("YOUR_API_KEY") - - -@router.get("/stock/{symbol}") -async def get_stock_valuation( - symbol: str, fmp_client: FMPClient = Depends(get_fmp_client) -): - financial_data = await fmp_client.get_financial_data(symbol) - growth_rates = calculate_growth_rates(financial_data) - - # Assuming we have a way to get the current P/E ratio - current_pe = 15 # This should be fetched from the API - - return { - "symbol": symbol, - "growth_rates": growth_rates, - "is_quality_dividend_growth_stock": is_quality_dividend_growth_stock( - growth_rates - ), - "is_undervalued": is_undervalued(growth_rates, current_pe), - } diff --git a/src/stock_valuation_app/data/fmp_client.py b/src/stock_valuation_app/data/fmp_client.py index ef5ba02..c516de0 100644 --- a/src/stock_valuation_app/data/fmp_client.py +++ b/src/stock_valuation_app/data/fmp_client.py @@ -3,7 +3,6 @@ import httpx from typing import Any, Optional from pydantic import BaseModel -from stock_valuation_app.utils import utils @dataclass @@ -25,9 +24,4 @@ async def fetch_data(self, endpoint: str, symbol: str, period: Optional[str] = " response = await client.get(url) response.raise_for_status() data = response.json() - return data - -api_client = FMPClient() -rating_endpoint = utils.get_endpoint("rating") -data = api_client.fetch_data(rating_endpoint, "AAPL") -print(data) \ No newline at end of file + return data \ No newline at end of file diff --git a/src/stock_valuation_app/main.py b/src/stock_valuation_app/main.py index e69de29..73a77ab 100644 --- a/src/stock_valuation_app/main.py +++ b/src/stock_valuation_app/main.py @@ -0,0 +1,60 @@ +import os +import yaml +from dotenv import load_dotenv +from stock_valuation_app.data.fmp_client import FMPClient + + +# Load environment variables from .env file +load_dotenv() + + +def load_config(): + """Load configuration from a YAML file.""" + with open("config.yml", "r", encoding="utf-8") as file: + config = yaml.safe_load(file) + return config + + +def get_section_config(section: str): + """Get configuration for a specific section.""" + match section: + case "api": + return load_config().get("api") + case "valuation": + return load_config().get("valuation") + case "dashboard": + return load_config().get("dashboard") + case "database": + return load_config().get("database") + case "logging": + return load_config().get("logging") + case _: + raise ValueError(f"Invalid section: {section}") + + +def get_endpoint(url: str) -> str: + """Get endpoints from the configuration.""" + api_config = get_section_config("api") + return api_config.get(f"{url}") + + +def get_base_url() -> str: + """Get the base URL from the configuration.""" + return get_endpoint("base_url") + + +def get_api_key() -> str: + """Get the API key from the .env file.""" + return os.getenv("FMP_API_KEY", "") + + +def main() -> None: + """Main function to fetch data from the Financial Modeling Prep API.""" + api_client = FMPClient() + rating_endpoint = get_endpoint("rating") + data = api_client.fetch_data(rating_endpoint, "AAPL") + print(data) + + +if __name__ == "__main__": + main() diff --git a/src/stock_valuation_app/models/__init__.py b/src/stock_valuation_app/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/stock_valuation_app/models/stock.py b/src/stock_valuation_app/models/stock.py deleted file mode 100644 index 63e85ef..0000000 --- a/src/stock_valuation_app/models/stock.py +++ /dev/null @@ -1,55 +0,0 @@ -from pydantic import BaseModel, Field -from typing import Optional, List - - -class FinancialData(BaseModel): - revenue: float - net_income: float - free_cash_flow: float - dividends_paid: float - date: str - - -class GrowthRates(BaseModel): - revenue: dict[str, float] - net_income: dict[str, float] - free_cash_flow: dict[str, float] - dividends_paid: dict[str, float] - - -class StockValuation(BaseModel): - symbol: str - current_price: float - pe_ratio: float - dividend_yield: Optional[float] = None - growth_rates: GrowthRates - is_quality_dividend_growth_stock: bool - is_undervalued: bool - - -class Stock(BaseModel): - symbol: str - company_name: str - sector: Optional[str] = None - industry: Optional[str] = None - financial_data: List[FinancialData] - valuation: Optional[StockValuation] = None - - class Config: - schema_extra = { - "example": { - "symbol": "AAPL", - "company_name": "Apple Inc.", - "sector": "Technology", - "industry": "Consumer Electronics", - "financial_data": [ - { - "revenue": 365817000000, - "net_income": 94680000000, - "free_cash_flow": 90215000000, - "dividends_paid": 14467000000, - "date": "2022-09-30", - } - ], - } - } diff --git a/src/stock_valuation_app/services/__init__.py b/src/stock_valuation_app/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/stock_valuation_app/services/valuation.py b/src/stock_valuation_app/services/valuation.py deleted file mode 100644 index f6f0c4d..0000000 --- a/src/stock_valuation_app/services/valuation.py +++ /dev/null @@ -1,36 +0,0 @@ -import polars as pl -from ..data.fmp_client import FinancialData - - -def calculate_growth_rates(data: list[FinancialData]) -> dict[str, dict[str, float]]: - df = pl.DataFrame([d.dict() for d in data]) - - metrics = ["revenue", "netIncome", "freeCashFlow", "dividendsPaid"] - periods = [3, 5, 10] - - results = {} - for metric in metrics: - metric_results = {} - for period in periods: - if len(df) >= period: - growth_rate = (df[metric][0] / df[metric][period - 1]) ** ( - 1 / period - ) - 1 - metric_results[f"{period}y"] = growth_rate - results[metric] = metric_results - - return results - - -def is_quality_dividend_growth_stock(growth_rates: dict[str, dict[str, float]]) -> bool: - revenue_growth = growth_rates["revenue"]["5y"] - dividend_growth = growth_rates["dividendsPaid"]["5y"] - return revenue_growth > 0.05 and dividend_growth > 0.05 - - -def is_undervalued( - growth_rates: dict[str, dict[str, float]], current_pe: float -) -> bool: - earnings_growth = growth_rates["netIncome"]["5y"] - fair_pe = earnings_growth * 100 # PEG ratio of 1 - return current_pe < fair_pe diff --git a/src/stock_valuation_app/ui/__init__.py b/src/stock_valuation_app/ui/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/stock_valuation_app/ui/dashboard.py b/src/stock_valuation_app/ui/dashboard.py deleted file mode 100644 index 38fcaf1..0000000 --- a/src/stock_valuation_app/ui/dashboard.py +++ /dev/null @@ -1,54 +0,0 @@ -import dash -from dash import dcc, html -from dash.dependencies import Input, Output -import plotly.graph_objs as go -import httpx - -app = dash.Dash(__name__) - -app.layout = html.Div( - [ - html.H1("Stock Valuation Dashboard"), - dcc.Input(id="stock-input", type="text", placeholder="Enter stock symbol"), - html.Button("Analyze", id="analyze-button"), - html.Div(id="valuation-output"), - dcc.Graph(id="growth-chart"), - ] -) - - -@app.callback( - [Output("valuation-output", "children"), Output("growth-chart", "figure")], - [Input("analyze-button", "n_clicks")], - [dash.dependencies.State("stock-input", "value")], -) -def update_valuation(n_clicks, symbol): - if n_clicks is None or not symbol: - return dash.no_update, dash.no_update - - # Fetch data from our FastAPI endpoint - response = httpx.get(f"http://localhost:8000/stock/{symbol}") - data = response.json() - - # Create valuation output - valuation_output = [ - html.P( - f"Is quality dividend growth stock: {data['is_quality_dividend_growth_stock']}" - ), - html.P(f"Is undervalued: {data['is_undervalued']}"), - ] - - # Create growth chart - traces = [] - for metric, rates in data["growth_rates"].items(): - trace = go.Bar(x=list(rates.keys()), y=list(rates.values()), name=metric) - traces.append(trace) - - layout = go.Layout(title="Growth Rates", barmode="group") - figure = go.Figure(data=traces, layout=layout) - - return valuation_output, figure - - -if __name__ == "__main__": - app.run_server(debug=True) diff --git a/src/stock_valuation_app/utils/__init__.py b/src/stock_valuation_app/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/stock_valuation_app/utils/utils.py b/src/stock_valuation_app/utils/utils.py deleted file mode 100644 index ce6587b..0000000 --- a/src/stock_valuation_app/utils/utils.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -from pathlib import Path -from dotenv import load_dotenv -import yaml - -# Load environment variables from .env file -load_dotenv() -# Define the path to the config.yml file -config_file = Path("../config.yml") - -def load_config(): - """Load configuration from a YAML file.""" - with open(f"{config_file}", "r", encoding="utf-8") as file: - config = yaml.safe_load(file) - return config - - -def get_section_config(section: str): - """Get configuration for a specific section.""" - match section: - case "api": - return load_config().get("api") - case "valuation": - return load_config().get("valuation") - case "dashboard": - return load_config().get("dashboard") - case "database": - return load_config().get("database") - case "logging": - return load_config().get("logging") - case _: - raise ValueError(f"Invalid section: {section}") - -def get_endpoint(url: str) -> str: - """Get endpoints from the configuration.""" - api_config = get_section_config("api") - return api_config.get(f"{url}") - -def get_base_url() -> str: - """Get the base URL from the configuration.""" - return get_endpoint("base_url") - -def get_api_key() -> str: - """Get the API key from the .env file.""" - return os.getenv("FMP_API_KEY", "") \ No newline at end of file From 734935e3a58f6ac32d16e02cc2b17143ff9223de Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Sun, 17 Nov 2024 15:35:06 -0700 Subject: [PATCH 04/19] Updated fmp_client.py file --- .gitignore | 1 + pyproject.toml | 4 + src/{stock_valuation_app => }/config.yml | 6 +- src/main.py | 13 ++++ src/stock_valuation_app/__init__.py | 9 --- src/stock_valuation_app/api/__init__.py | 0 src/stock_valuation_app/api/routes.py | 2 + src/stock_valuation_app/data/fmp_client.py | 68 +++++++++++++---- src/stock_valuation_app/main.py | 60 --------------- src/stock_valuation_app/models/__init__.py | 0 src/stock_valuation_app/models/stock.py | 87 ++++++++++++++++++++++ src/utils.py | 31 ++++++++ uv.lock | 11 +++ 13 files changed, 204 insertions(+), 88 deletions(-) rename src/{stock_valuation_app => }/config.yml (81%) create mode 100644 src/main.py create mode 100644 src/stock_valuation_app/api/__init__.py create mode 100644 src/stock_valuation_app/api/routes.py delete mode 100644 src/stock_valuation_app/main.py create mode 100644 src/stock_valuation_app/models/__init__.py create mode 100644 src/stock_valuation_app/models/stock.py create mode 100644 src/utils.py diff --git a/.gitignore b/.gitignore index de64abe..01a9328 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Python-generated files __pycache__/ +.mypy_cache *.py[cod] *$py.class *.so diff --git a/pyproject.toml b/pyproject.toml index 868d288..2a98262 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ authors = [ ] requires-python = ">=3.12" dependencies = [ + "asyncio>=3.4.3", "dash>=2.18.2", "duckdb>=1.1.3", "fastapi>=0.115.4", @@ -33,3 +34,6 @@ dev = [ "pytest>=8.3.3", "ruff>=0.7.3", ] + +[tool.setuptools] +package-dir = {"" = "src"} diff --git a/src/stock_valuation_app/config.yml b/src/config.yml similarity index 81% rename from src/stock_valuation_app/config.yml rename to src/config.yml index 5d25ea5..2e7db70 100644 --- a/src/stock_valuation_app/config.yml +++ b/src/config.yml @@ -1,8 +1,8 @@ api: base_url: "https://financialmodelingprep.com/api/v3" - annual_ratios: "ratios/" - annual_financial_growth: "financial-growth/" - rating: "rating/" + annual_ratios: "ratios" + annual_financial_growth: "financial-growth" + rating: "rating" valuation: default_pe_ratio: 15 diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..a4a1eea --- /dev/null +++ b/src/main.py @@ -0,0 +1,13 @@ +import asyncio +from stock_valuation_app.data import fmp_client + +async def main() -> None: + """Main function to fetch data from the Financial Modeling Prep API.""" + api_client = fmp_client.FMPClient() + rating_endpoint = fmp_client.get_endpoint("annual_ratios") + data = await api_client.fetch_data(rating_endpoint, "AAPL") + print(data) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/stock_valuation_app/__init__.py b/src/stock_valuation_app/__init__.py index c346459..e69de29 100644 --- a/src/stock_valuation_app/__init__.py +++ b/src/stock_valuation_app/__init__.py @@ -1,9 +0,0 @@ -from fastapi import FastAPI -from .api.routes import router -from .ui.dashboard import app as dash_app - -app = FastAPI() -app.include_router(router) - -# Mount Dash app -app.mount("/dashboard", dash_app.server) diff --git a/src/stock_valuation_app/api/__init__.py b/src/stock_valuation_app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/stock_valuation_app/api/routes.py b/src/stock_valuation_app/api/routes.py new file mode 100644 index 0000000..dc7215d --- /dev/null +++ b/src/stock_valuation_app/api/routes.py @@ -0,0 +1,2 @@ +from fastapi import APIRouter, Depends +from stock_valuation_app.data.fmp_client import FMPClient \ No newline at end of file diff --git a/src/stock_valuation_app/data/fmp_client.py b/src/stock_valuation_app/data/fmp_client.py index c516de0..ba2d8d1 100644 --- a/src/stock_valuation_app/data/fmp_client.py +++ b/src/stock_valuation_app/data/fmp_client.py @@ -1,27 +1,63 @@ +import os +import asyncio from dataclasses import dataclass, field -from urllib.parse import urljoin +from typing import Any import httpx -from typing import Any, Optional -from pydantic import BaseModel +import utils +from stock_valuation_app.models.stock import CombinedModel + + +def get_endpoint(url: str) -> str: + """Get endpoints from the configuration.""" + api_config = utils.get_section_config("api") + return api_config.get(f"{url}") + + +def get_base_url() -> str: + """Get the base URL from the configuration.""" + return get_endpoint("base_url") + + +def get_api_key() -> str: + """Get the API key from the .env file.""" + return os.getenv("FMP_API_KEY", "") @dataclass class FMPClient: """A client for interacting with the Financial Modeling Prep API.""" - base_url: str = field(default_factory=utils.get_base_url) - api_key: str = field(default_factory=utils.get_api_key) + base_url: str = field(default_factory=get_base_url) + api_key: str = field(default_factory=get_api_key) + metric_types: list[str] = field(default_factory=lambda: ["profile", "rating", "ratios", "key-metrics", "financial-growth",]) + async def get_data(self, client: httpx.Client, url: str) -> dict[str, Any]: + """Call API endpoint asynchronously""" + response = await client.get(url) + data = response.json() + return data - async def fetch_data(self, endpoint: str, symbol: str, period: Optional[str] = "annual"): # params: dict[str, Any] - """Fetch data from the Financial Modeling Prep API.""" - base_endpoint = urljoin(self.base_url, endpoint) - if period is None: - url = f"{base_endpoint}/{symbol}?apikey={self.api_key}" - else: - url = f"{base_endpoint}/{symbol}?period={period}&apikey={self.api_key}" + async def fetch_data(self, ticker: str) -> dict[str, list[dict[str, Any]]]: + urls = [] + for metric in self.metric_types: + if metric in ["profile", "rating"]: + endpoint = f"{self.base_url}/{metric}/{ticker}?apikey={self.api_key}" + else: + endpoint = f"{self.base_url}/{metric}/{ticker}?period=annual&apikey={self.api_key}" + urls.append(endpoint) async with httpx.AsyncClient() as client: - response = await client.get(url) - response.raise_for_status() - data = response.json() - return data \ No newline at end of file + tasks = [] + for url in urls: + tasks.append(asyncio.create_task(self.get_data(client, url))) + + results = await asyncio.gather(*tasks) + + # Rename some metric types to match with fields defined in the CombinedModel + replace_metric_types = {"key-metrics": "key_metrics", "financial-growth": "growth",} + new_metric_types = [replace_metric_types.get(item, item) for item in self.metric_types] + + # Create combined records dict for validation + records = dict(zip(new_metric_types, results)) + + # Validate the combined records + return CombinedModel(**records).model_dump() diff --git a/src/stock_valuation_app/main.py b/src/stock_valuation_app/main.py deleted file mode 100644 index 73a77ab..0000000 --- a/src/stock_valuation_app/main.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import yaml -from dotenv import load_dotenv -from stock_valuation_app.data.fmp_client import FMPClient - - -# Load environment variables from .env file -load_dotenv() - - -def load_config(): - """Load configuration from a YAML file.""" - with open("config.yml", "r", encoding="utf-8") as file: - config = yaml.safe_load(file) - return config - - -def get_section_config(section: str): - """Get configuration for a specific section.""" - match section: - case "api": - return load_config().get("api") - case "valuation": - return load_config().get("valuation") - case "dashboard": - return load_config().get("dashboard") - case "database": - return load_config().get("database") - case "logging": - return load_config().get("logging") - case _: - raise ValueError(f"Invalid section: {section}") - - -def get_endpoint(url: str) -> str: - """Get endpoints from the configuration.""" - api_config = get_section_config("api") - return api_config.get(f"{url}") - - -def get_base_url() -> str: - """Get the base URL from the configuration.""" - return get_endpoint("base_url") - - -def get_api_key() -> str: - """Get the API key from the .env file.""" - return os.getenv("FMP_API_KEY", "") - - -def main() -> None: - """Main function to fetch data from the Financial Modeling Prep API.""" - api_client = FMPClient() - rating_endpoint = get_endpoint("rating") - data = api_client.fetch_data(rating_endpoint, "AAPL") - print(data) - - -if __name__ == "__main__": - main() diff --git a/src/stock_valuation_app/models/__init__.py b/src/stock_valuation_app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/stock_valuation_app/models/stock.py b/src/stock_valuation_app/models/stock.py new file mode 100644 index 0000000..ae852de --- /dev/null +++ b/src/stock_valuation_app/models/stock.py @@ -0,0 +1,87 @@ +from typing import Optional +from pydantic import BaseModel, Field + + +class CompanyProfile(BaseModel): + symbol: str + company_name: str = Field(...,alias="companyName") + sector: Optional[str] = None + industry: Optional[str] = None + description: Optional[str] = None + + +class Rating(BaseModel): + #symbol: str + date: str + rating: str + score: int = Field(..., alias="ratingScore") + recommendation: str = Field(..., alias="ratingRecommendation") + dcf_score: int = Field(..., alias="ratingDetailsDCFScore") + dcf_rec: str = Field(..., alias="ratingDetailsDCFRecommendation") + roe_score: int = Field(..., alias="ratingDetailsROEScore") + roe_rec: str = Field(..., alias="ratingDetailsROERecommendation") + roa_score: int = Field(..., alias="ratingDetailsROAScore") + roa_rec: str = Field(..., alias="ratingDetailsROARecommendation") + de_score: int = Field(..., alias="ratingDetailsDEScore") + de_rec: str = Field(..., alias="ratingDetailsDERecommendation") + pe_score: int = Field(..., alias="ratingDetailsPEScore") + pe_rec: str = Field(..., alias="ratingDetailsPERecommendation") + pb_score: int = Field(..., alias="ratingDetailsPBScore") + pb_rec: str = Field(..., alias="ratingDetailsPBRecommendation") + + +class Ratios(BaseModel): + #symbol: str + year: str = Field(..., alias="calendarYear") + de_ratio: float = Field(..., alias="debtEquityRatio") + fcf_ps: float = Field(..., alias="freeCashFlowPerShare") + pb_ratio: float = Field(..., alias="priceToBookRatio") + ps_ratio: float = Field(..., alias="priceToSalesRatio") + pe_ratio: float = Field(..., alias="priceEarningsRatio") + p_fcf_ratio: float = Field(..., alias="priceToFreeCashFlowsRatio") + peg_ratio: float = Field(..., alias="priceEarningsToGrowthRatio") + div_yield: int = Field(..., alias="dividendYield") + curr_ratio: float = Field(..., alias="currentRatio") + + +class KeyMetrics(BaseModel): + #symbol: str + year: str = Field(..., alias="calendarYear") + rev_per_share: float = Field(..., alias="revenuePerShare") + net_income_per_share: float = Field(..., alias="netIncomePerShare") + op_cf_per_share: float = Field(..., alias="operatingCashFlowPerShare") + fcf_per_share: float = Field(..., alias="freeCashFlowPerShare") + book_val_per_share: float = Field(..., alias="bookValuePerShare") + ev_over_ebitda: float = Field(..., alias="enterpriseValueOverEBITDA") + fcf_yield: float = Field(..., alias="freeCashFlowYield") + int_coverage: float = Field(..., alias="interestCoverage") + roic: float + + +class Growth(BaseModel): + #symbol: str + year: str = Field(..., alias="calendarYear") + rev_growth: float = Field(..., alias="revenueGrowth") + net_inc_growth: float = Field(..., alias="netIncomeGrowth") + eps_growth: float = Field(..., alias="epsdilutedGrowth") + dps_growth: float = Field(..., alias="dividendsperShareGrowth") + fcf_growth: float = Field(..., alias="freeCashFlowGrowth") + rev_growth_10y: float = Field(..., alias="tenYRevenueGrowthPerShare") + rev_growth_5y: float = Field(..., alias="fiveYRevenueGrowthPerShare") + rev_growth_3y: float = Field(..., alias="threeYRevenueGrowthPerShare") + net_inc_growth_10y: float = Field(..., alias="tenYNetIncomeGrowthPerShare") + net_inc_growth_5y: float = Field(..., alias="fiveYNetIncomeGrowthPerShare") + net_inc_growth_3y: float = Field(..., alias="threeYNetIncomeGrowthPerShare") + dps_growth_10y: float = Field(..., alias="tenYDividendperShareGrowthPerShare") + dps_growth_5y: float = Field(..., alias="fiveYDividendperShareGrowthPerShare") + dps_growth_3y: float = Field(..., alias="threeYDividendperShareGrowthPerShare") + bvps_growth: float = Field(..., alias="bookValueperShareGrowth") + debt_growth: float = Field(..., alias="debtGrowth") + + +class CombinedModel(BaseModel): + profile: list[CompanyProfile] + rating: list[Rating] + key_metrics: list[KeyMetrics] + ratios: list[Ratios] + growth: list[Growth] \ No newline at end of file diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..9294b09 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,31 @@ +from pathlib import Path + +import yaml +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + + +def load_config(): + """Load configuration from a YAML file.""" + with open(Path("src/config.yml").absolute(), "r", encoding="utf-8") as file: + config = yaml.safe_load(file) + return config + + +def get_section_config(section: str): + """Get configuration for a specific section.""" + match section: + case "api": + return load_config().get("api") + case "valuation": + return load_config().get("valuation") + case "dashboard": + return load_config().get("dashboard") + case "database": + return load_config().get("database") + case "logging": + return load_config().get("logging") + case _: + raise ValueError(f"Invalid section: {section}") diff --git a/uv.lock b/uv.lock index aebb64c..c8b46d3 100644 --- a/uv.lock +++ b/uv.lock @@ -48,6 +48,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764 }, ] +[[package]] +name = "asyncio" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/54/054bafaf2c0fb8473d423743e191fcdf49b2c1fd5e9af3524efbe097bafd/asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", size = 204411 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767 }, +] + [[package]] name = "blinker" version = "1.9.0" @@ -1019,6 +1028,7 @@ name = "stock-valuation-app" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "asyncio" }, { name = "dash" }, { name = "duckdb" }, { name = "fastapi" }, @@ -1040,6 +1050,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "asyncio", specifier = ">=3.4.3" }, { name = "dash", specifier = ">=2.18.2" }, { name = "duckdb", specifier = ">=1.1.3" }, { name = "fastapi", specifier = ">=0.115.4" }, From b4d13eae2bd4a124aa25e53badbec77631a05c6a Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Sun, 17 Nov 2024 16:10:48 -0700 Subject: [PATCH 05/19] Started work on valuation.py file --- src/stock_valuation_app/services/valuation.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/stock_valuation_app/services/valuation.py diff --git a/src/stock_valuation_app/services/valuation.py b/src/stock_valuation_app/services/valuation.py new file mode 100644 index 0000000..5d6df7c --- /dev/null +++ b/src/stock_valuation_app/services/valuation.py @@ -0,0 +1,22 @@ +import asyncio +import polars as pl +from stock_valuation_app.data.fmp_client import FMPClient + + + +async def extract_source_data(ticker: str): + source_data = await FMPClient().fetch_data(ticker) + return source_data + +if __name__ == "__main__": + raw_data = asyncio.run(extract_source_data('PAYS')) + + profile_df = pl.DataFrame(raw_data["profile"]) + rating_df = pl.DataFrame(raw_data["rating"]) + metric_df = pl.DataFrame(raw_data["key_metrics"]) + growth_df = pl.DataFrame(raw_data["growth"]) + + print(profile_df.head()) + print(rating_df.head()) + print(metric_df.head()) + print(growth_df.head()) \ No newline at end of file From 24dab154cc1efba5bac334e49aeecb02dc2d971f Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Sun, 17 Nov 2024 16:14:59 -0700 Subject: [PATCH 06/19] Modified Stock.py file. --- src/stock_valuation_app/models/stock.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stock_valuation_app/models/stock.py b/src/stock_valuation_app/models/stock.py index ae852de..bbb148f 100644 --- a/src/stock_valuation_app/models/stock.py +++ b/src/stock_valuation_app/models/stock.py @@ -11,7 +11,7 @@ class CompanyProfile(BaseModel): class Rating(BaseModel): - #symbol: str + symbol: str date: str rating: str score: int = Field(..., alias="ratingScore") @@ -31,7 +31,7 @@ class Rating(BaseModel): class Ratios(BaseModel): - #symbol: str + symbol: str year: str = Field(..., alias="calendarYear") de_ratio: float = Field(..., alias="debtEquityRatio") fcf_ps: float = Field(..., alias="freeCashFlowPerShare") @@ -45,7 +45,7 @@ class Ratios(BaseModel): class KeyMetrics(BaseModel): - #symbol: str + symbol: str year: str = Field(..., alias="calendarYear") rev_per_share: float = Field(..., alias="revenuePerShare") net_income_per_share: float = Field(..., alias="netIncomePerShare") @@ -59,7 +59,7 @@ class KeyMetrics(BaseModel): class Growth(BaseModel): - #symbol: str + symbol: str year: str = Field(..., alias="calendarYear") rev_growth: float = Field(..., alias="revenueGrowth") net_inc_growth: float = Field(..., alias="netIncomeGrowth") From 1daacd4857be717907cb1d7bae64cf885182c6e6 Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Wed, 20 Nov 2024 23:44:28 -0700 Subject: [PATCH 07/19] Modified project structure --- .gitignore | 1 + pyproject.toml | 3 +- src/{stock_valuation_app => api}/__init__.py | 0 .../data => api}/fmp_client.py | 5 +- src/main.py | 13 - .../api => models}/__init__.py | 0 .../models/stock.py => models/stock_data.py} | 0 .../data => services}/__init__.py | 0 src/services/stock_analysis.py | 50 ++ src/stock_valuation_app/api/routes.py | 2 - src/stock_valuation_app/services/valuation.py | 22 - .../models => ui}/__init__.py | 0 src/ui/app.py | 44 ++ uv.lock | 515 +++++++++++++----- 14 files changed, 492 insertions(+), 163 deletions(-) rename src/{stock_valuation_app => api}/__init__.py (100%) rename src/{stock_valuation_app/data => api}/fmp_client.py (92%) rename src/{stock_valuation_app/api => models}/__init__.py (100%) rename src/{stock_valuation_app/models/stock.py => models/stock_data.py} (100%) rename src/{stock_valuation_app/data => services}/__init__.py (100%) create mode 100644 src/services/stock_analysis.py delete mode 100644 src/stock_valuation_app/api/routes.py delete mode 100644 src/stock_valuation_app/services/valuation.py rename src/{stock_valuation_app/models => ui}/__init__.py (100%) create mode 100644 src/ui/app.py diff --git a/.gitignore b/.gitignore index 01a9328..596ccf0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Python-generated files __pycache__/ .mypy_cache +.ruff_cache *.py[cod] *$py.class *.so diff --git a/pyproject.toml b/pyproject.toml index 2a98262..31c1eae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ authors = [ requires-python = ">=3.12" dependencies = [ "asyncio>=3.4.3", - "dash>=2.18.2", "duckdb>=1.1.3", "fastapi>=0.115.4", "httpx>=0.27.2", @@ -18,6 +17,8 @@ dependencies = [ "pydantic>=2.9.2", "python-dotenv>=1.0.1", "pyyaml>=6.0.2", + "streamlit>=1.40.1", + "uvicorn>=0.32.0", ] [project.scripts] diff --git a/src/stock_valuation_app/__init__.py b/src/api/__init__.py similarity index 100% rename from src/stock_valuation_app/__init__.py rename to src/api/__init__.py diff --git a/src/stock_valuation_app/data/fmp_client.py b/src/api/fmp_client.py similarity index 92% rename from src/stock_valuation_app/data/fmp_client.py rename to src/api/fmp_client.py index ba2d8d1..9e85c89 100644 --- a/src/stock_valuation_app/data/fmp_client.py +++ b/src/api/fmp_client.py @@ -4,7 +4,7 @@ from typing import Any import httpx import utils -from stock_valuation_app.models.stock import CombinedModel +from models.stock_data import CombinedModel def get_endpoint(url: str) -> str: @@ -37,6 +37,7 @@ async def get_data(self, client: httpx.Client, url: str) -> dict[str, Any]: return data async def fetch_data(self, ticker: str) -> dict[str, list[dict[str, Any]]]: + """Extracts data asynchronously from multiple FMP endpoints""" urls = [] for metric in self.metric_types: if metric in ["profile", "rating"]: @@ -60,4 +61,4 @@ async def fetch_data(self, ticker: str) -> dict[str, list[dict[str, Any]]]: records = dict(zip(new_metric_types, results)) # Validate the combined records - return CombinedModel(**records).model_dump() + return CombinedModel(**records).model_dump() \ No newline at end of file diff --git a/src/main.py b/src/main.py index a4a1eea..e69de29 100644 --- a/src/main.py +++ b/src/main.py @@ -1,13 +0,0 @@ -import asyncio -from stock_valuation_app.data import fmp_client - -async def main() -> None: - """Main function to fetch data from the Financial Modeling Prep API.""" - api_client = fmp_client.FMPClient() - rating_endpoint = fmp_client.get_endpoint("annual_ratios") - data = await api_client.fetch_data(rating_endpoint, "AAPL") - print(data) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/src/stock_valuation_app/api/__init__.py b/src/models/__init__.py similarity index 100% rename from src/stock_valuation_app/api/__init__.py rename to src/models/__init__.py diff --git a/src/stock_valuation_app/models/stock.py b/src/models/stock_data.py similarity index 100% rename from src/stock_valuation_app/models/stock.py rename to src/models/stock_data.py diff --git a/src/stock_valuation_app/data/__init__.py b/src/services/__init__.py similarity index 100% rename from src/stock_valuation_app/data/__init__.py rename to src/services/__init__.py diff --git a/src/services/stock_analysis.py b/src/services/stock_analysis.py new file mode 100644 index 0000000..baa3647 --- /dev/null +++ b/src/services/stock_analysis.py @@ -0,0 +1,50 @@ +import asyncio +import polars as pl +# from stock_valuation_app.data.fmp_client import FMPClient + + + + +raw_data = asyncio.run(extract_source_data("PAYS")) + +profile_df = pl.DataFrame(raw_data["profile"]) +rating_df = pl.DataFrame(raw_data["rating"]) +metric_df = pl.DataFrame(raw_data["key_metrics"]) +growth_df = pl.DataFrame(raw_data["growth"]) + +# Pivot the DataFrame +pivoted_df = profile_df.unpivot(variable_name="Column", value_name="Value") +# Sort the result to maintain the original order +pivoted_df = pivoted_df.sort("Column") + +# Convert DataFrame to formatted text +formatted_text = [] +for row in pivoted_df.iter_rows(): + column, value = row + formatted_text.append(html.Div([html.Strong(f"{column}: "), f"{value}"])) + +# async def extract_source_data(ticker: str): +# source_data = await FMPClient().fetch_data(ticker) +# return { +# "profile": source_data["profile"], +# "rating": source_data["rating"], +# "key_metrics": source_data["key_metrics"], +# "growth": source_data["growth"], +# } + +# async def extract_source_data(ticker: str): +# source_data = await FMPClient().fetch_data(ticker) +# return source_data + + +# if __name__ == "__main__": +# pass + # raw_data = asyncio.run(extract_source_data('PAYS')) + + # def get_dataframes(): + # """Captures source dataframes""" + # profile_df = pl.DataFrame(raw_data["profile"]) + # rating_df = pl.DataFrame(raw_data["rating"]) + # metric_df = pl.DataFrame(raw_data["key_metrics"]) + # growth_df = pl.DataFrame(raw_data["growth"]) + # return (profile_df, rating_df, metric_df, growth_df) diff --git a/src/stock_valuation_app/api/routes.py b/src/stock_valuation_app/api/routes.py deleted file mode 100644 index dc7215d..0000000 --- a/src/stock_valuation_app/api/routes.py +++ /dev/null @@ -1,2 +0,0 @@ -from fastapi import APIRouter, Depends -from stock_valuation_app.data.fmp_client import FMPClient \ No newline at end of file diff --git a/src/stock_valuation_app/services/valuation.py b/src/stock_valuation_app/services/valuation.py deleted file mode 100644 index 5d6df7c..0000000 --- a/src/stock_valuation_app/services/valuation.py +++ /dev/null @@ -1,22 +0,0 @@ -import asyncio -import polars as pl -from stock_valuation_app.data.fmp_client import FMPClient - - - -async def extract_source_data(ticker: str): - source_data = await FMPClient().fetch_data(ticker) - return source_data - -if __name__ == "__main__": - raw_data = asyncio.run(extract_source_data('PAYS')) - - profile_df = pl.DataFrame(raw_data["profile"]) - rating_df = pl.DataFrame(raw_data["rating"]) - metric_df = pl.DataFrame(raw_data["key_metrics"]) - growth_df = pl.DataFrame(raw_data["growth"]) - - print(profile_df.head()) - print(rating_df.head()) - print(metric_df.head()) - print(growth_df.head()) \ No newline at end of file diff --git a/src/stock_valuation_app/models/__init__.py b/src/ui/__init__.py similarity index 100% rename from src/stock_valuation_app/models/__init__.py rename to src/ui/__init__.py diff --git a/src/ui/app.py b/src/ui/app.py new file mode 100644 index 0000000..f40c26a --- /dev/null +++ b/src/ui/app.py @@ -0,0 +1,44 @@ +import asyncio +import streamlit as st +from api.fmp_client import FMPClient +# from services.stock_analysis import ( +# analyze_stock, +# is_quality_dividend_growth_stock, +# is_undervalued, +# ) + +async def main(): + #st.title("Stock Valuation App") + st.title("Stock Valuation Dashboard") + st.subheader("Get stock quality and valuation insights from historical financial data.", divider="gray") + + #api_key = st.secrets["FMP_API_KEY"] + symbol = st.text_input("Enter stock symbol:") + + if st.button("Analyze"): + # Get stock symbol as user input + if symbol: + data = await FMPClient().fetch_data(symbol) + + if data: + analysis = analyze_stock(data) + + st.subheader("Growth Rates") + for metric, value in analysis.items(): + st.metric(metric, f"{value:.2%}") + + quality_stock = is_quality_dividend_growth_stock(analysis) + st.subheader("Quality Dividend Growth Stock") + st.write("Yes" if quality_stock else "No") + + current_price = st.number_input("Enter current stock price:") + if current_price: + undervalued = is_undervalued(current_price, analysis) + st.subheader("Stock Valuation") + st.write("Undervalued" if undervalued else "Overvalued") + else: + st.warning("Please enter a valid stock symbol") + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/uv.lock b/uv.lock index c8b46d3..8170bb6 100644 --- a/uv.lock +++ b/uv.lock @@ -5,6 +5,22 @@ resolution-markers = [ "python_full_version >= '3.13'", ] +[[package]] +name = "altair" +version = "5.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/09/38904138a49f29e529b61b4f39954a6837f443d828c1bc57814be7bd4813/altair-5.4.1.tar.gz", hash = "sha256:0ce8c2e66546cb327e5f2d7572ec0e7c6feece816203215613962f0ec1d76a82", size = 636465 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/52/4a86a4fa1cc2aae79137cc9510b7080c3e5aede2310d14fae5486feec7f7/altair-5.4.1-py3-none-any.whl", hash = "sha256:0fb130b8297a569d08991fb6fe763582e7569f8a04643bbd9212436e3be04aef", size = 658150 }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -57,6 +73,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767 }, ] +[[package]] +name = "attrs" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, +] + [[package]] name = "blinker" version = "1.9.0" @@ -66,6 +91,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, ] +[[package]] +name = "cachetools" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/38/a0f315319737ecf45b4319a8cd1f3a908e29d9277b46942263292115eee7/cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a", size = 27661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524 }, +] + [[package]] name = "certifi" version = "2024.8.30" @@ -180,56 +214,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180 }, ] -[[package]] -name = "dash" -version = "2.18.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dash-core-components" }, - { name = "dash-html-components" }, - { name = "dash-table" }, - { name = "flask" }, - { name = "importlib-metadata" }, - { name = "nest-asyncio" }, - { name = "plotly" }, - { name = "requests" }, - { name = "retrying" }, - { name = "setuptools" }, - { name = "typing-extensions" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/ae/dfd8c42c83cad1b903e4e3e7be7042074d5d7d16be97eaede6656b8ead95/dash-2.18.2.tar.gz", hash = "sha256:20e8404f73d0fe88ce2eae33c25bbc513cbe52f30d23a401fa5f24dbb44296c8", size = 7457235 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/ef/d46131f4817f18b329e4fb7c53ba1d31774239d91266a74bccdc932708cc/dash-2.18.2-py3-none-any.whl", hash = "sha256:0ce0479d1bc958e934630e2de7023b8a4558f23ce1f9f5a4b34b65eb3903a869", size = 7792658 }, -] - -[[package]] -name = "dash-core-components" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/55/ad4a2cf9b7d4134779bd8d3a7e5b5f8cc757f421809e07c3e73bb374fdd7/dash_core_components-2.0.0.tar.gz", hash = "sha256:c6733874af975e552f95a1398a16c2ee7df14ce43fa60bb3718a3c6e0b63ffee", size = 3427 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/9e/a29f726e84e531a36d56cff187e61d8c96d2cc253c5bcef9a7695acb7e6a/dash_core_components-2.0.0-py3-none-any.whl", hash = "sha256:52b8e8cce13b18d0802ee3acbc5e888cb1248a04968f962d63d070400af2e346", size = 3822 }, -] - -[[package]] -name = "dash-html-components" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/c6/957d5e83b620473eb3c8557a253fb01c6a817b10ca43d3ff9d31796f32a6/dash_html_components-2.0.0.tar.gz", hash = "sha256:8703a601080f02619a6390998e0b3da4a5daabe97a1fd7a9cebc09d015f26e50", size = 3840 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/65/1b16b853844ef59b2742a7de74a598f376ac0ab581f0dcc34db294e5c90e/dash_html_components-2.0.0-py3-none-any.whl", hash = "sha256:b42cc903713c9706af03b3f2548bda4be7307a7cf89b7d6eae3da872717d1b63", size = 4092 }, -] - -[[package]] -name = "dash-table" -version = "5.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/81/34983fa0c67125d7fff9d55e5d1a065127bde7ca49ca32d04dedd55f9f35/dash_table-5.0.0.tar.gz", hash = "sha256:18624d693d4c8ef2ddec99a6f167593437a7ea0bf153aa20f318c170c5bc7308", size = 3391 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/ce/43f77dc8e7bbad02a9f88d07bf794eaf68359df756a28bb9f2f78e255bb1/dash_table-5.0.0-py3-none-any.whl", hash = "sha256:19036fa352bb1c11baf38068ec62d172f0515f73ca3276c79dee49b95ddc16c9", size = 3912 }, -] - [[package]] name = "debugpy" version = "1.8.8" @@ -304,19 +288,27 @@ wheels = [ ] [[package]] -name = "flask" -version = "3.0.3" +name = "gitdb" +version = "4.0.11" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "blinker" }, - { name = "click" }, - { name = "itsdangerous" }, - { name = "jinja2" }, - { name = "werkzeug" }, + { name = "smmap" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/e1/d104c83026f8d35dfd2c261df7d64738341067526406b40190bc063e829a/flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842", size = 676315 } +sdist = { url = "https://files.pythonhosted.org/packages/19/0d/bbb5b5ee188dec84647a4664f3e11b06ade2bde568dbd489d9d64adef8ed/gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b", size = 394469 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", size = 101735 }, + { url = "https://files.pythonhosted.org/packages/fd/5b/8f0c4a5bb9fd491c277c21eff7ccae71b47d43c4446c9d0c6cff2fe8c2c4/gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", size = 62721 }, +] + +[[package]] +name = "gitpython" +version = "3.1.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/a1/106fd9fa2dd989b6fb36e5893961f82992cf676381707253e0bf93eb1662/GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c", size = 214149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/bd/cc3a402a6439c15c3d4294333e13042b915bbeab54edc457c723931fed3f/GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff", size = 207337 }, ] [[package]] @@ -366,18 +358,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] -[[package]] -name = "importlib-metadata" -version = "8.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, -] - [[package]] name = "iniconfig" version = "2.0.0" @@ -431,15 +411,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/a5/c15ed187f1b3fac445bb42a2dedd8dec1eee1718b35129242049a13a962f/ipython-8.29.0-py3-none-any.whl", hash = "sha256:0188a1bd83267192123ccea7f4a8ed0a78910535dbaa3f37671dca76ebd429c8", size = 819911 }, ] -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, -] - [[package]] name = "jedi" version = "0.19.2" @@ -464,6 +435,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, ] +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, +] + [[package]] name = "jupyter-client" version = "8.6.3" @@ -494,6 +492,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", size = 28965 }, ] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + [[package]] name = "markupsafe" version = "3.0.2" @@ -544,6 +554,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + [[package]] name = "mypy" version = "1.13.0" @@ -576,6 +595,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] +[[package]] +name = "narwhals" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/1c/da74c6c96ffc7cdbb40b9b53efa8f97f2b257a430842d220acbcea60f2f4/narwhals-1.14.1.tar.gz", hash = "sha256:8262d77afec11960852e3f24d250e882575d9115ce7076df20b7c3bdce1281eb", size = 186792 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/38/63dcb45e12f6e5fcdb7b05d4c3b884502c50613b56c0e6fec78803cf14a7/narwhals-1.14.1-py3-none-any.whl", hash = "sha256:b737db277df174ca41b45950e50f48a738c88bd9b896398ffa8872e4e3930def", size = 220586 }, +] + [[package]] name = "nest-asyncio" version = "1.6.0" @@ -585,6 +613,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, ] +[[package]] +name = "numpy" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658 }, + { url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258 }, + { url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249 }, + { url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704 }, + { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089 }, + { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185 }, + { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751 }, + { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705 }, + { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077 }, + { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858 }, + { url = "https://files.pythonhosted.org/packages/4d/0b/620591441457e25f3404c8057eb924d04f161244cb8a3680d529419aa86e/numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", size = 20836263 }, + { url = "https://files.pythonhosted.org/packages/45/e1/210b2d8b31ce9119145433e6ea78046e30771de3fe353f313b2778142f34/numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", size = 13507771 }, + { url = "https://files.pythonhosted.org/packages/55/44/aa9ee3caee02fa5a45f2c3b95cafe59c44e4b278fbbf895a93e88b308555/numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", size = 5075805 }, + { url = "https://files.pythonhosted.org/packages/78/d6/61de6e7e31915ba4d87bbe1ae859e83e6582ea14c6add07c8f7eefd8488f/numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", size = 6608380 }, + { url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451 }, + { url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822 }, + { url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822 }, + { url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598 }, + { url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021 }, + { url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405 }, + { url = "https://files.pythonhosted.org/packages/a7/45/7f9244cd792e163b334e3a7f02dff1239d2890b6f37ebf9e82cbe17debc0/numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", size = 20859062 }, + { url = "https://files.pythonhosted.org/packages/b1/b4/a084218e7e92b506d634105b13e27a3a6645312b93e1c699cc9025adb0e1/numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", size = 13515839 }, + { url = "https://files.pythonhosted.org/packages/27/45/58ed3f88028dcf80e6ea580311dc3edefdd94248f5770deb980500ef85dd/numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", size = 5116031 }, + { url = "https://files.pythonhosted.org/packages/37/a8/eb689432eb977d83229094b58b0f53249d2209742f7de529c49d61a124a0/numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", size = 6629977 }, + { url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951 }, + { url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655 }, + { url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902 }, + { url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180 }, + { url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907 }, + { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098 }, +] + [[package]] name = "packaging" version = "24.2" @@ -594,6 +660,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, +] + [[package]] name = "parso" version = "0.8.4" @@ -615,6 +715,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, ] +[[package]] +name = "pillow" +version = "11.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642 }, + { url = "https://files.pythonhosted.org/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999 }, + { url = "https://files.pythonhosted.org/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794 }, + { url = "https://files.pythonhosted.org/packages/9d/21/84c9f287d17180f26263b5f5c8fb201de0f88b1afddf8a2597a5c9fe787f/pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", size = 4300762 }, + { url = "https://files.pythonhosted.org/packages/84/39/63fb87cd07cc541438b448b1fed467c4d687ad18aa786a7f8e67b255d1aa/pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9", size = 4210468 }, + { url = "https://files.pythonhosted.org/packages/7f/42/6e0f2c2d5c60f499aa29be14f860dd4539de322cd8fb84ee01553493fb4d/pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", size = 4381824 }, + { url = "https://files.pythonhosted.org/packages/31/69/1ef0fb9d2f8d2d114db982b78ca4eeb9db9a29f7477821e160b8c1253f67/pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", size = 4296436 }, + { url = "https://files.pythonhosted.org/packages/44/ea/dad2818c675c44f6012289a7c4f46068c548768bc6c7f4e8c4ae5bbbc811/pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", size = 4429714 }, + { url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631 }, + { url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533 }, + { url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890 }, + { url = "https://files.pythonhosted.org/packages/63/24/e2e15e392d00fcf4215907465d8ec2a2f23bcec1481a8ebe4ae760459995/pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", size = 3147300 }, + { url = "https://files.pythonhosted.org/packages/43/72/92ad4afaa2afc233dc44184adff289c2e77e8cd916b3ddb72ac69495bda3/pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", size = 2978742 }, + { url = "https://files.pythonhosted.org/packages/9e/da/c8d69c5bc85d72a8523fe862f05ababdc52c0a755cfe3d362656bb86552b/pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", size = 4194349 }, + { url = "https://files.pythonhosted.org/packages/cd/e8/686d0caeed6b998351d57796496a70185376ed9c8ec7d99e1d19ad591fc6/pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", size = 4298714 }, + { url = "https://files.pythonhosted.org/packages/ec/da/430015cec620d622f06854be67fd2f6721f52fc17fca8ac34b32e2d60739/pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", size = 4208514 }, + { url = "https://files.pythonhosted.org/packages/44/ae/7e4f6662a9b1cb5f92b9cc9cab8321c381ffbee309210940e57432a4063a/pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", size = 4380055 }, + { url = "https://files.pythonhosted.org/packages/74/d5/1a807779ac8a0eeed57f2b92a3c32ea1b696e6140c15bd42eaf908a261cd/pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", size = 4296751 }, + { url = "https://files.pythonhosted.org/packages/38/8c/5fa3385163ee7080bc13026d59656267daaaaf3c728c233d530e2c2757c8/pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", size = 4430378 }, + { url = "https://files.pythonhosted.org/packages/ca/1d/ad9c14811133977ff87035bf426875b93097fb50af747793f013979facdb/pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", size = 2249588 }, + { url = "https://files.pythonhosted.org/packages/fb/01/3755ba287dac715e6afdb333cb1f6d69740a7475220b4637b5ce3d78cec2/pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", size = 2567509 }, + { url = "https://files.pythonhosted.org/packages/c0/98/2c7d727079b6be1aba82d195767d35fcc2d32204c7a5820f822df5330152/pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", size = 2254791 }, + { url = "https://files.pythonhosted.org/packages/eb/38/998b04cc6f474e78b563716b20eecf42a2fa16a84589d23c8898e64b0ffd/pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", size = 3150854 }, + { url = "https://files.pythonhosted.org/packages/13/8e/be23a96292113c6cb26b2aa3c8b3681ec62b44ed5c2bd0b258bd59503d3c/pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", size = 2982369 }, + { url = "https://files.pythonhosted.org/packages/97/8a/3db4eaabb7a2ae8203cd3a332a005e4aba00067fc514aaaf3e9721be31f1/pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", size = 4333703 }, + { url = "https://files.pythonhosted.org/packages/28/ac/629ffc84ff67b9228fe87a97272ab125bbd4dc462745f35f192d37b822f1/pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", size = 4412550 }, + { url = "https://files.pythonhosted.org/packages/d6/07/a505921d36bb2df6868806eaf56ef58699c16c388e378b0dcdb6e5b2fb36/pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", size = 4461038 }, + { url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197 }, + { url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169 }, + { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828 }, +] + [[package]] name = "platformdirs" version = "4.3.6" @@ -624,19 +762,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, ] -[[package]] -name = "plotly" -version = "5.24.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "tenacity" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/79/4f/428f6d959818d7425a94c190a6b26fbc58035cbef40bf249be0b62a9aedd/plotly-5.24.1.tar.gz", hash = "sha256:dbc8ac8339d248a4bcc36e08a5659bacfe1b079390b8953533f4eb22169b4bae", size = 9479398 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/ae/580600f441f6fc05218bd6c9d5794f4aef072a7d9093b291f1c50a9db8bc/plotly-5.24.1-py3-none-any.whl", hash = "sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089", size = 19054220 }, -] - [[package]] name = "pluggy" version = "1.5.0" @@ -671,6 +796,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 }, ] +[[package]] +name = "protobuf" +version = "5.28.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/6e/e69eb906fddcb38f8530a12f4b410699972ab7ced4e21524ece9d546ac27/protobuf-5.28.3.tar.gz", hash = "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b", size = 422479 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c5/05163fad52d7c43e124a545f1372d18266db36036377ad29de4271134a6a/protobuf-5.28.3-cp310-abi3-win32.whl", hash = "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24", size = 419624 }, + { url = "https://files.pythonhosted.org/packages/9c/4c/4563ebe001ff30dca9d7ed12e471fa098d9759712980cde1fd03a3a44fb7/protobuf-5.28.3-cp310-abi3-win_amd64.whl", hash = "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868", size = 431464 }, + { url = "https://files.pythonhosted.org/packages/1c/f2/baf397f3dd1d3e4af7e3f5a0382b868d25ac068eefe1ebde05132333436c/protobuf-5.28.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687", size = 414743 }, + { url = "https://files.pythonhosted.org/packages/85/50/cd61a358ba1601f40e7d38bcfba22e053f40ef2c50d55b55926aecc8fec7/protobuf-5.28.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584", size = 316511 }, + { url = "https://files.pythonhosted.org/packages/5d/ae/3257b09328c0b4e59535e497b0c7537d4954038bdd53a2f0d2f49d15a7c4/protobuf-5.28.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135", size = 316624 }, + { url = "https://files.pythonhosted.org/packages/ad/c3/2377c159e28ea89a91cf1ca223f827ae8deccb2c9c401e5ca233cd73002f/protobuf-5.28.3-py3-none-any.whl", hash = "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed", size = 169511 }, +] + [[package]] name = "psutil" version = "6.1.0" @@ -790,6 +929,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, ] +[[package]] +name = "pydeck" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403 }, +] + [[package]] name = "pygments" version = "2.18.0" @@ -835,6 +987,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, ] +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + [[package]] name = "pywin32" version = "308" @@ -918,6 +1079,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/d6/32fd69744afb53995619bc5effa2a405ae0d343cd3e747d0fbc43fe894ee/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0", size = 1392485 }, ] +[[package]] +name = "referencing" +version = "0.35.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/73ca1f8e72fff6fa52119dbd185f73a907b1989428917b24cff660129b6d/referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", size = 62991 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/59/2056f61236782a2c86b33906c025d4f4a0b17be0161b63b70fd9e8775d36/referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de", size = 26684 }, +] + [[package]] name = "requests" version = "2.32.3" @@ -934,15 +1108,50 @@ wheels = [ ] [[package]] -name = "retrying" -version = "1.3.4" +name = "rich" +version = "13.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, + { name = "markdown-it-py" }, + { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/70/15ce8551d65b324e18c5aa6ef6998880f21ead51ebe5ed743c0950d7d9dd/retrying-1.3.4.tar.gz", hash = "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e", size = 10929 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "rpds-py" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/80/afdf96daf9b27d61483ef05b38f282121db0e38f5fd4e89f40f5c86c2a4f/rpds_py-0.21.0.tar.gz", hash = "sha256:ed6378c9d66d0de903763e7706383d60c33829581f0adff47b6535f1802fa6db", size = 26335 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/04/9e36f28be4c0532c0e9207ff9dc01fb13a2b0eb036476a213b0000837d0e/retrying-1.3.4-py3-none-any.whl", hash = "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35", size = 11602 }, + { url = "https://files.pythonhosted.org/packages/d9/5a/3aa6f5d8bacbe4f55ebf9a3c9628dad40cdb57f845124cf13c78895ea156/rpds_py-0.21.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:30b912c965b2aa76ba5168fd610087bad7fcde47f0a8367ee8f1876086ee6d1d", size = 329516 }, + { url = "https://files.pythonhosted.org/packages/df/c0/67c8c8ac850c6e3681e356a59d46315bf73bc77cb50c9a32db8ae44325b7/rpds_py-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca9989d5d9b1b300bc18e1801c67b9f6d2c66b8fd9621b36072ed1df2c977f72", size = 321245 }, + { url = "https://files.pythonhosted.org/packages/64/83/bf31341f21fa594035891ff04a497dc86b210cc1a903a9cc01b097cc614f/rpds_py-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f54e7106f0001244a5f4cf810ba8d3f9c542e2730821b16e969d6887b664266", size = 363951 }, + { url = "https://files.pythonhosted.org/packages/a2/e1/8218bba36737621262df316fbb729639af25ff611cc07bfeaadc1bfa6292/rpds_py-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fed5dfefdf384d6fe975cc026886aece4f292feaf69d0eeb716cfd3c5a4dd8be", size = 373113 }, + { url = "https://files.pythonhosted.org/packages/39/8d/4afcd688e3ad33ec273900f42e6a41e9bd9f43cfc509b6d498683d2d0338/rpds_py-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590ef88db231c9c1eece44dcfefd7515d8bf0d986d64d0caf06a81998a9e8cab", size = 405944 }, + { url = "https://files.pythonhosted.org/packages/fa/65/3326efa721b6ecd70262aab69a26c9bc19398cdb0a2a416ef30b58326460/rpds_py-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f983e4c2f603c95dde63df633eec42955508eefd8d0f0e6d236d31a044c882d7", size = 422874 }, + { url = "https://files.pythonhosted.org/packages/31/fb/48a647d0afab74289dd21a4128002d58684c22600a22c4bfb76cb9e3bfb0/rpds_py-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b229ce052ddf1a01c67d68166c19cb004fb3612424921b81c46e7ea7ccf7c3bf", size = 364227 }, + { url = "https://files.pythonhosted.org/packages/f1/b0/1cdd179d7382dd52d65b1fd19c54d090b6bd0688dfbe259bb5ab7548c359/rpds_py-0.21.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ebf64e281a06c904a7636781d2e973d1f0926a5b8b480ac658dc0f556e7779f4", size = 386447 }, + { url = "https://files.pythonhosted.org/packages/dc/41/84ace07f31aac3a96b73a374d89106cf252f7d3274e7cae85d17a27c602d/rpds_py-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:998a8080c4495e4f72132f3d66ff91f5997d799e86cec6ee05342f8f3cda7dca", size = 549386 }, + { url = "https://files.pythonhosted.org/packages/33/ce/bf51bc5a3aa539171ea8c7737ab5ac06cef54c79b6b2a0511afc41533c89/rpds_py-0.21.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:98486337f7b4f3c324ab402e83453e25bb844f44418c066623db88e4c56b7c7b", size = 554777 }, + { url = "https://files.pythonhosted.org/packages/76/b1/950568e55a94c2979c2b61ec24e76e648a525fbc7551ccfc1f2841e39d44/rpds_py-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a78d8b634c9df7f8d175451cfeac3810a702ccb85f98ec95797fa98b942cea11", size = 530918 }, + { url = "https://files.pythonhosted.org/packages/78/84/93f00e3613426c8a7a9ca16782d2828f2ac55296dd5c6b599379d9f59ee2/rpds_py-0.21.0-cp312-none-win32.whl", hash = "sha256:a58ce66847711c4aa2ecfcfaff04cb0327f907fead8945ffc47d9407f41ff952", size = 203112 }, + { url = "https://files.pythonhosted.org/packages/e6/08/7a186847dd78881a781d2be9b42c8e49c3261c0f4a6d0289ba9a1e4cde71/rpds_py-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:e860f065cc4ea6f256d6f411aba4b1251255366e48e972f8a347cf88077b24fd", size = 220735 }, + { url = "https://files.pythonhosted.org/packages/32/3a/e69ec108eefb9b1f19ee00dde7a800b485942e62b123f01d9156a6d8569c/rpds_py-0.21.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ee4eafd77cc98d355a0d02f263efc0d3ae3ce4a7c24740010a8b4012bbb24937", size = 329206 }, + { url = "https://files.pythonhosted.org/packages/f6/c0/fa689498fa3415565306398c8d2a596207c2a13d3cc03724f32514bddfbc/rpds_py-0.21.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:688c93b77e468d72579351a84b95f976bd7b3e84aa6686be6497045ba84be560", size = 320245 }, + { url = "https://files.pythonhosted.org/packages/68/d0/466b61007005f1b2fd8501f23e4bdee4d71c7381b61358750920d1882ac9/rpds_py-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c38dbf31c57032667dd5a2f0568ccde66e868e8f78d5a0d27dcc56d70f3fcd3b", size = 363585 }, + { url = "https://files.pythonhosted.org/packages/1e/e2/787ea3a0f4b197893c62c254e6f14929c40bbcff86922928ac4eafaa8edf/rpds_py-0.21.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d6129137f43f7fa02d41542ffff4871d4aefa724a5fe38e2c31a4e0fd343fb0", size = 372302 }, + { url = "https://files.pythonhosted.org/packages/b5/ef/99f2cfe6aa128c21f1b30c66ecd348cbd59792953ca35eeb6efa38b88aa1/rpds_py-0.21.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520ed8b99b0bf86a176271f6fe23024323862ac674b1ce5b02a72bfeff3fff44", size = 405344 }, + { url = "https://files.pythonhosted.org/packages/30/3c/9d12d0b76ecfe80a7ba4770459828dda495d72b18cafd6dfd54c67b2e282/rpds_py-0.21.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaeb25ccfb9b9014a10eaf70904ebf3f79faaa8e60e99e19eef9f478651b9b74", size = 422322 }, + { url = "https://files.pythonhosted.org/packages/f9/22/387aec1cd6e124adbc3b1f40c4e4152c3963ae47d78d3ca650102ea72c4f/rpds_py-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af04ac89c738e0f0f1b913918024c3eab6e3ace989518ea838807177d38a2e94", size = 363739 }, + { url = "https://files.pythonhosted.org/packages/d1/3e/0ad65b776db13d13f002ab363fe3821cd1adec500d8e05e0a81047a75f9d/rpds_py-0.21.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9b76e2afd585803c53c5b29e992ecd183f68285b62fe2668383a18e74abe7a3", size = 386579 }, + { url = "https://files.pythonhosted.org/packages/4f/3b/c68c1067b24a7df47edcc0325a825908601aba399e2d372a156edc631ad1/rpds_py-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5afb5efde74c54724e1a01118c6e5c15e54e642c42a1ba588ab1f03544ac8c7a", size = 548924 }, + { url = "https://files.pythonhosted.org/packages/ab/1c/35f1a5cce4bca71c49664f00140010a96b126e5f443ebaf6db741c25b9b7/rpds_py-0.21.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:52c041802a6efa625ea18027a0723676a778869481d16803481ef6cc02ea8cb3", size = 554217 }, + { url = "https://files.pythonhosted.org/packages/c8/d0/48154c152f9adb8304b21d867d28e79be3b352633fb195c03c7107a4da9a/rpds_py-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee1e4fc267b437bb89990b2f2abf6c25765b89b72dd4a11e21934df449e0c976", size = 530540 }, + { url = "https://files.pythonhosted.org/packages/50/e8/78847f4e112e99fd5b7bc30fea3e4a44c20b811473d6755f944c5bf0aec7/rpds_py-0.21.0-cp313-none-win32.whl", hash = "sha256:0c025820b78817db6a76413fff6866790786c38f95ea3f3d3c93dbb73b632202", size = 202604 }, + { url = "https://files.pythonhosted.org/packages/60/31/083e6337775e133fb0217ed0ab0752380efa6e5112f2250d592d4135a228/rpds_py-0.21.0-cp313-none-win_amd64.whl", hash = "sha256:320c808df533695326610a1b6a0a6e98f033e49de55d7dc36a13c8a30cfa756e", size = 220448 }, ] [[package]] @@ -971,21 +1180,21 @@ wheels = [ ] [[package]] -name = "setuptools" -version = "75.4.0" +name = "six" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/73/c1ccf3e057ef6331cc6861412905dc218203bde46dfe8262c1631aa7fb11/setuptools-75.4.0.tar.gz", hash = "sha256:1dc484f5cf56fd3fe7216d7b8df820802e7246cfb534a1db2aa64f14fcb9cdcb", size = 1336593 } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/df/7c6bb83dcb45b35dc35b310d752f254211cde0bcd2a35290ea6e2862b2a9/setuptools-75.4.0-py3-none-any.whl", hash = "sha256:b3c5d862f98500b06ffdf7cc4499b48c46c317d8d56cb30b5c8bce4d88f5c216", size = 1223131 }, + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, ] [[package]] -name = "six" -version = "1.16.0" +name = "smmap" +version = "5.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +sdist = { url = "https://files.pythonhosted.org/packages/88/04/b5bf6d21dc4041000ccba7eb17dd3055feb237e7ffc2c20d3fae3af62baa/smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", size = 22291 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, + { url = "https://files.pythonhosted.org/packages/a7/a5/10f97f73544edcdef54409f1d839f6049a0d79df68adbc1ceb24d1aaca42/smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da", size = 24282 }, ] [[package]] @@ -1029,7 +1238,6 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "asyncio" }, - { name = "dash" }, { name = "duckdb" }, { name = "fastapi" }, { name = "httpx" }, @@ -1038,6 +1246,8 @@ dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "pyyaml" }, + { name = "streamlit" }, + { name = "uvicorn" }, ] [package.dev-dependencies] @@ -1051,7 +1261,6 @@ dev = [ [package.metadata] requires-dist = [ { name = "asyncio", specifier = ">=3.4.3" }, - { name = "dash", specifier = ">=2.18.2" }, { name = "duckdb", specifier = ">=1.1.3" }, { name = "fastapi", specifier = ">=0.115.4" }, { name = "httpx", specifier = ">=0.27.2" }, @@ -1060,6 +1269,8 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.9.2" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "streamlit", specifier = ">=1.40.1" }, + { name = "uvicorn", specifier = ">=0.32.0" }, ] [package.metadata.requires-dev] @@ -1070,6 +1281,36 @@ dev = [ { name = "ruff", specifier = ">=0.7.3" }, ] +[[package]] +name = "streamlit" +version = "1.40.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altair" }, + { name = "blinker" }, + { name = "cachetools" }, + { name = "click" }, + { name = "gitpython" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "pyarrow" }, + { name = "pydeck" }, + { name = "requests" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "toml" }, + { name = "tornado" }, + { name = "typing-extensions" }, + { name = "watchdog", marker = "platform_system != 'Darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/70/b76a32201b04a5a2a1c667fe7327cb7a3cc25d726d3863ba5863d2b0dccf/streamlit-1.40.1.tar.gz", hash = "sha256:1f2b09f04b6ad366a2c7b4d48104697d1c8bc33f48bdf7ed939cc04c12d3aec6", size = 8266452 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/14/857d0734989f3d26f2f965b2e3f67568ea7a6e8a60cb9c1ed7f774b6d606/streamlit-1.40.1-py2.py3-none-any.whl", hash = "sha256:b9d7a317a0cc88edd7857c7e07dde9cf95647d3ae51cbfa8a3db82fbb8a2990d", size = 8645398 }, +] + [[package]] name = "tenacity" version = "9.0.0" @@ -1079,6 +1320,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", size = 28169 }, ] +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, +] + [[package]] name = "tornado" version = "6.4.1" @@ -1115,6 +1365,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + [[package]] name = "urllib3" version = "2.2.3" @@ -1125,31 +1384,41 @@ wheels = [ ] [[package]] -name = "wcwidth" -version = "0.2.13" +name = "uvicorn" +version = "0.32.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, + { url = "https://files.pythonhosted.org/packages/eb/14/78bd0e95dd2444b6caacbca2b730671d4295ccb628ef58b81bee903629df/uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", size = 63723 }, ] [[package]] -name = "werkzeug" -version = "3.0.6" +name = "watchdog" +version = "6.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d4/f9/0ba83eaa0df9b9e9d1efeb2ea351d0677c37d41ee5d0f91e98423c7281c9/werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d", size = 805170 } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/69/05837f91dfe42109203ffa3e488214ff86a6d68b2ed6c167da6cdc42349b/werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17", size = 227979 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, ] [[package]] -name = "zipp" -version = "3.21.0" +name = "wcwidth" +version = "0.2.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, ] From 410112b7005bfef1b48cd907eac31be67303ebbb Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Sun, 15 Dec 2024 09:00:42 -0700 Subject: [PATCH 08/19] Modified code files --- .streamlit/config.toml | 6 + pyproject.toml | 16 +- src/main.py | 0 src/models/stock_data.py | 87 -- src/services/stock_analysis.py | 50 - src/{ => stock_valuation_app}/api/__init__.py | 0 .../api/fmp_client.py | 23 +- src/stock_valuation_app/main.py | 51 + .../models/__init__.py | 0 .../models/stock_models.py | 100 ++ .../services/__init__.py | 0 .../services/stock_analysis.py | 161 ++++ src/{ => stock_valuation_app}/ui/__init__.py | 0 src/stock_valuation_app/ui/app.py | 875 ++++++++++++++++++ src/stock_valuation_app/ui/style.html | 49 + src/{ => stock_valuation_app}/utils.py | 0 src/ui/app.py | 44 - uv.lock | 62 +- 18 files changed, 1288 insertions(+), 236 deletions(-) create mode 100644 .streamlit/config.toml delete mode 100644 src/main.py delete mode 100644 src/models/stock_data.py delete mode 100644 src/services/stock_analysis.py rename src/{ => stock_valuation_app}/api/__init__.py (100%) rename src/{ => stock_valuation_app}/api/fmp_client.py (75%) create mode 100644 src/stock_valuation_app/main.py rename src/{ => stock_valuation_app}/models/__init__.py (100%) create mode 100644 src/stock_valuation_app/models/stock_models.py rename src/{ => stock_valuation_app}/services/__init__.py (100%) create mode 100644 src/stock_valuation_app/services/stock_analysis.py rename src/{ => stock_valuation_app}/ui/__init__.py (100%) create mode 100644 src/stock_valuation_app/ui/app.py create mode 100644 src/stock_valuation_app/ui/style.html rename src/{ => stock_valuation_app}/utils.py (100%) delete mode 100644 src/ui/app.py diff --git a/.streamlit/config.toml b/.streamlit/config.toml new file mode 100644 index 0000000..3767d48 --- /dev/null +++ b/.streamlit/config.toml @@ -0,0 +1,6 @@ +[theme] +base="dark" +backgroundColor="#F5F5F5" # "#E3F2FD" +secondaryBackgroundColor="#F0F2F6" +textColor="#333333" +font="sans serif" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 31c1eae..62ecd69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,20 +7,23 @@ authors = [ { name = "Dimeji Salau", email = "dimejisalau@protonmail.com" } ] requires-python = ">=3.12" + dependencies = [ "asyncio>=3.4.3", "duckdb>=1.1.3", - "fastapi>=0.115.4", "httpx>=0.27.2", + "pandas>=2.2.3", + "plotly>=5.24.1", "polars>=1.12.0", - "pyarrow>=18.0.0", "pydantic>=2.9.2", "python-dotenv>=1.0.1", "pyyaml>=6.0.2", "streamlit>=1.40.1", - "uvicorn>=0.32.0", ] +[tool.hatch.build.targets.wheel] +packages = ["src/stock_valuation_app"] + [project.scripts] stock-valuation-app = "stock_valuation_app:main" @@ -36,5 +39,8 @@ dev = [ "ruff>=0.7.3", ] -[tool.setuptools] -package-dir = {"" = "src"} +[tool.pytest.ini.options] +pythonpath = "src/stock_valuation_app" + +# [tool.setuptools] +# package-dir = {"" = "src"} diff --git a/src/main.py b/src/main.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/models/stock_data.py b/src/models/stock_data.py deleted file mode 100644 index bbb148f..0000000 --- a/src/models/stock_data.py +++ /dev/null @@ -1,87 +0,0 @@ -from typing import Optional -from pydantic import BaseModel, Field - - -class CompanyProfile(BaseModel): - symbol: str - company_name: str = Field(...,alias="companyName") - sector: Optional[str] = None - industry: Optional[str] = None - description: Optional[str] = None - - -class Rating(BaseModel): - symbol: str - date: str - rating: str - score: int = Field(..., alias="ratingScore") - recommendation: str = Field(..., alias="ratingRecommendation") - dcf_score: int = Field(..., alias="ratingDetailsDCFScore") - dcf_rec: str = Field(..., alias="ratingDetailsDCFRecommendation") - roe_score: int = Field(..., alias="ratingDetailsROEScore") - roe_rec: str = Field(..., alias="ratingDetailsROERecommendation") - roa_score: int = Field(..., alias="ratingDetailsROAScore") - roa_rec: str = Field(..., alias="ratingDetailsROARecommendation") - de_score: int = Field(..., alias="ratingDetailsDEScore") - de_rec: str = Field(..., alias="ratingDetailsDERecommendation") - pe_score: int = Field(..., alias="ratingDetailsPEScore") - pe_rec: str = Field(..., alias="ratingDetailsPERecommendation") - pb_score: int = Field(..., alias="ratingDetailsPBScore") - pb_rec: str = Field(..., alias="ratingDetailsPBRecommendation") - - -class Ratios(BaseModel): - symbol: str - year: str = Field(..., alias="calendarYear") - de_ratio: float = Field(..., alias="debtEquityRatio") - fcf_ps: float = Field(..., alias="freeCashFlowPerShare") - pb_ratio: float = Field(..., alias="priceToBookRatio") - ps_ratio: float = Field(..., alias="priceToSalesRatio") - pe_ratio: float = Field(..., alias="priceEarningsRatio") - p_fcf_ratio: float = Field(..., alias="priceToFreeCashFlowsRatio") - peg_ratio: float = Field(..., alias="priceEarningsToGrowthRatio") - div_yield: int = Field(..., alias="dividendYield") - curr_ratio: float = Field(..., alias="currentRatio") - - -class KeyMetrics(BaseModel): - symbol: str - year: str = Field(..., alias="calendarYear") - rev_per_share: float = Field(..., alias="revenuePerShare") - net_income_per_share: float = Field(..., alias="netIncomePerShare") - op_cf_per_share: float = Field(..., alias="operatingCashFlowPerShare") - fcf_per_share: float = Field(..., alias="freeCashFlowPerShare") - book_val_per_share: float = Field(..., alias="bookValuePerShare") - ev_over_ebitda: float = Field(..., alias="enterpriseValueOverEBITDA") - fcf_yield: float = Field(..., alias="freeCashFlowYield") - int_coverage: float = Field(..., alias="interestCoverage") - roic: float - - -class Growth(BaseModel): - symbol: str - year: str = Field(..., alias="calendarYear") - rev_growth: float = Field(..., alias="revenueGrowth") - net_inc_growth: float = Field(..., alias="netIncomeGrowth") - eps_growth: float = Field(..., alias="epsdilutedGrowth") - dps_growth: float = Field(..., alias="dividendsperShareGrowth") - fcf_growth: float = Field(..., alias="freeCashFlowGrowth") - rev_growth_10y: float = Field(..., alias="tenYRevenueGrowthPerShare") - rev_growth_5y: float = Field(..., alias="fiveYRevenueGrowthPerShare") - rev_growth_3y: float = Field(..., alias="threeYRevenueGrowthPerShare") - net_inc_growth_10y: float = Field(..., alias="tenYNetIncomeGrowthPerShare") - net_inc_growth_5y: float = Field(..., alias="fiveYNetIncomeGrowthPerShare") - net_inc_growth_3y: float = Field(..., alias="threeYNetIncomeGrowthPerShare") - dps_growth_10y: float = Field(..., alias="tenYDividendperShareGrowthPerShare") - dps_growth_5y: float = Field(..., alias="fiveYDividendperShareGrowthPerShare") - dps_growth_3y: float = Field(..., alias="threeYDividendperShareGrowthPerShare") - bvps_growth: float = Field(..., alias="bookValueperShareGrowth") - debt_growth: float = Field(..., alias="debtGrowth") - - -class CombinedModel(BaseModel): - profile: list[CompanyProfile] - rating: list[Rating] - key_metrics: list[KeyMetrics] - ratios: list[Ratios] - growth: list[Growth] \ No newline at end of file diff --git a/src/services/stock_analysis.py b/src/services/stock_analysis.py deleted file mode 100644 index baa3647..0000000 --- a/src/services/stock_analysis.py +++ /dev/null @@ -1,50 +0,0 @@ -import asyncio -import polars as pl -# from stock_valuation_app.data.fmp_client import FMPClient - - - - -raw_data = asyncio.run(extract_source_data("PAYS")) - -profile_df = pl.DataFrame(raw_data["profile"]) -rating_df = pl.DataFrame(raw_data["rating"]) -metric_df = pl.DataFrame(raw_data["key_metrics"]) -growth_df = pl.DataFrame(raw_data["growth"]) - -# Pivot the DataFrame -pivoted_df = profile_df.unpivot(variable_name="Column", value_name="Value") -# Sort the result to maintain the original order -pivoted_df = pivoted_df.sort("Column") - -# Convert DataFrame to formatted text -formatted_text = [] -for row in pivoted_df.iter_rows(): - column, value = row - formatted_text.append(html.Div([html.Strong(f"{column}: "), f"{value}"])) - -# async def extract_source_data(ticker: str): -# source_data = await FMPClient().fetch_data(ticker) -# return { -# "profile": source_data["profile"], -# "rating": source_data["rating"], -# "key_metrics": source_data["key_metrics"], -# "growth": source_data["growth"], -# } - -# async def extract_source_data(ticker: str): -# source_data = await FMPClient().fetch_data(ticker) -# return source_data - - -# if __name__ == "__main__": -# pass - # raw_data = asyncio.run(extract_source_data('PAYS')) - - # def get_dataframes(): - # """Captures source dataframes""" - # profile_df = pl.DataFrame(raw_data["profile"]) - # rating_df = pl.DataFrame(raw_data["rating"]) - # metric_df = pl.DataFrame(raw_data["key_metrics"]) - # growth_df = pl.DataFrame(raw_data["growth"]) - # return (profile_df, rating_df, metric_df, growth_df) diff --git a/src/api/__init__.py b/src/stock_valuation_app/api/__init__.py similarity index 100% rename from src/api/__init__.py rename to src/stock_valuation_app/api/__init__.py diff --git a/src/api/fmp_client.py b/src/stock_valuation_app/api/fmp_client.py similarity index 75% rename from src/api/fmp_client.py rename to src/stock_valuation_app/api/fmp_client.py index 9e85c89..b558fe5 100644 --- a/src/api/fmp_client.py +++ b/src/stock_valuation_app/api/fmp_client.py @@ -3,8 +3,8 @@ from dataclasses import dataclass, field from typing import Any import httpx -import utils -from models.stock_data import CombinedModel +import stock_valuation_app.utils as utils +from stock_valuation_app.models.stock_models import CombinedModel def get_endpoint(url: str) -> str: @@ -28,7 +28,16 @@ class FMPClient: """A client for interacting with the Financial Modeling Prep API.""" base_url: str = field(default_factory=get_base_url) api_key: str = field(default_factory=get_api_key) - metric_types: list[str] = field(default_factory=lambda: ["profile", "rating", "ratios", "key-metrics", "financial-growth",]) + metric_types: list[str] = field( + default_factory=lambda: [ + "profile", + "rating", + "quote", + "key-metrics-ttm", + "key-metrics", + "financial-growth", + ] + ) # async def get_data(self, client: httpx.Client, url: str) -> dict[str, Any]: """Call API endpoint asynchronously""" @@ -40,7 +49,7 @@ async def fetch_data(self, ticker: str) -> dict[str, list[dict[str, Any]]]: """Extracts data asynchronously from multiple FMP endpoints""" urls = [] for metric in self.metric_types: - if metric in ["profile", "rating"]: + if metric in ["profile", "quote", "rating", "key-metrics-ttm"]: endpoint = f"{self.base_url}/{metric}/{ticker}?apikey={self.api_key}" else: endpoint = f"{self.base_url}/{metric}/{ticker}?period=annual&apikey={self.api_key}" @@ -54,7 +63,11 @@ async def fetch_data(self, ticker: str) -> dict[str, list[dict[str, Any]]]: results = await asyncio.gather(*tasks) # Rename some metric types to match with fields defined in the CombinedModel - replace_metric_types = {"key-metrics": "key_metrics", "financial-growth": "growth",} + replace_metric_types = { + "rating": "ratings", + "key-metrics": "key_metrics", + "key-metrics-ttm": "key_metrics_ttm", + "financial-growth": "growth",} # new_metric_types = [replace_metric_types.get(item, item) for item in self.metric_types] # Create combined records dict for validation diff --git a/src/stock_valuation_app/main.py b/src/stock_valuation_app/main.py new file mode 100644 index 0000000..8533fb8 --- /dev/null +++ b/src/stock_valuation_app/main.py @@ -0,0 +1,51 @@ +import streamlit as st + + +def display_stock_price(label, stock_price): + html_content = f""" + +
+
{label}
+
Stock Price
+
${stock_price}
+
+ """ + st.html(html_content) + + +# Streamlit app +def main(): + st.title("Stock Price Display") + display_stock_price("AAPL", 150.25) + + +if __name__ == "__main__": + main() diff --git a/src/models/__init__.py b/src/stock_valuation_app/models/__init__.py similarity index 100% rename from src/models/__init__.py rename to src/stock_valuation_app/models/__init__.py diff --git a/src/stock_valuation_app/models/stock_models.py b/src/stock_valuation_app/models/stock_models.py new file mode 100644 index 0000000..0db419b --- /dev/null +++ b/src/stock_valuation_app/models/stock_models.py @@ -0,0 +1,100 @@ +from typing import Optional +from pydantic import BaseModel, Field + + +class CompanyProfile(BaseModel): + symbol: str + beta: float + range: str + company_name: str = Field(..., alias="companyName") + sector: Optional[str] = None + industry: Optional[str] = None + description: Optional[str] = None + image: Optional[str] = None + + +class Quote(BaseModel): + symbol: str + price: float + change_percent: Optional[float] = Field(..., alias="changesPercentage") + year_high: float = Field(..., alias="yearHigh") + year_low: float = Field(..., alias="yearLow") + market_cap: float = Field(..., alias="marketCap") + vol_avg: int = Field(..., alias="avgVolume") + eps: float + pe: float + earning_date: str = Field(..., alias="earningsAnnouncement") + shares_outstanding: int = Field(..., alias="sharesOutstanding") + + + +class Ratings(BaseModel): + symbol: str + date: str + rating: str + score: int = Field(..., alias="ratingScore") + recommendation: str = Field(..., alias="ratingRecommendation") + dcf_score: int = Field(..., alias="ratingDetailsDCFScore") + dcf_rec: str = Field(..., alias="ratingDetailsDCFRecommendation") + roe_score: int = Field(..., alias="ratingDetailsROEScore") + roe_rec: str = Field(..., alias="ratingDetailsROERecommendation") + roa_score: int = Field(..., alias="ratingDetailsROAScore") + roa_rec: str = Field(..., alias="ratingDetailsROARecommendation") + de_score: int = Field(..., alias="ratingDetailsDEScore") + de_rec: str = Field(..., alias="ratingDetailsDERecommendation") + pe_score: int = Field(..., alias="ratingDetailsPEScore") + pe_rec: str = Field(..., alias="ratingDetailsPERecommendation") + pb_score: int = Field(..., alias="ratingDetailsPBScore") + pb_rec: str = Field(..., alias="ratingDetailsPBRecommendation") + + +class KeyMetricsTTM(BaseModel): + rev_per_share_ttm: float = Field(..., alias="revenuePerShareTTM") + net_income_per_share_ttm: float = Field(..., alias="netIncomePerShareTTM") + fcf_per_share_ttm: float = Field(..., alias="freeCashFlowPerShareTTM") + pe_ratio_ttm: float = Field(..., alias="peRatioTTM") + ev_over_ebitda_ttm: float = Field(..., alias="enterpriseValueOverEBITDATTM") + ev_to_fcf_ttm: float = Field(..., alias="evToFreeCashFlowTTM") + fcf_yield_ttm: float = Field(..., alias="freeCashFlowYieldTTM") + pts_ratio_ttm: float = Field(..., alias="priceToSalesRatioTTM") + ptb_ratio_ttm: float = Field(..., alias="ptbRatioTTM") + pfcf_ratio_ttm: float = Field(..., alias="pfcfRatioTTM") + dvd_yield_pct_ttm: float = Field(..., alias="dividendYieldPercentageTTM") + dvd_per_share_ttm: float = Field(..., alias="dividendPerShareTTM") + payout_ratio_ttm: float = Field(..., alias="payoutRatioTTM") + + +class KeyMetrics(BaseModel): + symbol: str + date: str + rev_per_share: float = Field(..., alias="revenuePerShare") + fcf_per_share: float = Field(..., alias="freeCashFlowPerShare") + pe_ratio: float = Field(..., alias="peRatio") + ev_over_ebitda: float = Field(..., alias="enterpriseValueOverEBITDA") + ev_to_fcf: float = Field(..., alias="evToFreeCashFlow") + fcf_yield: float = Field(..., alias="freeCashFlowYield") + + +class Growth(BaseModel): + symbol: str + date: str + rev_growth: float = Field(..., alias="revenueGrowth") + eps_growth: float = Field(..., alias="epsdilutedGrowth") + dps_growth: float = Field(..., alias="dividendsperShareGrowth") + fcf_growth: float = Field(..., alias="freeCashFlowGrowth") + debt_growth: float = Field(..., alias="debtGrowth") + fiveY_rev_growth_per_share: float = Field(..., alias="fiveYRevenueGrowthPerShare") + fiveY_ni_growth_per_share: float = Field(..., alias="fiveYNetIncomeGrowthPerShare") + fiveY_dps_growth_per_share: float = Field(..., alias="fiveYDividendperShareGrowthPerShare") + fiveY_opcf_growth_per_share: float = Field(..., alias="fiveYOperatingCFGrowthPerShare") + + + +class CombinedModel(BaseModel): + profile: list[CompanyProfile] + quote: list[Quote] + ratings: list[Ratings] + key_metrics_ttm: list[KeyMetricsTTM] + key_metrics: list[KeyMetrics] + growth: list[Growth] + diff --git a/src/services/__init__.py b/src/stock_valuation_app/services/__init__.py similarity index 100% rename from src/services/__init__.py rename to src/stock_valuation_app/services/__init__.py diff --git a/src/stock_valuation_app/services/stock_analysis.py b/src/stock_valuation_app/services/stock_analysis.py new file mode 100644 index 0000000..c51b87d --- /dev/null +++ b/src/stock_valuation_app/services/stock_analysis.py @@ -0,0 +1,161 @@ +import asyncio +import duckdb as db +from typing import Any, Optional +import polars as pl +import streamlit as st +from stock_valuation_app.api.fmp_client import FMPClient + + + +# async def extract_source_data(ticker: str): +# source_data = await FMPClient().fetch_data(ticker) +# return source_data + + +# raw_data = asyncio.run(extract_source_data("PAYS")) + +# profile_df = pl.DataFrame(raw_data["profile"]) + +StockData = dict[str, list[dict[str, Any]]] + +#@st.cache_resource +async def extract_source_data(ticker: str) -> StockData: + """Pull source data for a given ticker""" + source_data = await FMPClient().fetch_data(ticker) + return source_data + + +def get_long_df(df: pl.DataFrame, var: str, value: str) -> pl.DataFrame: + """Convert wide dataframe to long format""" + longdf = df.unpivot(variable_name=var, value_name=value) + return longdf + + +#@st.cache_data +def transform_profile(stock_data: StockData): + """Processes profile dataset""" + df = pl.DataFrame(stock_data.get("profile", None)).cast(pl.String()) + + if df is None: + print("Dataframe is empty") + return None + + ndf = db.sql("""WITH ref_data AS (SELECT symbol AS Symbol, price AS Price, beta AS Beta, + vol_avg AS 'Average Volume', mkt_cap AS 'Market Cap', last_div AS 'Last Dividend', + low_high AS '52w Low - High', price_change AS 'Price Change', currency AS Currency, + exchange AS Exchange, sector AS Sector, industry AS Industry, description AS Description + FROM df + ) + UNPIVOT ref_data + ON COLUMNS(*) + INTO + NAME metric + VALUE values + """ + ).pl() + result_dict = dict(zip(ndf["metric"], ndf["values"])) + return result_dict + + +# @st.cache_data +def transform_rating(stock_data: StockData): + """Processes rating dataset""" + df = pl.DataFrame(stock_data.get("ratings", None)).cast(pl.String()) + + if df is None: + print("Dataframe is empty") + return None + + ndf = db.sql("""WITH ref_data AS (SELECT symbol AS Symbol, date AS Date, rating AS Rating, score AS Score, + recommendation AS Recommendation, dcf_score AS 'DCF Score', dcf_rec AS 'DCF Recommendation', + roe_score AS 'ROE Score', roe_rec AS 'ROE Recommendation', roa_score AS 'ROA Score', + roa_rec AS 'ROA Recommendation', de_score AS 'DE Score', de_rec AS 'DE Recommendation', + pe_score AS 'PE Score', pe_rec AS 'PE Recommendation', pb_score AS 'PB Score', + pb_rec AS 'PB Recommendation' + FROM df + ) + UNPIVOT ref_data + ON COLUMNS(*) + INTO + NAME 'Metric' + VALUE Rating + """ + ).pl() + return ndf + +#@st.cache_data +def transform_ratios(stock_data: StockData): + """Processes ratios dataset""" + + df = pl.DataFrame(stock_data.get("ratios", None)).cast(pl.String()) + + # if df is None: + # print("Dataframe is empty") + # return None + + xdf = db.sql("""WITH ref_data AS (SELECT * EXCLUDE (symbol, pb_ratio, curr_ratio) FROM df) UNPIVOT ref_data ON COLUMNS(* EXCLUDE year) INTO NAME metric VALUE values; """).pl() + return xdf + + # if not df.is_empty(): + # newdf = df.select(pl.exclude(["symbol", "book_val_per_share"])) + # val_var = newdf.select(pl.exclude(["year"])).columns + # idx = newdf.select(pl.col("year")).columns + # long_newdf = newdf.unpivot(on=val_var, index=idx, variable_name="Metric", value_name="Value") + # return (long_newdf, val_var) + + + + +# data = asyncio.run(extract_source_data('NVDA')) +# xdf = transform_ratios(data) +# # # print(xdf.schema) +# # # print() +# print(xdf.head(10)) +# # xdf, val_var = transform_ratios(data) +# # print(xdf.head()) +# # print(val_var) + +# df = pl.DataFrame(data.get("ratios", None)) +# print(df.lazy().collect().schema) + +# profile_df = pl.DataFrame(raw_data["profile"]) +# rating_df = pl.DataFrame(raw_data["rating"]) +# metric_df = pl.DataFrame(raw_data["key_metrics"]) +# growth_df = pl.DataFrame(raw_data["growth"]) + +# # Pivot the DataFrame +# pivoted_df = profile_df.unpivot(variable_name="Column", value_name="Value") +# # Sort the result to maintain the original order +# pivoted_df = pivoted_df.sort("Column") + +# # Convert DataFrame to formatted text +# formatted_text = [] +# for row in pivoted_df.iter_rows(): +# column, value = row +# formatted_text.append(html.Div([html.Strong(f"{column}: "), f"{value}"])) + +# async def extract_source_data(ticker: str): +# source_data = await FMPClient().fetch_data(ticker) +# return { +# "profile": source_data["profile"], +# "rating": source_data["rating"], +# "key_metrics": source_data["key_metrics"], +# "growth": source_data["growth"], +# } + +# async def extract_source_data(ticker: str): +# source_data = await FMPClient().fetch_data(ticker) +# return source_data + + +# if __name__ == "__main__": +# pass + # raw_data = asyncio.run(extract_source_data('PAYS')) + + # def get_dataframes(): + # """Captures source dataframes""" + # profile_df = pl.DataFrame(raw_data["profile"]) + # rating_df = pl.DataFrame(raw_data["rating"]) + # metric_df = pl.DataFrame(raw_data["key_metrics"]) + # growth_df = pl.DataFrame(raw_data["growth"]) + # return (profile_df, rating_df, metric_df, growth_df) diff --git a/src/ui/__init__.py b/src/stock_valuation_app/ui/__init__.py similarity index 100% rename from src/ui/__init__.py rename to src/stock_valuation_app/ui/__init__.py diff --git a/src/stock_valuation_app/ui/app.py b/src/stock_valuation_app/ui/app.py new file mode 100644 index 0000000..57adc17 --- /dev/null +++ b/src/stock_valuation_app/ui/app.py @@ -0,0 +1,875 @@ +from typing import Any +import asyncio +import pandas as pd +import duckdb as db +import polars as pl +import streamlit as st +import streamlit.components.v1 as components +import plotly.graph_objects as go +import plotly.express as px +from stock_valuation_app.services.stock_analysis import extract_source_data + + +def display_profile(profile_data: dict[str, Any]): + """Display company profile in sidebar""" + try: + if profile_data: + st.sidebar.write("## Company Profile") + st.sidebar.image(profile_data["image"], width=180) + st.sidebar.write(f"## {profile_data['company_name']}") + st.sidebar.write(f"**Sector**: {profile_data['sector']}") + st.sidebar.write(f"**Industry**: {profile_data['industry']}") + st.sidebar.write(f"**Description**: {profile_data['description']}") + except IndexError: + st.write("Couldn't find company profile.") + + +def display_quotes(quote_data: dict[str, Any], profile_data: dict[str, Any]): + + def display_stock_metric(header, value): + html_content = f""" + +
+
{header}
+
{value}
+
+ """ + st.html(html_content) + try: + #label = quote_data["symbol"] + stock_price = f"${quote_data['price']:,.2f}" + change_price = f"{quote_data['change_percent']:,.2f}%" + year_low = f"${quote_data['year_low']:,.2f}" + year_high = f"${quote_data['year_high']:,.2f}" + market_cap = f"{quote_data["market_cap"]/1_000_000_000:.2f}B" + vol_avg = f"{quote_data["vol_avg"]/1_000_000:.2f}M" + earning_date = quote_data["earning_date"][:10] + shares_outstanding = f"{quote_data["shares_outstanding"]/1_000_000_000:.2f}B" + beta = profile_data["beta"] + + col1, col2, col3, col4, col5, col6, col7, col8, col9 = st.columns(9) + + with col1: + display_stock_metric("Price", stock_price) + with col2: + display_stock_metric("Change Percent", change_price) + with col3: + display_stock_metric("52w Low", year_low) + with col4: + display_stock_metric("52w High", year_high) + with col5: + display_stock_metric("Avg Volume", vol_avg) + with col6: + display_stock_metric("Market Cap", market_cap) + with col7: + display_stock_metric("Shares", shares_outstanding) + with col8: + display_stock_metric("Beta", beta) + with col9: + display_stock_metric("Earning Date", earning_date) + except Exception as e: + print(e) + + +def display_metric_tables(data: list[dict[str, Any]]): + + def generate_table_rows(data: dict[str, Any]): # table_name: str + rows_html = "" + for metric, value in data.items(): + row = f""" + +
+ {metric}: + {value} +
+ """ + rows_html += row + return rows_html + + def display_table(data: dict[str, Any], header_name: str): + table_html = f""" + +
+
{header_name}
+ {generate_table_rows(data)} +
+ """ + return components.html(table_html, height=210) #210 + + + + # Get data and generate tables + quote_data, key_metrics_ttm_data, growth_data, ratings_data = data + latest_growth_data = growth_data[0] + latest_ratings_data = ratings_data[0] + + valuation_data = { + "PE Ratio (TTM)": f"{quote_data['pe']:,.2f}", + "EPS (TTM)": f"{quote_data['eps']:,.2f}", + "EV/EBITDA (TTM)": f"{key_metrics_ttm_data['ev_over_ebitda_ttm']:,.2f}", + "Price/Sales (TTM)": f"{key_metrics_ttm_data['pts_ratio_ttm']:,.2f}", + "Price/Book (TTM)": f"{key_metrics_ttm_data['ptb_ratio_ttm']:,.2f}", + } + + freecashflow_data = { + "FCF Yield (TTM)": f"{key_metrics_ttm_data['fcf_yield_ttm'] * 100:,.2f}%", + "Price/FCF (TTM)": f"{key_metrics_ttm_data['pfcf_ratio_ttm']:,.2f}", + "EV/FCF (TTM)": f"{key_metrics_ttm_data['ev_to_fcf_ttm']:,.2f}", + "FCF/Share (TTM)": f"{key_metrics_ttm_data['fcf_per_share_ttm']:,.2f}", + } + growth_metric_data = { + "5Y Rev Growth/Share": f"{latest_growth_data['fiveY_rev_growth_per_share']:,.2f}", + "5Y NI Growth/Share": f"{latest_growth_data['fiveY_ni_growth_per_share']:,.2f}", + "5Y Div Growth/Share": f"{latest_growth_data['fiveY_dps_growth_per_share']:,.2f}", + "5Y OCF Growth/Share": f"{latest_growth_data['fiveY_opcf_growth_per_share']:,.2f}", + } + + dividend_data = { + "Div Yield (TTM)": f"{key_metrics_ttm_data['dvd_yield_pct_ttm']:,.2f}%", + "Div/Share (TTM)": f"{key_metrics_ttm_data['dvd_per_share_ttm']:,.2f}", + "Payout Ratio (TTM)": f"{key_metrics_ttm_data['payout_ratio_ttm'] * 100:,.2f}%", + } + + rating_data = { + #"Symbol": f"{latest_ratings_data['symbol']}", + "Date": f"{latest_ratings_data['date']}", + "Rating": f"{latest_ratings_data['rating']}", + "Score": f"{latest_ratings_data['score']}", + "Recommendation": f"{latest_ratings_data['recommendation']}", + } + + # Define tables layout + container = st.empty() + col1, col2, col3, col4, col5 = st.columns(5) + with col1: + container.markdown( + display_table(valuation_data, "Valuation"), unsafe_allow_html=True + ) + with col2: + container.markdown( + display_table(freecashflow_data, "Cash Flow"), unsafe_allow_html=True + ) + with col3: + container.markdown( + display_table(growth_metric_data, "Growth"), unsafe_allow_html=True + ) + with col4: + container.markdown( + display_table(dividend_data, "Dividend"), unsafe_allow_html=True + ) + with col5: + container.markdown( + display_table(rating_data, "Rating"), unsafe_allow_html=True + ) + + +# def display_ratings(ratings_data: list[dict[str, Any]]): +# """Displays rating dataset""" + +# dfx = pl.DataFrame(ratings_data).cast(pl.String()) + +# if dfx is None: +# print("Dataframe is empty") +# return None + +# ndf = db.sql("""SELECT symbol AS Symbol, date AS Date, rating AS Rating, score AS Score, +# recommendation AS Recommendation, CONCAT(dcf_score, ' ', '(', dcf_rec, ')') AS 'Discounted Cash Flow', +# CONCAT(roe_score, ' ', '(', roe_rec, ')') AS 'Return on Equity', +# CONCAT(roa_score, ' ', '(', roa_rec, ')') AS 'Return on Assets', +# CONCAT(de_score, ' ', '(', de_rec, ')') AS 'Debt-to-Equity', +# CONCAT(pe_score, ' ', '(', pe_rec, ')') AS 'Price-to-Earnings', +# CONCAT(pb_score, ' ', '(', pb_rec, ')') AS 'Price-to-Book' +# FROM dfx +# """).df() + +# # Define custom CSS for larger text and center alignment +# custom_css = """ +# +# """ + +# # Apply styling to the DataFrame +# styled_df = ndf.set_index("Symbol").style.set_properties(**{'text-align': 'center'}) + +# # Display the custom CSS and the styled DataFrame +# st.markdown(custom_css, unsafe_allow_html=True) +# return st.dataframe(styled_df, use_container_width=True) + + + +def display_metrics_charts(metrics_data: list[dict[str, Any]], key_metrics_ttm_data: dict[str, Any]): + + def create_compact_line_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value): + fig = go.Figure() + + # Historical data line + fig.add_trace( + go.Scatter( + x=data[x_col], + y=data[y_col], + mode="lines+markers", + line=dict(width=2, color="royalblue"), # Professional blue color + marker=dict(size=6, color="royalblue", line=dict(width=1, color="darkblue")), + name=f"Historical", + ) + ) + + # TTM value point + latest_date = data[x_col].dt.max() + fig.add_trace( + go.Scatter( + x=[latest_date], + y=[ttm_value], + mode="markers+text", + marker=dict( + size=6, + color="firebrick", + symbol="diamond", + line=dict(width=2, color="darkred"), + ), + name=f"TTM", + text=[f"{ttm_value:.2f}"], # Display TTM value as text + textposition="top center", + ) + ) + + # Horizontal line for TTM value + fig.add_shape( + type="line", + x0=data[x_col].dt.min(), + y0=ttm_value, + x1=latest_date, + y1=ttm_value, + line=dict(color="firebrick", width=2, dash="dash"), + ) + + # Update layout for a more professional appearance + fig.update_layout( + title="", + title_font=dict(size=16, family="Arial", color="black"), + xaxis_title="Year", + yaxis_title=title, + plot_bgcolor="white", # Clean background + height=400, + margin=dict(l=40, r=40, t=40, b=40), + font=dict(family="Arial", size=12), + showlegend=True, + legend=dict( + orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 + ), + hovermode="x unified", # Unified hover for better readability + ) + + # Customize axes + fig.update_xaxes( + tickmode="array", + tickvals=data[x_col], + ticktext=data[x_col], + gridcolor="lightgrey", + ) + fig.update_yaxes(gridcolor="lightgrey") + + return fig + + # Create charts + df = ( + pl.DataFrame(metrics_data) + .with_columns( + pl.col("date") + .str.strptime(pl.Date, format="%Y-%m-%d") + .alias("FYDateEnding") + ) + .sort("date") + .drop("date") + ) + + if df is None: + print("Dataframe is empty") + return None + + + + def plot_chart(metrics: list[tuple[str, str]]): + for metric, title in metrics: + ttm_value = key_metrics_ttm_data[f"{metric}_ttm"] + dfx = df.select("FYDateEnding", f"{metric}") + + st.plotly_chart( + create_compact_line_chart( + dfx, + x_col="FYDateEnding", + y_col=f"{metric}", + title=f"{title}", + ttm_value=ttm_value, + ), + use_container_width=True, + ) + + + col1, col2 = st.columns(2) + col3, col4 = st.columns(2) + col5, col6 = st.columns(2) + + with col1: + col1_metrics = [("rev_per_share", "Rev/Share"),] + plot_chart(col1_metrics) + with col2: + col2_metrics = [("pe_ratio", "PE Ratio"),] + plot_chart(col2_metrics) + with col3: + col3_metrics = [("fcf_per_share", "FCF/Share"),] + plot_chart(col3_metrics) + with col4: + col4_metrics = [("ev_over_ebitda", "EV/EBITDA"),] + plot_chart(col4_metrics) + with col5: + col5_metrics = [("ev_to_fcf", "EV/FCF"),] + plot_chart(col5_metrics) + with col6: + col6_metrics = [("fcf_yield", "FCF Yield"),] + plot_chart(col6_metrics) + + + +def display_growth_charts(growth_data: list[dict[str, Any]]): + def create_compact_line_chart(data: pl.DataFrame, x_col, y_col, title): + fig = go.Figure() + + # Historical data line + fig.add_trace( + go.Scatter( + x=data[x_col], + y=data[y_col], + mode="lines+markers", + line=dict(width=2, color="royalblue"), # Professional blue color + marker=dict( + size=6, color="royalblue", line=dict(width=1, color="darkblue") + ), + name="", #f"Historical {title}", + ) + ) + + # Update layout for a more professional appearance + fig.update_layout( + title="", + title_font=dict(size=16, family="Arial", color="black"), + xaxis_title="Year", + yaxis_title=title, + plot_bgcolor="white", # Clean background + height=400, + margin=dict(l=40, r=40, t=40, b=40), + font=dict(family="Arial", size=12), + showlegend=True, + legend=dict( + orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 + ), + hovermode="x unified", # Unified hover for better readability + ) + + # Customize axes + fig.update_xaxes( + tickmode="array", + tickvals=data[x_col], + ticktext=data[x_col], + gridcolor="lightgrey", + ) + fig.update_yaxes(gridcolor="lightgrey") + + return fig + + # Create charts + df = ( + pl.DataFrame(growth_data) + .with_columns( + pl.col("date") + .str.strptime(pl.Date, format="%Y-%m-%d") + .alias("Year") + ) + .sort("date") + .drop("date") + ) + + if df is None: + print("Dataframe is empty") + return None + + + def plot_chart(metrics: list[tuple[str, str]]): + for metric, title in metrics: + dfx = df.select("Year", f"{metric}") + + st.plotly_chart( + create_compact_line_chart( + dfx, + x_col="Year", + y_col=f"{metric}", + title=f"{title}", + ), + use_container_width=True, + ) + + col1, col2 = st.columns(2) + col3, col4 = st.columns(2) + col5, col6 = st.columns(2) + + with col1: + col1_metrics = [("rev_growth", "Rev Growth"),] + plot_chart(col1_metrics) + with col2: + col2_metrics = [("eps_growth", "EPS Growth"),] + plot_chart(col2_metrics) + with col3: + col3_metrics = [("dps_growth", "DPS Growth"),] + plot_chart(col3_metrics) + with col4: + col4_metrics = [("fcf_growth", "FCF Growth"),] + plot_chart(col4_metrics) + with col5: + col5_metrics = [("debt_growth", "Debt Growth"),] + plot_chart(col5_metrics) + # with col6: + # col6_metrics = [("fcf_yield", "FCF Yield"),] + # plot_chart(col6_metrics) + + + +def main(): + # Main UI + st.set_page_config( + page_title="Stock Valuation Dashboard", + layout="wide", + ) + + st.title("Stock Valuation Dashboard") + #st.divider() + st.subheader( + "Get stock quality and valuation insights from historical financial data.", + divider="gray", + ) + + st.markdown('
', unsafe_allow_html=True) + + # Sidebar + st.sidebar.markdown( + """ + + """, + unsafe_allow_html=True, + ) + ticker = st.sidebar.text_input(r"$\textsf{\Large Enter stock symbol:}$") + analyze_button = st.sidebar.button("Analyze") + + if ticker and analyze_button: + # Get data + stock_data = asyncio.run(extract_source_data(ticker)) + profile_data = stock_data["profile"][0] + quote_data = stock_data["quote"][0] + ratings_data = stock_data["ratings"] + key_metrics_ttm_data = stock_data["key_metrics_ttm"][0] + key_metrics_data = stock_data["key_metrics"] + growth_data = stock_data["growth"] + + table_data = [quote_data, key_metrics_ttm_data, growth_data, ratings_data,] + + if stock_data: + + # Display ticker profile + display_profile(profile_data) + + # Display ticker quotes + #st.markdown("#### Key Metrics") + display_quotes(quote_data, profile_data) + # st.markdown('
', unsafe_allow_html=True) + st.divider() + + display_metric_tables(table_data) + + st.divider() + + # st.markdown("#### Ratings") + # display_ratings(ratings_data) + + left, right = st.columns(2) + + with left: + st.markdown("#### Valuation Metrics") + display_metrics_charts(key_metrics_data, key_metrics_ttm_data) + with right: + st.markdown("#### Growth Metrics") + display_growth_charts(growth_data) + + + +if __name__ == "__main__": + main() + + + + + + + + +# analysis = analyze_stock(data) +# st.subheader("Growth Rates") +# for metric, value in analysis.items(): +# st.metric(metric, f"{value:.2%}") + +# quality_stock = is_quality_dividend_growth_stock(analysis) +# st.subheader("Quality Dividend Growth Stock") +# st.write("Yes" if quality_stock else "No") + +# current_price = st.number_input("Enter current stock price:") +# if current_price: +# undervalued = is_undervalued(current_price, analysis) +# st.subheader("Stock Valuation") +# st.write("Undervalued" if undervalued else "Overvalued") + + +# /Users/skyfox/dim-dev/dimPythonProject/dim_projects/stock-valuation-app/src/stock_valuation_app/ui/app.py + + + + + +# Based on the search results, here are the top 6 metrics you can plot on a chart to determine a company valuation, without comparing to other companies: + +# 1. Revenue Growth Rate: This metric shows the percentage increase in revenue over time, indicating the company's growth trajectory[1]. + +# 2. Earnings per Share (EPS): EPS represents the company's profit allocated to each outstanding share of common stock[4]. + +# 3. Price-to-Earnings (P/E) Ratio: While this typically involves comparison, you can plot the P/E ratio over time to see how the market values the company's earnings[1][4]. + +# 4. Enterprise Value-to-EBIT (EV/EBIT) Ratio: This metric compares the company's enterprise value to its earnings before interest and taxes, providing insight into the company's value relative to its operating earnings[1]. + +# 5. Enterprise Value-to-Free Cash Flow (EV/FCF) Ratio: This measures the company's enterprise value relative to its free cash flow, indicating the company's ability to generate excess cash[1]. + +# 6. Net Revenue Retention (NRR): This metric is particularly important for SaaS companies, showing the percentage of recurring revenue retained from existing customers over time[5]. + +# These metrics, when plotted over time, can provide valuable insights into a company's financial health, growth potential, and overall valuation. Remember that while these metrics are useful individually, a comprehensive valuation should consider multiple factors and industry-specific nuances. + +# Citations: +# [1] https://quartr.com/insights/investing/valuation-metrics-estimating-the-true-worth-of-a-company +# [2] https://www.adamsbrowncpa.com/blog/how-investors-evaluate-key-metrics-in-a-business-valuation/ +# [3] https://eqvista.com/business-valuation-metrics/ +# [4] https://www.business-case-analysis.com/valuation.html +# [5] https://www.saasacademy.com/blog/saas-company-valuation-metrics +# [6] https://365financialanalyst.com/knowledge-hub/financial-analysis/valuation-ratios/ +# [7] https://www.reddit.com/r/SecurityAnalysis/comments/kwrg26/what_metrics_do_you_use_to_analyse_highgrowth/ +# [8] https://corporatefinanceinstitute.com/resources/valuation/types-of-valuation-multiples/ + + + + +# import streamlit as st +# import streamlit.components.v1 as components + + +# def create_pe_comparison(label, stock_pe, industry_pe): +# with open("combined_styles.html", "r") as file: +# html_content = file.read() + +# formatted_html = ( +# html_content.replace("{label}", label) +# .replace("{stock_pe}", str(stock_pe)) +# .replace("{industry_pe}", str(industry_pe)) +# ) +# components.html(formatted_html, height=150) + + +# def display_stock_price(label, stock_price): +# with open("combined_styles.html", "r") as file: +# html_content = file.read() + +# formatted_html = html_content.replace("{label}", label).replace( +# "{stock_price}", str(stock_price) +# ) +# components.html(formatted_html, height=120) + + +# # Streamlit app +# def main(): +# st.title("Stock Information Display") +# create_pe_comparison("PE Comparison", 15.6, 18.2) +# display_stock_price("AAPL", 150.25) + + +# if __name__ == "__main__": +# main() + + +# import streamlit as st +# import plotly.graph_objects as go +# import pandas as pd +# from typing import Any, List, Dict + + +# def display_growth_charts(growth_data: List[Dict[str, Any]], ttm_value: float): +# def create_compact_line_chart(data, x_col, y_col, title, ttm_value): +# fig = go.Figure() + +# # Historical data line +# fig.add_trace( +# go.Scatter( +# x=data[x_col], +# y=data[y_col], +# mode="lines+markers", +# line=dict(width=2, color="royalblue"), # Professional blue color +# marker=dict( +# size=8, color="royalblue", line=dict(width=1, color="darkblue") +# ), +# name="Historical Net Income Per Share", +# ) +# ) + +# # TTM value point +# latest_date = data[x_col].iloc[-1] +# fig.add_trace( +# go.Scatter( +# x=[latest_date], +# y=[ttm_value], +# mode="markers+text", +# marker=dict( +# size=12, +# color="firebrick", +# symbol="diamond", +# line=dict(width=2, color="darkred"), +# ), +# name="TTM Net Income Per Share", +# text=[f"{ttm_value:.2f}"], # Display TTM value as text +# textposition="top center", +# ) +# ) + +# # Horizontal line for TTM value +# fig.add_shape( +# type="line", +# x0=data[x_col].iloc[0], +# y0=ttm_value, +# x1=latest_date, +# y1=ttm_value, +# line=dict(color="firebrick", width=2, dash="dash"), +# ) + +# # Update layout for a more professional appearance +# fig.update_layout( +# title=title, +# title_font=dict(size=16, family="Arial", color="black"), +# xaxis_title="Year", +# yaxis_title="Net Income Per Share", +# plot_bgcolor="white", # Clean background +# height=400, +# margin=dict(l=40, r=40, t=40, b=40), +# font=dict(family="Arial", size=12), +# showlegend=True, +# legend=dict( +# orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 +# ), +# hovermode="x unified", # Unified hover for better readability +# ) + +# # Customize axes +# fig.update_xaxes( +# tickmode="array", +# tickvals=data[x_col], +# ticktext=data[x_col], +# gridcolor="lightgrey", +# ) +# fig.update_yaxes(gridcolor="lightgrey") + +# return fig + +# # Create and display the chart +# chart = create_compact_line_chart( +# data=pd.DataFrame(growth_data), +# x_col="FYDateEnding", +# y_col="netIncomePerShare", +# title="Net Income Per Share Growth", +# ttm_value=ttm_value, +# ) + +# st.plotly_chart(chart, use_container_width=True) + + +# # Example usage with container data +# growth_data = [ +# {"FYDateEnding": "2020-12-31", "netIncomePerShare": 3.00}, +# {"FYDateEnding": "2021-12-31", "netIncomePerShare": 4.50}, +# {"FYDateEnding": "2022-12-31", "netIncomePerShare": 5.00}, +# {"FYDateEnding": "2023-12-31", "netIncomePerShare": 6.00}, +# ] +# display_growth_charts(growth_data, ttm_value=5.67) + + + + + + + + + + + + + + + + + + + + +# import streamlit as st +# import plotly.graph_objects as go +# from typing import Any, List, Dict + + +# def display_growth_charts(growth_data: List[Dict[str, Any]], ttm_value: float): +# def create_compact_line_chart(data, x_col, y_col, title, ttm_value): +# fig = go.Figure() + +# # Historical data line +# fig.add_trace( +# go.Scatter( +# x=data[x_col], +# y=data[y_col], +# mode="lines+markers", +# line=dict(width=2, color="blue"), +# marker=dict(size=8), +# name="Historical Net Income Per Share", +# ) +# ) + +# # TTM value point +# latest_date = data[x_col].iloc[-1] +# fig.add_trace( +# go.Scatter( +# x=[latest_date], +# y=[ttm_value], +# mode="markers", +# marker=dict(size=12, color="red", symbol="diamond"), +# name="TTM Net Income Per Share", +# ) +# ) + +# # Horizontal line for TTM value +# fig.add_shape( +# type="line", +# x0=data[x_col].iloc[0], +# y0=ttm_value, +# x1=latest_date, +# y1=ttm_value, +# line=dict(color="red", width=1, dash="dash"), +# ) + +# fig.update_layout( +# title=title, +# xaxis_title="Year", +# yaxis_title="Net Income Per Share", +# plot_bgcolor="lightgrey", +# height=400, # Slightly increased height to accommodate legend +# margin=dict(l=40, r=40, t=40, b=40), +# font=dict(family="Arial", size=12), +# showlegend=True, +# legend=dict( +# orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 +# ), +# ) + +# fig.update_xaxes(tickmode="array", tickvals=data[x_col], ticktext=data[x_col]) +# fig.update_yaxes(gridcolor="white", gridwidth=1) + +# # Add annotation for TTM value +# fig.add_annotation( +# x=latest_date, +# y=ttm_value, +# text=f"TTM: {ttm_value:.2f}", +# showarrow=True, +# arrowhead=2, +# arrowsize=1, +# arrowwidth=2, +# arrowcolor="red", +# ax=40, +# ay=-40, +# ) + +# return fig + +# # Assuming you have the TTM value available +# ttm_value = 5.67 # Replace with actual TTM value + +# # Create and display the chart +# chart = create_compact_line_chart( +# data=pd.DataFrame(growth_data), +# x_col="FYDateEnding", +# y_col="netIncomePerShare", +# title="Net Income Per Share Growth", +# ttm_value=ttm_value, +# ) + +# st.plotly_chart(chart, use_container_width=True) + + +# # Usage +# growth_data = [...] # Your list of dictionaries with historical data +# display_growth_charts(growth_data, ttm_value=5.67) \ No newline at end of file diff --git a/src/stock_valuation_app/ui/style.html b/src/stock_valuation_app/ui/style.html new file mode 100644 index 0000000..3926322 --- /dev/null +++ b/src/stock_valuation_app/ui/style.html @@ -0,0 +1,49 @@ + + + +
+
{label}
+
Stock Price
+
${stock_price}
+
+ + \ No newline at end of file diff --git a/src/utils.py b/src/stock_valuation_app/utils.py similarity index 100% rename from src/utils.py rename to src/stock_valuation_app/utils.py diff --git a/src/ui/app.py b/src/ui/app.py deleted file mode 100644 index f40c26a..0000000 --- a/src/ui/app.py +++ /dev/null @@ -1,44 +0,0 @@ -import asyncio -import streamlit as st -from api.fmp_client import FMPClient -# from services.stock_analysis import ( -# analyze_stock, -# is_quality_dividend_growth_stock, -# is_undervalued, -# ) - -async def main(): - #st.title("Stock Valuation App") - st.title("Stock Valuation Dashboard") - st.subheader("Get stock quality and valuation insights from historical financial data.", divider="gray") - - #api_key = st.secrets["FMP_API_KEY"] - symbol = st.text_input("Enter stock symbol:") - - if st.button("Analyze"): - # Get stock symbol as user input - if symbol: - data = await FMPClient().fetch_data(symbol) - - if data: - analysis = analyze_stock(data) - - st.subheader("Growth Rates") - for metric, value in analysis.items(): - st.metric(metric, f"{value:.2%}") - - quality_stock = is_quality_dividend_growth_stock(analysis) - st.subheader("Quality Dividend Growth Stock") - st.write("Yes" if quality_stock else "No") - - current_price = st.number_input("Enter current stock price:") - if current_price: - undervalued = is_undervalued(current_price, analysis) - st.subheader("Stock Valuation") - st.write("Undervalued" if undervalued else "Overvalued") - else: - st.warning("Please enter a valid stock symbol") - - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/uv.lock b/uv.lock index 8170bb6..0a80861 100644 --- a/uv.lock +++ b/uv.lock @@ -273,20 +273,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", size = 25805 }, ] -[[package]] -name = "fastapi" -version = "0.115.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/db/5781f19bd30745885e0737ff3fdd4e63e7bc691710f9da691128bb0dc73b/fastapi-0.115.4.tar.gz", hash = "sha256:db653475586b091cb8b2fec2ac54a680ac6a158e07406e1abae31679e8826349", size = 300737 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/f6/af0d1f58f86002be0cf1e2665cdd6f7a4a71cdc8a7a9438cdc9e3b5375fe/fastapi-0.115.4-py3-none-any.whl", hash = "sha256:0b504a063ffb3cf96a5e27dc1bc32c80ca743a2528574f9cdc77daa2d31b4742", size = 94732 }, -] - [[package]] name = "gitdb" version = "4.0.11" @@ -762,6 +748,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, ] +[[package]] +name = "plotly" +version = "5.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/4f/428f6d959818d7425a94c190a6b26fbc58035cbef40bf249be0b62a9aedd/plotly-5.24.1.tar.gz", hash = "sha256:dbc8ac8339d248a4bcc36e08a5659bacfe1b079390b8953533f4eb22169b4bae", size = 9479398 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ae/580600f441f6fc05218bd6c9d5794f4aef072a7d9093b291f1c50a9db8bc/plotly-5.24.1-py3-none-any.whl", hash = "sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089", size = 19054220 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -1220,18 +1219,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, ] -[[package]] -name = "starlette" -version = "0.41.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259 }, -] - [[package]] name = "stock-valuation-app" version = "0.1.0" @@ -1239,15 +1226,14 @@ source = { editable = "." } dependencies = [ { name = "asyncio" }, { name = "duckdb" }, - { name = "fastapi" }, { name = "httpx" }, + { name = "pandas" }, + { name = "plotly" }, { name = "polars" }, - { name = "pyarrow" }, { name = "pydantic" }, { name = "python-dotenv" }, { name = "pyyaml" }, { name = "streamlit" }, - { name = "uvicorn" }, ] [package.dev-dependencies] @@ -1262,15 +1248,14 @@ dev = [ requires-dist = [ { name = "asyncio", specifier = ">=3.4.3" }, { name = "duckdb", specifier = ">=1.1.3" }, - { name = "fastapi", specifier = ">=0.115.4" }, { name = "httpx", specifier = ">=0.27.2" }, + { name = "pandas", specifier = ">=2.2.3" }, + { name = "plotly", specifier = ">=5.24.1" }, { name = "polars", specifier = ">=1.12.0" }, - { name = "pyarrow", specifier = ">=18.0.0" }, { name = "pydantic", specifier = ">=2.9.2" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "streamlit", specifier = ">=1.40.1" }, - { name = "uvicorn", specifier = ">=0.32.0" }, ] [package.metadata.requires-dev] @@ -1383,19 +1368,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, ] -[[package]] -name = "uvicorn" -version = "0.32.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/14/78bd0e95dd2444b6caacbca2b730671d4295ccb628ef58b81bee903629df/uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", size = 63723 }, -] - [[package]] name = "watchdog" version = "6.0.0" From 705e0bc2295395f2d164dc2b8764bd6dd6e673c0 Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Tue, 18 Mar 2025 23:54:26 -0600 Subject: [PATCH 09/19] Streamlined the project structure --- .dockerignore | 61 --- Dockerfile | 10 - docker-compose.yml | 8 - pyproject.toml | 23 +- src/{stock_valuation_app => }/api/__init__.py | 0 .../api/fmp_client.py | 36 +- src/{stock_valuation_app/ui => }/app.py | 466 +++--------------- src/config.py | 14 + src/config.yml | 25 - .../models/__init__.py | 0 src/models/stock_models.py | 106 ++++ src/stock_valuation_app/main.py | 51 -- .../models/stock_models.py | 100 ---- src/stock_valuation_app/services/__init__.py | 0 .../services/stock_analysis.py | 161 ------ src/stock_valuation_app/ui/__init__.py | 0 src/stock_valuation_app/ui/style.html | 49 -- src/stock_valuation_app/utils.py | 31 -- src/utils.py | 87 ++++ uv.lock | 122 ++--- 20 files changed, 339 insertions(+), 1011 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Dockerfile delete mode 100644 docker-compose.yml rename src/{stock_valuation_app => }/api/__init__.py (100%) rename src/{stock_valuation_app => }/api/fmp_client.py (68%) rename src/{stock_valuation_app/ui => }/app.py (53%) create mode 100644 src/config.py delete mode 100644 src/config.yml rename src/{stock_valuation_app => }/models/__init__.py (100%) create mode 100644 src/models/stock_models.py delete mode 100644 src/stock_valuation_app/main.py delete mode 100644 src/stock_valuation_app/models/stock_models.py delete mode 100644 src/stock_valuation_app/services/__init__.py delete mode 100644 src/stock_valuation_app/services/stock_analysis.py delete mode 100644 src/stock_valuation_app/ui/__init__.py delete mode 100644 src/stock_valuation_app/ui/style.html delete mode 100644 src/stock_valuation_app/utils.py create mode 100644 src/utils.py diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 07143dc..0000000 --- a/.dockerignore +++ /dev/null @@ -1,61 +0,0 @@ -# Git -.git -.gitignore - -# Python -__pycache__ -*.pyc -*.pyo -*.pyd -.Python -env -venv -pip-log.txt -pip-delete-this-directory.txt -.tox -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.log -.mypy_cache -.pytest_cache -.hypothesis - -# Environments -.env -.venv -env/ -venv/ -ENV/ - -# IDEs -.vscode -.idea - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Project specific -tests/ -*.md -LICENSE -.github/ -docker-compose.yml - -# DuckDB -*.duckdb - -# Jupyter Notebook -.ipynb_checkpoints - -# uv specific -.uv/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ee05652..0000000 --- a/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM python:3.11-slim - -WORKDIR /app - -COPY pyproject.toml uv.lock ./ -RUN pip install uv && uv pip install -e . - -COPY src ./src - -CMD ["uvicorn", "stock_valuation_app:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 2f52801..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: '3' -services: - web: - build: . - ports: - - "8000:8000" - environment: - - FMP_API_KEY=your_api_key_here \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 62ecd69..8802cdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,35 +12,16 @@ dependencies = [ "asyncio>=3.4.3", "duckdb>=1.1.3", "httpx>=0.27.2", - "pandas>=2.2.3", "plotly>=5.24.1", "polars>=1.12.0", - "pydantic>=2.9.2", - "python-dotenv>=1.0.1", - "pyyaml>=6.0.2", + "pydantic-settings>=2.8.1", "streamlit>=1.40.1", ] -[tool.hatch.build.targets.wheel] -packages = ["src/stock_valuation_app"] - -[project.scripts] -stock-valuation-app = "stock_valuation_app:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - [dependency-groups] dev = [ "ipykernel>=6.29.5", "mypy>=1.13.0", "pytest>=8.3.3", "ruff>=0.7.3", -] - -[tool.pytest.ini.options] -pythonpath = "src/stock_valuation_app" - -# [tool.setuptools] -# package-dir = {"" = "src"} +] \ No newline at end of file diff --git a/src/stock_valuation_app/api/__init__.py b/src/api/__init__.py similarity index 100% rename from src/stock_valuation_app/api/__init__.py rename to src/api/__init__.py diff --git a/src/stock_valuation_app/api/fmp_client.py b/src/api/fmp_client.py similarity index 68% rename from src/stock_valuation_app/api/fmp_client.py rename to src/api/fmp_client.py index b558fe5..1715b66 100644 --- a/src/stock_valuation_app/api/fmp_client.py +++ b/src/api/fmp_client.py @@ -1,33 +1,20 @@ -import os import asyncio from dataclasses import dataclass, field from typing import Any -import httpx -import stock_valuation_app.utils as utils -from stock_valuation_app.models.stock_models import CombinedModel - - -def get_endpoint(url: str) -> str: - """Get endpoints from the configuration.""" - api_config = utils.get_section_config("api") - return api_config.get(f"{url}") - - -def get_base_url() -> str: - """Get the base URL from the configuration.""" - return get_endpoint("base_url") +import httpx -def get_api_key() -> str: - """Get the API key from the .env file.""" - return os.getenv("FMP_API_KEY", "") +import utils as utils +from config import settings +from models.stock_models import CombinedModel @dataclass class FMPClient: """A client for interacting with the Financial Modeling Prep API.""" - base_url: str = field(default_factory=get_base_url) - api_key: str = field(default_factory=get_api_key) + + base_url: str = field(default_factory=settings.base_url) + api_key: str = field(default_factory=settings.fmp_api_key) metric_types: list[str] = field( default_factory=lambda: [ "profile", @@ -67,11 +54,14 @@ async def fetch_data(self, ticker: str) -> dict[str, list[dict[str, Any]]]: "rating": "ratings", "key-metrics": "key_metrics", "key-metrics-ttm": "key_metrics_ttm", - "financial-growth": "growth",} # - new_metric_types = [replace_metric_types.get(item, item) for item in self.metric_types] + "financial-growth": "growth", + } # + new_metric_types = [ + replace_metric_types.get(item, item) for item in self.metric_types + ] # Create combined records dict for validation records = dict(zip(new_metric_types, results)) # Validate the combined records - return CombinedModel(**records).model_dump() \ No newline at end of file + return CombinedModel(**records).model_dump() diff --git a/src/stock_valuation_app/ui/app.py b/src/app.py similarity index 53% rename from src/stock_valuation_app/ui/app.py rename to src/app.py index 57adc17..61be857 100644 --- a/src/stock_valuation_app/ui/app.py +++ b/src/app.py @@ -1,13 +1,10 @@ from typing import Any import asyncio -import pandas as pd -import duckdb as db import polars as pl import streamlit as st import streamlit.components.v1 as components import plotly.graph_objects as go -import plotly.express as px -from stock_valuation_app.services.stock_analysis import extract_source_data +from utils import extract_source_data def display_profile(profile_data: dict[str, Any]): @@ -61,11 +58,12 @@ def display_stock_metric(header, value): change_price = f"{quote_data['change_percent']:,.2f}%" year_low = f"${quote_data['year_low']:,.2f}" year_high = f"${quote_data['year_high']:,.2f}" - market_cap = f"{quote_data["market_cap"]/1_000_000_000:.2f}B" - vol_avg = f"{quote_data["vol_avg"]/1_000_000:.2f}M" + market_cap = f"{quote_data['market_cap']/1_000_000_000:.2f}B" + vol_avg = f"{quote_data['vol_avg']/1_000_000:.2f}M" earning_date = quote_data["earning_date"][:10] + eps = f"{quote_data['eps']:.2f}" shares_outstanding = f"{quote_data["shares_outstanding"]/1_000_000_000:.2f}B" - beta = profile_data["beta"] + #beta = profile_data["beta"] col1, col2, col3, col4, col5, col6, col7, col8, col9 = st.columns(9) @@ -82,9 +80,9 @@ def display_stock_metric(header, value): with col6: display_stock_metric("Market Cap", market_cap) with col7: - display_stock_metric("Shares", shares_outstanding) + display_stock_metric("EPS", eps) with col8: - display_stock_metric("Beta", beta) + display_stock_metric("Shares", shares_outstanding) with col9: display_stock_metric("Earning Date", earning_date) except Exception as e: @@ -142,7 +140,7 @@ def display_table(data: dict[str, Any], header_name: str): {generate_table_rows(data)} """ - return components.html(table_html, height=210) #210 + return components.html(table_html, height=185) #210 @@ -153,7 +151,6 @@ def display_table(data: dict[str, Any], header_name: str): valuation_data = { "PE Ratio (TTM)": f"{quote_data['pe']:,.2f}", - "EPS (TTM)": f"{quote_data['eps']:,.2f}", "EV/EBITDA (TTM)": f"{key_metrics_ttm_data['ev_over_ebitda_ttm']:,.2f}", "Price/Sales (TTM)": f"{key_metrics_ttm_data['pts_ratio_ttm']:,.2f}", "Price/Book (TTM)": f"{key_metrics_ttm_data['ptb_ratio_ttm']:,.2f}", @@ -165,11 +162,12 @@ def display_table(data: dict[str, Any], header_name: str): "EV/FCF (TTM)": f"{key_metrics_ttm_data['ev_to_fcf_ttm']:,.2f}", "FCF/Share (TTM)": f"{key_metrics_ttm_data['fcf_per_share_ttm']:,.2f}", } + growth_metric_data = { - "5Y Rev Growth/Share": f"{latest_growth_data['fiveY_rev_growth_per_share']:,.2f}", - "5Y NI Growth/Share": f"{latest_growth_data['fiveY_ni_growth_per_share']:,.2f}", - "5Y Div Growth/Share": f"{latest_growth_data['fiveY_dps_growth_per_share']:,.2f}", - "5Y OCF Growth/Share": f"{latest_growth_data['fiveY_opcf_growth_per_share']:,.2f}", + "5Y Rev Growth/Share": f"{latest_growth_data['fiveY_rev_growth_per_share'] * 100:,.2f}%", + "5Y NI Growth/Share": f"{latest_growth_data['fiveY_ni_growth_per_share'] * 100:,.2f}%", + "5Y Div Growth/Share": f"{latest_growth_data['fiveY_dps_growth_per_share'] * 100:,.2f}%", + "5Y OCF Growth/Share": f"{latest_growth_data['fiveY_opcf_growth_per_share'] * 100:,.2f}%", } dividend_data = { @@ -190,65 +188,24 @@ def display_table(data: dict[str, Any], header_name: str): container = st.empty() col1, col2, col3, col4, col5 = st.columns(5) with col1: - container.markdown( - display_table(valuation_data, "Valuation"), unsafe_allow_html=True - ) - with col2: - container.markdown( - display_table(freecashflow_data, "Cash Flow"), unsafe_allow_html=True - ) - with col3: - container.markdown( - display_table(growth_metric_data, "Growth"), unsafe_allow_html=True - ) - with col4: - container.markdown( - display_table(dividend_data, "Dividend"), unsafe_allow_html=True - ) - with col5: - container.markdown( - display_table(rating_data, "Rating"), unsafe_allow_html=True - ) - - -# def display_ratings(ratings_data: list[dict[str, Any]]): -# """Displays rating dataset""" + container.markdown(display_table(valuation_data, "Valuation"), unsafe_allow_html=True) + container.empty() -# dfx = pl.DataFrame(ratings_data).cast(pl.String()) - -# if dfx is None: -# print("Dataframe is empty") -# return None - -# ndf = db.sql("""SELECT symbol AS Symbol, date AS Date, rating AS Rating, score AS Score, -# recommendation AS Recommendation, CONCAT(dcf_score, ' ', '(', dcf_rec, ')') AS 'Discounted Cash Flow', -# CONCAT(roe_score, ' ', '(', roe_rec, ')') AS 'Return on Equity', -# CONCAT(roa_score, ' ', '(', roa_rec, ')') AS 'Return on Assets', -# CONCAT(de_score, ' ', '(', de_rec, ')') AS 'Debt-to-Equity', -# CONCAT(pe_score, ' ', '(', pe_rec, ')') AS 'Price-to-Earnings', -# CONCAT(pb_score, ' ', '(', pb_rec, ')') AS 'Price-to-Book' -# FROM dfx -# """).df() + with col2: + container.markdown(display_table(freecashflow_data, "Cash Flow"), unsafe_allow_html=True) + container.empty() -# # Define custom CSS for larger text and center alignment -# custom_css = """ -# -# """ + with col3: + container.markdown(display_table(growth_metric_data, "Growth"), unsafe_allow_html=True) + container.empty() -# # Apply styling to the DataFrame -# styled_df = ndf.set_index("Symbol").style.set_properties(**{'text-align': 'center'}) + with col4: + container.markdown(display_table(dividend_data, "Dividend"), unsafe_allow_html=True) + container.empty() -# # Display the custom CSS and the styled DataFrame -# st.markdown(custom_css, unsafe_allow_html=True) -# return st.dataframe(styled_df, use_container_width=True) + with col5: + container.markdown(display_table(rating_data, "Rating"), unsafe_allow_html=True) + container.empty() @@ -436,13 +393,14 @@ def create_compact_line_chart(data: pl.DataFrame, x_col, y_col, title): df = ( pl.DataFrame(growth_data) .with_columns( - pl.col("date") - .str.strptime(pl.Date, format="%Y-%m-%d") - .alias("Year") + pl.exclude(["symbol", "date"]).map_elements( + lambda x: round(x * 100, 2), return_dtype=pl.Float64 + ), + pl.col("date").str.strptime(pl.Date, format="%Y-%m-%d").alias("Year"), ) .sort("date") .drop("date") - ) + )# pl.col("col_name").list.eval(pl.element().sqrt()). if df is None: print("Dataframe is empty") @@ -495,16 +453,18 @@ def main(): layout="wide", ) - st.title("Stock Valuation Dashboard") - #st.divider() - st.subheader( - "Get stock quality and valuation insights from historical financial data.", - divider="gray", - ) - - st.markdown('
', unsafe_allow_html=True) + # Custom CSS + st.markdown(""" + + """, unsafe_allow_html=True) - # Sidebar st.sidebar.markdown( """ -
-
{label}
-
Stock Price
-
${stock_price}
-
- """ - st.html(html_content) - - -# Streamlit app -def main(): - st.title("Stock Price Display") - display_stock_price("AAPL", 150.25) - - -if __name__ == "__main__": - main() diff --git a/src/stock_valuation_app/models/stock_models.py b/src/stock_valuation_app/models/stock_models.py deleted file mode 100644 index 0db419b..0000000 --- a/src/stock_valuation_app/models/stock_models.py +++ /dev/null @@ -1,100 +0,0 @@ -from typing import Optional -from pydantic import BaseModel, Field - - -class CompanyProfile(BaseModel): - symbol: str - beta: float - range: str - company_name: str = Field(..., alias="companyName") - sector: Optional[str] = None - industry: Optional[str] = None - description: Optional[str] = None - image: Optional[str] = None - - -class Quote(BaseModel): - symbol: str - price: float - change_percent: Optional[float] = Field(..., alias="changesPercentage") - year_high: float = Field(..., alias="yearHigh") - year_low: float = Field(..., alias="yearLow") - market_cap: float = Field(..., alias="marketCap") - vol_avg: int = Field(..., alias="avgVolume") - eps: float - pe: float - earning_date: str = Field(..., alias="earningsAnnouncement") - shares_outstanding: int = Field(..., alias="sharesOutstanding") - - - -class Ratings(BaseModel): - symbol: str - date: str - rating: str - score: int = Field(..., alias="ratingScore") - recommendation: str = Field(..., alias="ratingRecommendation") - dcf_score: int = Field(..., alias="ratingDetailsDCFScore") - dcf_rec: str = Field(..., alias="ratingDetailsDCFRecommendation") - roe_score: int = Field(..., alias="ratingDetailsROEScore") - roe_rec: str = Field(..., alias="ratingDetailsROERecommendation") - roa_score: int = Field(..., alias="ratingDetailsROAScore") - roa_rec: str = Field(..., alias="ratingDetailsROARecommendation") - de_score: int = Field(..., alias="ratingDetailsDEScore") - de_rec: str = Field(..., alias="ratingDetailsDERecommendation") - pe_score: int = Field(..., alias="ratingDetailsPEScore") - pe_rec: str = Field(..., alias="ratingDetailsPERecommendation") - pb_score: int = Field(..., alias="ratingDetailsPBScore") - pb_rec: str = Field(..., alias="ratingDetailsPBRecommendation") - - -class KeyMetricsTTM(BaseModel): - rev_per_share_ttm: float = Field(..., alias="revenuePerShareTTM") - net_income_per_share_ttm: float = Field(..., alias="netIncomePerShareTTM") - fcf_per_share_ttm: float = Field(..., alias="freeCashFlowPerShareTTM") - pe_ratio_ttm: float = Field(..., alias="peRatioTTM") - ev_over_ebitda_ttm: float = Field(..., alias="enterpriseValueOverEBITDATTM") - ev_to_fcf_ttm: float = Field(..., alias="evToFreeCashFlowTTM") - fcf_yield_ttm: float = Field(..., alias="freeCashFlowYieldTTM") - pts_ratio_ttm: float = Field(..., alias="priceToSalesRatioTTM") - ptb_ratio_ttm: float = Field(..., alias="ptbRatioTTM") - pfcf_ratio_ttm: float = Field(..., alias="pfcfRatioTTM") - dvd_yield_pct_ttm: float = Field(..., alias="dividendYieldPercentageTTM") - dvd_per_share_ttm: float = Field(..., alias="dividendPerShareTTM") - payout_ratio_ttm: float = Field(..., alias="payoutRatioTTM") - - -class KeyMetrics(BaseModel): - symbol: str - date: str - rev_per_share: float = Field(..., alias="revenuePerShare") - fcf_per_share: float = Field(..., alias="freeCashFlowPerShare") - pe_ratio: float = Field(..., alias="peRatio") - ev_over_ebitda: float = Field(..., alias="enterpriseValueOverEBITDA") - ev_to_fcf: float = Field(..., alias="evToFreeCashFlow") - fcf_yield: float = Field(..., alias="freeCashFlowYield") - - -class Growth(BaseModel): - symbol: str - date: str - rev_growth: float = Field(..., alias="revenueGrowth") - eps_growth: float = Field(..., alias="epsdilutedGrowth") - dps_growth: float = Field(..., alias="dividendsperShareGrowth") - fcf_growth: float = Field(..., alias="freeCashFlowGrowth") - debt_growth: float = Field(..., alias="debtGrowth") - fiveY_rev_growth_per_share: float = Field(..., alias="fiveYRevenueGrowthPerShare") - fiveY_ni_growth_per_share: float = Field(..., alias="fiveYNetIncomeGrowthPerShare") - fiveY_dps_growth_per_share: float = Field(..., alias="fiveYDividendperShareGrowthPerShare") - fiveY_opcf_growth_per_share: float = Field(..., alias="fiveYOperatingCFGrowthPerShare") - - - -class CombinedModel(BaseModel): - profile: list[CompanyProfile] - quote: list[Quote] - ratings: list[Ratings] - key_metrics_ttm: list[KeyMetricsTTM] - key_metrics: list[KeyMetrics] - growth: list[Growth] - diff --git a/src/stock_valuation_app/services/__init__.py b/src/stock_valuation_app/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/stock_valuation_app/services/stock_analysis.py b/src/stock_valuation_app/services/stock_analysis.py deleted file mode 100644 index c51b87d..0000000 --- a/src/stock_valuation_app/services/stock_analysis.py +++ /dev/null @@ -1,161 +0,0 @@ -import asyncio -import duckdb as db -from typing import Any, Optional -import polars as pl -import streamlit as st -from stock_valuation_app.api.fmp_client import FMPClient - - - -# async def extract_source_data(ticker: str): -# source_data = await FMPClient().fetch_data(ticker) -# return source_data - - -# raw_data = asyncio.run(extract_source_data("PAYS")) - -# profile_df = pl.DataFrame(raw_data["profile"]) - -StockData = dict[str, list[dict[str, Any]]] - -#@st.cache_resource -async def extract_source_data(ticker: str) -> StockData: - """Pull source data for a given ticker""" - source_data = await FMPClient().fetch_data(ticker) - return source_data - - -def get_long_df(df: pl.DataFrame, var: str, value: str) -> pl.DataFrame: - """Convert wide dataframe to long format""" - longdf = df.unpivot(variable_name=var, value_name=value) - return longdf - - -#@st.cache_data -def transform_profile(stock_data: StockData): - """Processes profile dataset""" - df = pl.DataFrame(stock_data.get("profile", None)).cast(pl.String()) - - if df is None: - print("Dataframe is empty") - return None - - ndf = db.sql("""WITH ref_data AS (SELECT symbol AS Symbol, price AS Price, beta AS Beta, - vol_avg AS 'Average Volume', mkt_cap AS 'Market Cap', last_div AS 'Last Dividend', - low_high AS '52w Low - High', price_change AS 'Price Change', currency AS Currency, - exchange AS Exchange, sector AS Sector, industry AS Industry, description AS Description - FROM df - ) - UNPIVOT ref_data - ON COLUMNS(*) - INTO - NAME metric - VALUE values - """ - ).pl() - result_dict = dict(zip(ndf["metric"], ndf["values"])) - return result_dict - - -# @st.cache_data -def transform_rating(stock_data: StockData): - """Processes rating dataset""" - df = pl.DataFrame(stock_data.get("ratings", None)).cast(pl.String()) - - if df is None: - print("Dataframe is empty") - return None - - ndf = db.sql("""WITH ref_data AS (SELECT symbol AS Symbol, date AS Date, rating AS Rating, score AS Score, - recommendation AS Recommendation, dcf_score AS 'DCF Score', dcf_rec AS 'DCF Recommendation', - roe_score AS 'ROE Score', roe_rec AS 'ROE Recommendation', roa_score AS 'ROA Score', - roa_rec AS 'ROA Recommendation', de_score AS 'DE Score', de_rec AS 'DE Recommendation', - pe_score AS 'PE Score', pe_rec AS 'PE Recommendation', pb_score AS 'PB Score', - pb_rec AS 'PB Recommendation' - FROM df - ) - UNPIVOT ref_data - ON COLUMNS(*) - INTO - NAME 'Metric' - VALUE Rating - """ - ).pl() - return ndf - -#@st.cache_data -def transform_ratios(stock_data: StockData): - """Processes ratios dataset""" - - df = pl.DataFrame(stock_data.get("ratios", None)).cast(pl.String()) - - # if df is None: - # print("Dataframe is empty") - # return None - - xdf = db.sql("""WITH ref_data AS (SELECT * EXCLUDE (symbol, pb_ratio, curr_ratio) FROM df) UNPIVOT ref_data ON COLUMNS(* EXCLUDE year) INTO NAME metric VALUE values; """).pl() - return xdf - - # if not df.is_empty(): - # newdf = df.select(pl.exclude(["symbol", "book_val_per_share"])) - # val_var = newdf.select(pl.exclude(["year"])).columns - # idx = newdf.select(pl.col("year")).columns - # long_newdf = newdf.unpivot(on=val_var, index=idx, variable_name="Metric", value_name="Value") - # return (long_newdf, val_var) - - - - -# data = asyncio.run(extract_source_data('NVDA')) -# xdf = transform_ratios(data) -# # # print(xdf.schema) -# # # print() -# print(xdf.head(10)) -# # xdf, val_var = transform_ratios(data) -# # print(xdf.head()) -# # print(val_var) - -# df = pl.DataFrame(data.get("ratios", None)) -# print(df.lazy().collect().schema) - -# profile_df = pl.DataFrame(raw_data["profile"]) -# rating_df = pl.DataFrame(raw_data["rating"]) -# metric_df = pl.DataFrame(raw_data["key_metrics"]) -# growth_df = pl.DataFrame(raw_data["growth"]) - -# # Pivot the DataFrame -# pivoted_df = profile_df.unpivot(variable_name="Column", value_name="Value") -# # Sort the result to maintain the original order -# pivoted_df = pivoted_df.sort("Column") - -# # Convert DataFrame to formatted text -# formatted_text = [] -# for row in pivoted_df.iter_rows(): -# column, value = row -# formatted_text.append(html.Div([html.Strong(f"{column}: "), f"{value}"])) - -# async def extract_source_data(ticker: str): -# source_data = await FMPClient().fetch_data(ticker) -# return { -# "profile": source_data["profile"], -# "rating": source_data["rating"], -# "key_metrics": source_data["key_metrics"], -# "growth": source_data["growth"], -# } - -# async def extract_source_data(ticker: str): -# source_data = await FMPClient().fetch_data(ticker) -# return source_data - - -# if __name__ == "__main__": -# pass - # raw_data = asyncio.run(extract_source_data('PAYS')) - - # def get_dataframes(): - # """Captures source dataframes""" - # profile_df = pl.DataFrame(raw_data["profile"]) - # rating_df = pl.DataFrame(raw_data["rating"]) - # metric_df = pl.DataFrame(raw_data["key_metrics"]) - # growth_df = pl.DataFrame(raw_data["growth"]) - # return (profile_df, rating_df, metric_df, growth_df) diff --git a/src/stock_valuation_app/ui/__init__.py b/src/stock_valuation_app/ui/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/stock_valuation_app/ui/style.html b/src/stock_valuation_app/ui/style.html deleted file mode 100644 index 3926322..0000000 --- a/src/stock_valuation_app/ui/style.html +++ /dev/null @@ -1,49 +0,0 @@ - - - -
-
{label}
-
Stock Price
-
${stock_price}
-
- - \ No newline at end of file diff --git a/src/stock_valuation_app/utils.py b/src/stock_valuation_app/utils.py deleted file mode 100644 index 9294b09..0000000 --- a/src/stock_valuation_app/utils.py +++ /dev/null @@ -1,31 +0,0 @@ -from pathlib import Path - -import yaml -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - - -def load_config(): - """Load configuration from a YAML file.""" - with open(Path("src/config.yml").absolute(), "r", encoding="utf-8") as file: - config = yaml.safe_load(file) - return config - - -def get_section_config(section: str): - """Get configuration for a specific section.""" - match section: - case "api": - return load_config().get("api") - case "valuation": - return load_config().get("valuation") - case "dashboard": - return load_config().get("dashboard") - case "database": - return load_config().get("database") - case "logging": - return load_config().get("logging") - case _: - raise ValueError(f"Invalid section: {section}") diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..57ded78 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,87 @@ +import duckdb as db +from typing import Any +import polars as pl +from api.fmp_client import FMPClient + + +StockData = dict[str, list[dict[str, Any]]] + + +# @st.cache_resource +async def extract_source_data(ticker: str) -> StockData: + """Pull source data for a given ticker""" + source_data = await FMPClient().fetch_data(ticker) + return source_data + + +def get_long_df(df: pl.DataFrame, var: str, value: str) -> pl.DataFrame: + """Convert wide dataframe to long format""" + longdf = df.unpivot(variable_name=var, value_name=value) + return longdf + + +# @st.cache_data +def transform_profile(stock_data: StockData): + """Processes profile dataset""" + df = pl.DataFrame(stock_data.get("profile", None)).cast(pl.String()) + + if df is None: + print("Dataframe is empty") + return None + + ndf = db.sql("""WITH ref_data AS (SELECT symbol AS Symbol, price AS Price, beta AS Beta, + vol_avg AS 'Average Volume', mkt_cap AS 'Market Cap', last_div AS 'Last Dividend', + low_high AS '52w Low - High', price_change AS 'Price Change', currency AS Currency, + exchange AS Exchange, sector AS Sector, industry AS Industry, description AS Description + FROM df + ) + UNPIVOT ref_data + ON COLUMNS(*) + INTO + NAME metric + VALUE values + """).pl() + result_dict = dict(zip(ndf["metric"], ndf["values"])) + return result_dict + + +# @st.cache_data +def transform_rating(stock_data: StockData): + """Processes rating dataset""" + df = pl.DataFrame(stock_data.get("ratings", None)).cast(pl.String()) + + if df is None: + print("Dataframe is empty") + return None + + ndf = db.sql("""WITH ref_data AS (SELECT symbol AS Symbol, date AS Date, rating AS Rating, score AS Score, + recommendation AS Recommendation, dcf_score AS 'DCF Score', dcf_rec AS 'DCF Recommendation', + roe_score AS 'ROE Score', roe_rec AS 'ROE Recommendation', roa_score AS 'ROA Score', + roa_rec AS 'ROA Recommendation', de_score AS 'DE Score', de_rec AS 'DE Recommendation', + pe_score AS 'PE Score', pe_rec AS 'PE Recommendation', pb_score AS 'PB Score', + pb_rec AS 'PB Recommendation' + FROM df + ) + UNPIVOT ref_data + ON COLUMNS(*) + INTO + NAME 'Metric' + VALUE Rating + """).pl() + return ndf + + +# @st.cache_data +def transform_ratios(stock_data: StockData): + """Processes ratios dataset""" + + df = pl.DataFrame(stock_data.get("ratios", None)).cast(pl.String()) + + # if df is None: + # print("Dataframe is empty") + # return None + + xdf = db.sql( + """WITH ref_data AS (SELECT * EXCLUDE (symbol, pb_ratio, curr_ratio) FROM df) UNPIVOT ref_data ON COLUMNS(* EXCLUDE year) INTO NAME metric VALUE values; """ + ).pl() + return xdf diff --git a/uv.lock b/uv.lock index 0a80861..5f2eda7 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.12" resolution-markers = [ "python_full_version < '3.13'", @@ -186,7 +187,7 @@ name = "click" version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ @@ -358,7 +359,7 @@ name = "ipykernel" version = "6.29.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "appnope", marker = "platform_system == 'Darwin'" }, + { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, { name = "ipython" }, @@ -881,51 +882,68 @@ wheels = [ [[package]] name = "pydantic" -version = "2.9.2" +version = "2.10.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, ] [[package]] name = "pydantic-core" -version = "2.23.4" +version = "2.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, - { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, - { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, - { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, - { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, - { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, - { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, - { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, - { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, - { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, - { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, - { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, - { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, - { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, - { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, - { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, - { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, - { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, - { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, - { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, - { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, - { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, - { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, - { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, ] [[package]] @@ -1008,32 +1026,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, ] -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, -] - [[package]] name = "pyzmq" version = "26.2.0" @@ -1222,17 +1214,14 @@ wheels = [ [[package]] name = "stock-valuation-app" version = "0.1.0" -source = { editable = "." } +source = { virtual = "." } dependencies = [ { name = "asyncio" }, { name = "duckdb" }, { name = "httpx" }, - { name = "pandas" }, { name = "plotly" }, { name = "polars" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, + { name = "pydantic-settings" }, { name = "streamlit" }, ] @@ -1249,12 +1238,9 @@ requires-dist = [ { name = "asyncio", specifier = ">=3.4.3" }, { name = "duckdb", specifier = ">=1.1.3" }, { name = "httpx", specifier = ">=0.27.2" }, - { name = "pandas", specifier = ">=2.2.3" }, { name = "plotly", specifier = ">=5.24.1" }, { name = "polars", specifier = ">=1.12.0" }, - { name = "pydantic", specifier = ">=2.9.2" }, - { name = "python-dotenv", specifier = ">=1.0.1" }, - { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "pydantic-settings", specifier = ">=2.8.1" }, { name = "streamlit", specifier = ">=1.40.1" }, ] @@ -1289,7 +1275,7 @@ dependencies = [ { name = "toml" }, { name = "tornado" }, { name = "typing-extensions" }, - { name = "watchdog", marker = "platform_system != 'Darwin'" }, + { name = "watchdog", marker = "sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/80/70/b76a32201b04a5a2a1c667fe7327cb7a3cc25d726d3863ba5863d2b0dccf/streamlit-1.40.1.tar.gz", hash = "sha256:1f2b09f04b6ad366a2c7b4d48104697d1c8bc33f48bdf7ed939cc04c12d3aec6", size = 8266452 } wheels = [ From 79dfab1d265827f2933a0bdaf707e55733269585 Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Sun, 23 Mar 2025 09:49:50 -0600 Subject: [PATCH 10/19] Added data_validation module --- .gitignore | 5 +- src/api/__init__.py | 0 src/api/fmp_client.py | 67 -------------- src/app.py | 154 +++++++++++++++++++------------ src/data_validation.py | 55 +++++++++++ src/models/__init__.py | 0 src/{models => }/stock_models.py | 0 src/utils.py | 140 ++++++++++++---------------- 8 files changed, 212 insertions(+), 209 deletions(-) delete mode 100644 src/api/__init__.py delete mode 100644 src/api/fmp_client.py create mode 100644 src/data_validation.py delete mode 100644 src/models/__init__.py rename src/{models => }/stock_models.py (100%) diff --git a/.gitignore b/.gitignore index 596ccf0..4144a1e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,7 @@ htmlcov/ .uv/ # DuckDB -*.duckdb \ No newline at end of file +*.duckdb + +# Scratch Pad +scratch.py \ No newline at end of file diff --git a/src/api/__init__.py b/src/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/api/fmp_client.py b/src/api/fmp_client.py deleted file mode 100644 index 1715b66..0000000 --- a/src/api/fmp_client.py +++ /dev/null @@ -1,67 +0,0 @@ -import asyncio -from dataclasses import dataclass, field -from typing import Any - -import httpx - -import utils as utils -from config import settings -from models.stock_models import CombinedModel - - -@dataclass -class FMPClient: - """A client for interacting with the Financial Modeling Prep API.""" - - base_url: str = field(default_factory=settings.base_url) - api_key: str = field(default_factory=settings.fmp_api_key) - metric_types: list[str] = field( - default_factory=lambda: [ - "profile", - "rating", - "quote", - "key-metrics-ttm", - "key-metrics", - "financial-growth", - ] - ) # - - async def get_data(self, client: httpx.Client, url: str) -> dict[str, Any]: - """Call API endpoint asynchronously""" - response = await client.get(url) - data = response.json() - return data - - async def fetch_data(self, ticker: str) -> dict[str, list[dict[str, Any]]]: - """Extracts data asynchronously from multiple FMP endpoints""" - urls = [] - for metric in self.metric_types: - if metric in ["profile", "quote", "rating", "key-metrics-ttm"]: - endpoint = f"{self.base_url}/{metric}/{ticker}?apikey={self.api_key}" - else: - endpoint = f"{self.base_url}/{metric}/{ticker}?period=annual&apikey={self.api_key}" - urls.append(endpoint) - - async with httpx.AsyncClient() as client: - tasks = [] - for url in urls: - tasks.append(asyncio.create_task(self.get_data(client, url))) - - results = await asyncio.gather(*tasks) - - # Rename some metric types to match with fields defined in the CombinedModel - replace_metric_types = { - "rating": "ratings", - "key-metrics": "key_metrics", - "key-metrics-ttm": "key_metrics_ttm", - "financial-growth": "growth", - } # - new_metric_types = [ - replace_metric_types.get(item, item) for item in self.metric_types - ] - - # Create combined records dict for validation - records = dict(zip(new_metric_types, results)) - - # Validate the combined records - return CombinedModel(**records).model_dump() diff --git a/src/app.py b/src/app.py index 61be857..1b835b4 100644 --- a/src/app.py +++ b/src/app.py @@ -1,14 +1,13 @@ from typing import Any -import asyncio import polars as pl import streamlit as st import streamlit.components.v1 as components import plotly.graph_objects as go -from utils import extract_source_data +from data_validation import get_validated_stock_data -def display_profile(profile_data: dict[str, Any]): - """Display company profile in sidebar""" +def display_profile(profile_data: dict[str, Any]) -> None: + """Displays company profile in sidebar""" try: if profile_data: st.sidebar.write("## Company Profile") @@ -21,9 +20,12 @@ def display_profile(profile_data: dict[str, Any]): st.write("Couldn't find company profile.") -def display_quotes(quote_data: dict[str, Any], profile_data: dict[str, Any]): +def display_quotes(quote_data: dict[str, Any], profile_data: dict[str, Any]) -> None: + """Displays company quotes in main area""" + + def display_stock_metric(header, value) -> None: + """Displays quotes in capsule at the top of the page""" - def display_stock_metric(header, value): html_content = f""" - """, unsafe_allow_html=True) + """, + unsafe_allow_html=True, + ) st.sidebar.markdown( - """ + """ """, - unsafe_allow_html=True, - ) - + unsafe_allow_html=True, + ) st.title("Stock Valuation Dashboard") - #st.divider() + # st.divider() st.subheader( "Get stock quality and valuation insights from historical financial data.", divider="gray", @@ -493,7 +520,7 @@ def main(): if ticker and analyze_button: # Get data - stock_data = asyncio.run(extract_source_data(ticker)) + stock_data = get_validated_stock_data(ticker) profile_data = stock_data["profile"][0] quote_data = stock_data["quote"][0] ratings_data = stock_data["ratings"] @@ -501,15 +528,19 @@ def main(): key_metrics_data = stock_data["key_metrics"] growth_data = stock_data["growth"] - table_data = [quote_data, key_metrics_ttm_data, growth_data, ratings_data,] + table_data = [ + quote_data, + key_metrics_ttm_data, + growth_data, + ratings_data, + ] if stock_data: - # Display ticker profile display_profile(profile_data) # Display ticker quotes - #st.markdown("#### Key Metrics") + # st.markdown("#### Key Metrics") display_quotes(quote_data, profile_data) st.markdown('
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) @@ -521,15 +552,20 @@ def main(): left, right = st.columns(2) with left: - st.markdown("""

Valuation Metrics

""", unsafe_allow_html=True) + st.markdown( + """

Valuation Metrics

""", + unsafe_allow_html=True, + ) display_metrics_charts(key_metrics_data, key_metrics_ttm_data) with right: - st.markdown("""

Growth Metrics

""", unsafe_allow_html=True) + st.markdown( + """

Growth Metrics

""", + unsafe_allow_html=True, + ) display_growth_charts(growth_data) st.divider() - if __name__ == "__main__": main() diff --git a/src/data_validation.py b/src/data_validation.py new file mode 100644 index 0000000..1012570 --- /dev/null +++ b/src/data_validation.py @@ -0,0 +1,55 @@ +# data_validation.py + +from pydantic import ValidationError +from typing import Any +from stock_models import CombinedModel +from utils import StockData, FMPClient +import asyncio + + +async def extract_stock_data(ticker: str) -> StockData: + """Pulls source data from Financial Modeling Prep (FMP) API endpoints for a given ticker""" + # Get stock data + stock_data = await FMPClient().fetch_data(ticker) + + # Get metric types + metric_types = FMPClient().metric_types + + # Rename some metric types to match with fields defined in the validation + rename_metric_types: dict[Any, Any] = { + "rating": "ratings", + "key-metrics": "key_metrics", + "key-metrics-ttm": "key_metrics_ttm", + "financial-growth": "growth", + } + new_metric_types = [rename_metric_types.get(item, item) for item in metric_types] + + # Create combined records dict + records = dict(zip(new_metric_types, stock_data)) + return records + + +class DataValidationError(Exception): + """Custom exception for validation error""" + + +def get_validated_stock_data(ticker: str) -> StockData: + """Validates stock data against the CombinedModel schema""" + + # Keep validation errors here + errors: list[str] = [] + + # Extract stock data + data = asyncio.run(extract_stock_data(ticker)) + + # Validate stock data + try: + validated_data = CombinedModel(**data).model_dump() + except ValidationError as e: + errors.append(f"Failed validation: {str(e)}") + if errors: + error_message = "\n".join(errors) + raise DataValidationError( + f"Data validation failed with following errors: \n{error_message}" + ) + return validated_data diff --git a/src/models/__init__.py b/src/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/models/stock_models.py b/src/stock_models.py similarity index 100% rename from src/models/stock_models.py rename to src/stock_models.py diff --git a/src/utils.py b/src/utils.py index 57ded78..af3aa11 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,87 +1,63 @@ -import duckdb as db +import sys +import logging from typing import Any -import polars as pl -from api.fmp_client import FMPClient - +import asyncio +from dataclasses import dataclass, field +import httpx +from config import settings +# Define stock data type StockData = dict[str, list[dict[str, Any]]] -# @st.cache_resource -async def extract_source_data(ticker: str) -> StockData: - """Pull source data for a given ticker""" - source_data = await FMPClient().fetch_data(ticker) - return source_data - - -def get_long_df(df: pl.DataFrame, var: str, value: str) -> pl.DataFrame: - """Convert wide dataframe to long format""" - longdf = df.unpivot(variable_name=var, value_name=value) - return longdf - - -# @st.cache_data -def transform_profile(stock_data: StockData): - """Processes profile dataset""" - df = pl.DataFrame(stock_data.get("profile", None)).cast(pl.String()) - - if df is None: - print("Dataframe is empty") - return None - - ndf = db.sql("""WITH ref_data AS (SELECT symbol AS Symbol, price AS Price, beta AS Beta, - vol_avg AS 'Average Volume', mkt_cap AS 'Market Cap', last_div AS 'Last Dividend', - low_high AS '52w Low - High', price_change AS 'Price Change', currency AS Currency, - exchange AS Exchange, sector AS Sector, industry AS Industry, description AS Description - FROM df - ) - UNPIVOT ref_data - ON COLUMNS(*) - INTO - NAME metric - VALUE values - """).pl() - result_dict = dict(zip(ndf["metric"], ndf["values"])) - return result_dict - - -# @st.cache_data -def transform_rating(stock_data: StockData): - """Processes rating dataset""" - df = pl.DataFrame(stock_data.get("ratings", None)).cast(pl.String()) - - if df is None: - print("Dataframe is empty") - return None - - ndf = db.sql("""WITH ref_data AS (SELECT symbol AS Symbol, date AS Date, rating AS Rating, score AS Score, - recommendation AS Recommendation, dcf_score AS 'DCF Score', dcf_rec AS 'DCF Recommendation', - roe_score AS 'ROE Score', roe_rec AS 'ROE Recommendation', roa_score AS 'ROA Score', - roa_rec AS 'ROA Recommendation', de_score AS 'DE Score', de_rec AS 'DE Recommendation', - pe_score AS 'PE Score', pe_rec AS 'PE Recommendation', pb_score AS 'PB Score', - pb_rec AS 'PB Recommendation' - FROM df - ) - UNPIVOT ref_data - ON COLUMNS(*) - INTO - NAME 'Metric' - VALUE Rating - """).pl() - return ndf - - -# @st.cache_data -def transform_ratios(stock_data: StockData): - """Processes ratios dataset""" - - df = pl.DataFrame(stock_data.get("ratios", None)).cast(pl.String()) - - # if df is None: - # print("Dataframe is empty") - # return None - - xdf = db.sql( - """WITH ref_data AS (SELECT * EXCLUDE (symbol, pb_ratio, curr_ratio) FROM df) UNPIVOT ref_data ON COLUMNS(* EXCLUDE year) INTO NAME metric VALUE values; """ - ).pl() - return xdf +def stock_logger(): + """Configures logging for stock data dashboard project""" + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + stream=sys.stdout, + force=True, # Ensures unbuffered output + ) + return logging.getLogger(__name__) + + +@dataclass +class FMPClient: + """A client for interacting with the Financial Modeling Prep API.""" + + base_url: str = settings.base_url + api_key: str = settings.fmp_api_key + metric_types: list[str] = field( + default_factory=lambda: [ + "profile", + "rating", + "quote", + "key-metrics-ttm", + "key-metrics", + "financial-growth", + ] + ) # + + async def get_data(self, client: httpx.Client, url: str) -> dict[str, Any]: + """Call API endpoint asynchronously""" + response = await client.get(url) + data = response.json() + return data + + async def fetch_data(self, ticker: str) -> dict[str, list[dict[str, Any]]]: + """Extracts data asynchronously from multiple FMP endpoints""" + urls = [] + for metric in self.metric_types: + if metric in ["profile", "quote", "rating", "key-metrics-ttm"]: + endpoint = f"{self.base_url}/{metric}/{ticker}?apikey={self.api_key}" + else: + endpoint = f"{self.base_url}/{metric}/{ticker}?period=annual&apikey={self.api_key}" + urls.append(endpoint) + + async with httpx.AsyncClient() as client: + tasks = [] + for url in urls: + tasks.append(asyncio.create_task(self.get_data(client, url))) + results = await asyncio.gather(*tasks) + return results From 0be74fd49113d6f47f0ba2ddc626ecf22157a8bf Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Sun, 23 Mar 2025 18:00:02 -0600 Subject: [PATCH 11/19] Changed line to bar charts --- src/app.py | 201 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 144 insertions(+), 57 deletions(-) diff --git a/src/app.py b/src/app.py index 1b835b4..ca7772d 100644 --- a/src/app.py +++ b/src/app.py @@ -1,8 +1,10 @@ from typing import Any + +import plotly.graph_objects as go import polars as pl import streamlit as st import streamlit.components.v1 as components -import plotly.graph_objects as go + from data_validation import get_validated_stock_data @@ -176,7 +178,6 @@ def display_table(data: dict[str, Any], header_name: str): } rating_data = { - # "Symbol": f"{latest_ratings_data['symbol']}", "Date": f"{latest_ratings_data['date']}", "Rating": f"{latest_ratings_data['rating']}", "Score": f"{latest_ratings_data['score']}", @@ -218,24 +219,26 @@ def display_table(data: dict[str, Any], header_name: str): def display_metrics_charts( metrics_data: list[dict[str, Any]], key_metrics_ttm_data: dict[str, Any] ): - def create_compact_line_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value): + def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value): fig = go.Figure() - # Historical data line + # Historical data bars fig.add_trace( - go.Scatter( + go.Bar( x=data[x_col], y=data[y_col], - mode="lines+markers", - line=dict(width=2, color="royalblue"), # Professional blue color + name="Historical", marker=dict( - size=6, color="royalblue", line=dict(width=1, color="darkblue") + color="#4C78A8", # Modern blue + line=dict(width=1.5, color="#2E2E2E"), # Dark outline ), - name="Historical", + width=0.75, + opacity=0.9, + hovertemplate="%{x}: %{y:.2f}", ) ) - # TTM value point + # TTM value with high-contrast marker latest_date = data[x_col].dt.max() fig.add_trace( go.Scatter( @@ -243,53 +246,103 @@ def create_compact_line_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value y=[ttm_value], mode="markers+text", marker=dict( - size=6, - color="firebrick", + size=14, + color="#F28C38", # Vibrant orange symbol="diamond", - line=dict(width=2, color="darkred"), + line=dict(width=2, color="#D76F1E"), # Darker orange outline ), name="TTM", - text=[f"{ttm_value:.2f}"], # Display TTM value as text + text=[f"{ttm_value:.2f}"], textposition="top center", + textfont=dict( + size=13, color="#2E2E2E", weight="bold" + ), # Dark gray for visibility + hovertemplate="TTM: %{y:.2f}", ) ) - # Horizontal line for TTM value + # TTM reference line fig.add_shape( type="line", x0=data[x_col].dt.min(), y0=ttm_value, x1=latest_date, y1=ttm_value, - line=dict(color="firebrick", width=2, dash="dash"), + line=dict( + color="#F28C38", + width=2, + dash="dash", + ), ) - # Update layout for a more professional appearance + # Adaptive layout fig.update_layout( - title="", - title_font=dict(size=16, family="Arial", color="black"), + title=dict( + text=title, + font=dict(size=16, color="#2E2E2E"), # Darker gray for contrast + x=0.5, + xanchor="center", + y=0.95, + yanchor="top", + ), xaxis_title="Year", yaxis_title=title, - plot_bgcolor="white", # Clean background - height=400, - margin=dict(l=40, r=40, t=40, b=40), - font=dict(family="Arial", size=12), + plot_bgcolor="rgba(0,0,0,0)", # Transparent plot + paper_bgcolor="rgba(0,0,0,0)", # Transparent paper + height=450, + margin=dict(l=60, r=40, t=80, b=60), + font=dict( + family="Inter, Arial, sans-serif", + size=13, + color="#2E2E2E", # Dark gray text + ), showlegend=True, legend=dict( - orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="right", + x=1, + font=dict(size=12), + bgcolor="rgba(255,255,255,0.8)", # Light background for legend on white ), - hovermode="x unified", # Unified hover for better readability + hovermode="x unified", + transition_duration=500, ) - # Customize axes + # Enhanced axes fig.update_xaxes( tickmode="array", tickvals=data[x_col], - ticktext=data[x_col], - gridcolor="lightgrey", + ticktext=data[x_col].dt.strftime("%Y"), + gridcolor="rgba(0,0,0,0.2)", # Darker gridlines for white background + linecolor="#666666", + linewidth=1.5, + ticks="outside", + tickfont=dict(size=12), + title_font=dict(size=14), + zeroline=False, ) - fig.update_yaxes(gridcolor="lightgrey") + fig.update_yaxes( + gridcolor="rgba(0,0,0,0.2)", # Darker gridlines + linecolor="#666666", + linewidth=1.5, + tickfont=dict(size=12), + title_font=dict(size=14), + zeroline=False, + showline=True, + ) + + # Enhanced hover + fig.update_traces( + hoverlabel=dict( + bgcolor="#FFFFFF", # White hover background + font_size=12, + font_color="#2E2E2E", # Dark gray text + bordercolor="#666666", + ), + ) return fig # Create charts @@ -304,7 +357,7 @@ def create_compact_line_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value .drop("date") ) - if df is None: + if df is None or df.is_empty(): print("Dataframe is empty") return None @@ -314,7 +367,7 @@ def plot_chart(metrics: list[tuple[str, str]]): dfx = df.select("FYDateEnding", f"{metric}") st.plotly_chart( - create_compact_line_chart( + create_compact_bar_chart( dfx, x_col="FYDateEnding", y_col=f"{metric}", @@ -361,48 +414,82 @@ def plot_chart(metrics: list[tuple[str, str]]): def display_growth_charts(growth_data: list[dict[str, Any]]): - def create_compact_line_chart(data: pl.DataFrame, x_col, y_col, title): + def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title): fig = go.Figure() - # Historical data line fig.add_trace( - go.Scatter( + go.Bar( x=data[x_col], y=data[y_col], - mode="lines+markers", - line=dict(width=2, color="royalblue"), # Professional blue color + name="", marker=dict( - size=6, color="royalblue", line=dict(width=1, color="darkblue") + color="#54A24B", # Modern green + line=dict(width=1.5, color="#2E2E2E"), # Dark outline ), - name="", # f"Historical {title}", + width=0.7, + hovertemplate="%{x}: %{y:.2f}%", ) ) - # Update layout for a more professional appearance fig.update_layout( - title="", - title_font=dict(size=16, family="Arial", color="black"), + title=dict( + text=title, + font=dict(size=18, color="#2E2E2E"), # Darker gray + x=0.5, + xanchor="center", + y=0.95, + yanchor="top", + ), xaxis_title="Year", - yaxis_title=title, - plot_bgcolor="white", # Clean background - height=400, - margin=dict(l=40, r=40, t=40, b=40), - font=dict(family="Arial", size=12), - showlegend=True, - legend=dict( - orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 + yaxis_title="Growth Rate (%)", + plot_bgcolor="rgba(0,0,0,0)", + paper_bgcolor="rgba(0,0,0,0)", + height=450, + margin=dict(l=50, r=50, t=80, b=60), + font=dict( + family="Inter, Arial, sans-serif", + size=13, + color="#2E2E2E", # Dark gray text ), - hovermode="x unified", # Unified hover for better readability + showlegend=False, + hovermode="x unified", + barmode="group", + transition_duration=400, ) - # Customize axes fig.update_xaxes( tickmode="array", tickvals=data[x_col], - ticktext=data[x_col], - gridcolor="lightgrey", + ticktext=data[x_col].dt.strftime("%Y"), + gridcolor="rgba(0,0,0,0.2)", # Darker gridlines + linecolor="#666666", + linewidth=1, + ticks="outside", + tickfont=dict(size=12), + title_font=dict(size=14), + zeroline=False, + ) + + fig.update_yaxes( + gridcolor="rgba(0,0,0,0.2)", + linecolor="#666666", + linewidth=1, + ticksuffix="%", + tickfont=dict(size=12), + title_font=dict(size=14), + zeroline=False, + showline=True, + ) + + fig.update_traces( + opacity=0.95, + hoverlabel=dict( + bgcolor="#FFFFFF", + font_size=12, + font_color="#2E2E2E", + bordercolor="#666666", + ), ) - fig.update_yaxes(gridcolor="lightgrey") return fig @@ -417,9 +504,9 @@ def create_compact_line_chart(data: pl.DataFrame, x_col, y_col, title): ) .sort("date") .drop("date") - ) # pl.col("col_name").list.eval(pl.element().sqrt()). + ) - if df is None: + if df is None or df.is_empty(): print("Dataframe is empty") return None @@ -428,7 +515,7 @@ def plot_chart(metrics: list[tuple[str, str]]): dfx = df.select("Year", f"{metric}") st.plotly_chart( - create_compact_line_chart( + create_compact_bar_chart( dfx, x_col="Year", y_col=f"{metric}", From 31e2b1cda130bf851f11ab86006659bf94109a5e Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Thu, 3 Apr 2025 21:54:41 -0600 Subject: [PATCH 12/19] Added test files --- .dockerignore | 108 +++++++++ .github/workflows/ci_cd.yml | 48 ++-- .streamlit/config.toml | 2 +- Dockerfile | 54 +++++ Makefile | 56 +++++ pyproject.toml | 12 +- src/app.py | 4 +- tests/test_data_validation.py | 13 + tests/test_stock_models.py | 173 +++++++++++++ tests/test_utils.py | 11 + uv.lock | 443 ---------------------------------- 11 files changed, 447 insertions(+), 477 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 tests/test_data_validation.py create mode 100644 tests/test_stock_models.py create mode 100644 tests/test_utils.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..26ad490 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,108 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +.git +.gitignore +.venv/ +__pycache__/ +*.log +.python-version +data/ + + +# Python-generated files +__pycache__/ +.mypy_cache +.ruff_cache +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Environment variables +.env +.venv +venv/ +env/ +ENV/ + +# IDEs and editors +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Operating System Files +.DS_Store +Thumbs.db + +# Jupyter Notebook +.ipynb_checkpoints + +# pytest +.pytest_cache/ + +# Coverage reports +htmlcov/ +.coverage +.coverage.* +.cache + +# Logs +*.log + +# uv specific +.uv/ + +# DuckDB +*.duckdb + +# Scratch Pad +scratch.py \ No newline at end of file diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index f57a483..2edfa17 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -1,38 +1,32 @@ -name: CI/CD +name: CI/CD Pipeline on: push: - branches: [ main ] + branches: [dev, main] pull_request: - branches: [ main ] + branches: [main] jobs: - test: + ci: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.11' - - name: Install dependencies - run: | - pip install uv - uv pip install -e .[dev] - - name: Run tests - run: pytest + - uses: actions/checkout@v4 + - name: Build dev image + run: make docker-build + - name: Run linting + run: make docker-check + - name: Run tests + run: make docker-test - deploy: - needs: test - runs-on: ubuntu-latest + cd: + needs: ci if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Build and push Docker image - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: | - docker build -t your-docker-repo/stock-valuation-app:latest . - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin - docker push your-docker-repo/stock-valuation-app:latest \ No newline at end of file + - uses: actions/checkout@v4 + - name: Build prod image + run: make docker-build-prod + - name: Login to Docker Hub + run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + - name: Push prod image + run: make docker-push \ No newline at end of file diff --git a/.streamlit/config.toml b/.streamlit/config.toml index 3767d48..80a2030 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -1,6 +1,6 @@ [theme] base="dark" -backgroundColor="#F5F5F5" # "#E3F2FD" +backgroundColor="#F5F5F5" secondaryBackgroundColor="#F0F2F6" textColor="#333333" font="sans serif" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..daae632 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +# Build stage (with dev tools for linting, testing, etc.) +FROM python:3.11-slim AS builder +WORKDIR /app + +# Prevent Python from writing pyc files and buffering output +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Install uv +RUN pip install --no-cache-dir uv + +# Copy dependency files first for caching +COPY pyproject.toml uv.lock /app/ + +# Sync dependencies (includes streamlit, ruff, pytest if in pyproject.toml) +RUN uv sync --frozen + +# Copy the rest of the project +COPY . /app + +# Final stage (production, lean image) +FROM python:3.11-slim AS production +WORKDIR /app + +# Prevent Python from writing pyc files and buffering output +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Create a non-privileged user +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser + +# Copy the virtual env and project files +COPY --from=builder /app/.venv /app/.venv +COPY . /app + +# Set PATH to use the virtual env +ENV PATH="/app/.venv/bin:$PATH" + +# Switch to non-privileged user +USER appuser + +# Expose Streamlit's default port +EXPOSE 8501 + +# Run Streamlit app +CMD ["streamlit", "run", "app.py"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4129ca3 --- /dev/null +++ b/Makefile @@ -0,0 +1,56 @@ +.PHONY: all deps check test docker-build docker-check docker-test docker-build-prod docker-run docker-tag docker-push docker-clean + +# Dependency management +deps: + uv sync + +# Linting and formatting +check: + - uv run ruff check . --fix + - uv run ruff format . + +# Testing +test: + uv run pytest -v + +# Dev: Build the dev image (builder stage) +docker-build: + docker build --target builder -t stock-analysis-project:dev . + +# Dev: ruff check +docker-check: docker-build + docker run --rm -v $(PWD):/app stock-analysis-project:dev ruff check . + +# Dev: test +docker-test: docker-build + docker run --rm stock-analysis-project:dev pytest --verbose + +# Prod: Build the production image (production stage) +docker-build-prod: + docker build --target production -t stock-analysis-project:latest . + +# Prod: Run the Streamlit app (production image) +docker-run: docker-build-prod + docker run --rm -e API_KEY=${API_KEY} -v $(PWD)/data:/app/data -p 8501:8501 stock-analysis-project:latest + +# Prod: Tag production image +docker-tag: docker-build-prod + docker tag stock-analysis-project skytics/stock-analysis-project:latest + +# Prod: Push prod image to Docker Hub +docker-push: docker-tag + docker push skytics/stock-analysis-project:latest + +# Delete images +docker-clean: + - docker image rm stock-analysis-project:dev || true + - docker image rm stock-analysis-project:latest || true + +# All-in-one +all: check test docker-build + @echo "All checks passed!" + + + + + diff --git a/pyproject.toml b/pyproject.toml index 8802cdb..0212ed6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ requires-python = ">=3.12" dependencies = [ "asyncio>=3.4.3", - "duckdb>=1.1.3", "httpx>=0.27.2", "plotly>=5.24.1", "polars>=1.12.0", @@ -20,8 +19,13 @@ dependencies = [ [dependency-groups] dev = [ - "ipykernel>=6.29.5", - "mypy>=1.13.0", "pytest>=8.3.3", "ruff>=0.7.3", -] \ No newline at end of file +] + +[tool.pytest.ini_options] +pythonpath = ["src"] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] \ No newline at end of file diff --git a/src/app.py b/src/app.py index ca7772d..712a2b1 100644 --- a/src/app.py +++ b/src/app.py @@ -592,10 +592,10 @@ def main(): unsafe_allow_html=True, ) - st.title("Stock Valuation Dashboard") + st.title("Stock Data View") # st.divider() st.subheader( - "Get stock quality and valuation insights from historical financial data.", + "Visualize stock fundamentals, ratings, and historical data using the Financial Modeling Prep (FMP) API", divider="gray", ) diff --git a/tests/test_data_validation.py b/tests/test_data_validation.py new file mode 100644 index 0000000..ac9e457 --- /dev/null +++ b/tests/test_data_validation.py @@ -0,0 +1,13 @@ +from src.data_validation import get_validated_stock_data + + +def test_get_validated_stock_data_valid_ticker(): + """Test data validation with valid ticker""" + ticker = "AAPL" + data = get_validated_stock_data(ticker) + + assert data is not None + assert "profile" in data + assert "quote" in data + assert "ratings" in data + assert "key_metrics_ttm" in data diff --git a/tests/test_stock_models.py b/tests/test_stock_models.py new file mode 100644 index 0000000..971fad5 --- /dev/null +++ b/tests/test_stock_models.py @@ -0,0 +1,173 @@ +from src.stock_models import ( + Growth, + KeyMetricsTTM, + Quote, + Ratings, +) + + +class TestQuote: + def test_valid_quote(self): + """Test creating a valid Quote instance""" + data = { + "symbol": "AAPL", + "price": 175.84, + "changesPercentage": 0.75, + "yearHigh": 198.23, + "yearLow": 124.17, + "marketCap": 2750000000000, + "avgVolume": 55000000, + "eps": 6.13, + "pe": 28.7, + "earningsAnnouncement": "2024-01-25", + "sharesOutstanding": 15600000000, + } + + quote = Quote(**data) + assert quote.symbol == "AAPL" + assert quote.price == 175.84 + assert quote.change_percent == 0.75 + assert quote.market_cap == 2750000000000 + + def test_quote_with_null_values(self): + """Test Quote with null values for optional fields""" + data = { + "symbol": "AAPL", + "price": 175.84, + "changesPercentage": None, + "yearHigh": 198.23, + "yearLow": 124.17, + "marketCap": 2750000000000, + "avgVolume": None, + "eps": None, + "pe": None, + "earningsAnnouncement": "2024-01-25", + "sharesOutstanding": 15600000000, + } + + quote = Quote(**data) + assert quote.change_percent is None + assert quote.eps is None + assert quote.pe is None + + +class TestRatings: + def test_valid_ratings(self): + """Test creating a valid Ratings instance""" + data = { + "symbol": "AAPL", + "date": "2024-01-15", + "rating": "Strong Buy", + "ratingScore": 5, + "ratingRecommendation": "Strong Buy", + "ratingDetailsDCFScore": 4, + "ratingDetailsDCFRecommendation": "Buy", + "ratingDetailsROEScore": 5, + "ratingDetailsROERecommendation": "Strong Buy", + "ratingDetailsROAScore": 4, + "ratingDetailsROARecommendation": "Buy", + "ratingDetailsDEScore": 5, + "ratingDetailsDERecommendation": "Strong Buy", + "ratingDetailsPEScore": 3, + "ratingDetailsPERecommendation": "Neutral", + "ratingDetailsPBScore": 4, + "ratingDetailsPBRecommendation": "Buy", + } + + ratings = Ratings(**data) + assert ratings.symbol == "AAPL" + assert ratings.score == 5 + assert ratings.dcf_score == 4 + assert ratings.pe_rec == "Neutral" + + +class TestKeyMetricsTTM: + def test_valid_key_metrics_ttm(self): + """Test creating a valid KeyMetricsTTM instance""" + data = { + "revenuePerShareTTM": 24.87, + "netIncomePerShareTTM": 6.13, + "freeCashFlowPerShareTTM": 6.85, + "peRatioTTM": 28.7, + "enterpriseValueOverEBITDATTM": 22.5, + "evToFreeCashFlowTTM": 25.3, + "freeCashFlowYieldTTM": 0.039, + "priceToSalesRatioTTM": 7.1, + "ptbRatioTTM": 45.8, + "pfcfRatioTTM": 25.7, + "dividendYieldPercentageTTM": 0.0051, + "dividendPerShareTTM": 0.92, + "payoutRatioTTM": 0.15, + } + + metrics = KeyMetricsTTM(**data) + assert metrics.rev_per_share_ttm == 24.87 + assert metrics.pe_ratio_ttm == 28.7 + assert metrics.dvd_yield_pct_ttm == 0.0051 + + def test_key_metrics_ttm_with_nulls(self): + """Test KeyMetricsTTM with null values""" + data = { + "revenuePerShareTTM": None, + "netIncomePerShareTTM": None, + "freeCashFlowPerShareTTM": 6.85, + "peRatioTTM": 28.7, + "enterpriseValueOverEBITDATTM": None, + "evToFreeCashFlowTTM": None, + "freeCashFlowYieldTTM": 0.039, + "priceToSalesRatioTTM": None, + "ptbRatioTTM": None, + "pfcfRatioTTM": None, + "dividendYieldPercentageTTM": None, + "dividendPerShareTTM": None, + "payoutRatioTTM": None, + } + + metrics = KeyMetricsTTM(**data) + assert metrics.rev_per_share_ttm is None + assert metrics.net_income_per_share_ttm is None + assert metrics.fcf_per_share_ttm == 6.85 + + +class TestGrowth: + def test_valid_growth(self): + """Test creating a valid Growth instance""" + data = { + "symbol": "AAPL", + "date": "2023-12-31", + "revenueGrowth": 0.08, + "epsdilutedGrowth": 0.09, + "dividendsperShareGrowth": 0.05, + "freeCashFlowGrowth": 0.07, + "debtGrowth": -0.02, + "fiveYRevenueGrowthPerShare": 0.15, + "fiveYNetIncomeGrowthPerShare": 0.18, + "fiveYDividendperShareGrowthPerShare": 0.08, + "fiveYOperatingCFGrowthPerShare": 0.12, + } + + growth = Growth(**data) + assert growth.symbol == "AAPL" + assert growth.rev_growth == 0.08 + assert growth.fiveY_rev_growth_per_share == 0.15 + + def test_growth_with_nulls(self): + """Test Growth with null values""" + data = { + "symbol": "AAPL", + "date": "2023-12-31", + "revenueGrowth": None, + "epsdilutedGrowth": None, + "dividendsperShareGrowth": None, + "freeCashFlowGrowth": 0.07, + "debtGrowth": None, + "fiveYRevenueGrowthPerShare": 0.15, + "fiveYNetIncomeGrowthPerShare": None, + "fiveYDividendperShareGrowthPerShare": None, + "fiveYOperatingCFGrowthPerShare": None, + } + + growth = Growth(**data) + assert growth.rev_growth is None + assert growth.fcf_growth == 0.07 + assert growth.fiveY_rev_growth_per_share == 0.15 diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..2733c54 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,11 @@ +from config import settings +from utils import FMPClient + + +def test_fmp_client_initialization(): + client = FMPClient() + assert client.base_url == settings.base_url + assert client.api_key == settings.fmp_api_key + assert len(client.metric_types) == 6 + assert "profile" in client.metric_types + assert "rating" in client.metric_types diff --git a/uv.lock b/uv.lock index 5f2eda7..c10bcd4 100644 --- a/uv.lock +++ b/uv.lock @@ -44,27 +44,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, ] -[[package]] -name = "appnope" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, -] - -[[package]] -name = "asttokens" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/45/1d/f03bcb60c4a3212e15f99a56085d93093a497718adf828d050b9d675da81/asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0", size = 62284 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764 }, -] - [[package]] name = "asyncio" version = "3.4.3" @@ -110,39 +89,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, ] -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, -] - [[package]] name = "charset-normalizer" version = "3.4.0" @@ -203,77 +149,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] -[[package]] -name = "comm" -version = "0.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180 }, -] - -[[package]] -name = "debugpy" -version = "1.8.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/5e/7667b95c9d7ddb25c047143a3a47685f9be2a5d3d177a85a730b22dc6e5c/debugpy-1.8.8.zip", hash = "sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091", size = 4928684 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/04/8e79824c4d9100049bda056aeaf8f2765d1325a4521a87f8bb373c977236/debugpy-1.8.8-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:0cc94186340be87b9ac5a707184ec8f36547fb66636d1029ff4f1cc020e53996", size = 2514549 }, - { url = "https://files.pythonhosted.org/packages/a5/6b/c336d1eba1aedc9f654aefcdfe47ec41657d149f28ca1477c5f9009681c6/debugpy-1.8.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64674e95916e53c2e9540a056e5f489e0ad4872645399d778f7c598eacb7b7f9", size = 4229617 }, - { url = "https://files.pythonhosted.org/packages/63/9c/d9276c41e9e14164b31bcba789c87a355c091d0fc2d4e4e36a4881c9aa54/debugpy-1.8.8-cp312-cp312-win32.whl", hash = "sha256:5c6e885dbf12015aed73770f29dec7023cb310d0dc2ba8bfbeb5c8e43f80edc9", size = 5167033 }, - { url = "https://files.pythonhosted.org/packages/6d/1c/fd4bc22196b2d0defaa9f644ea4d676d0cb53b6434091b5fa2d4e49c85f2/debugpy-1.8.8-cp312-cp312-win_amd64.whl", hash = "sha256:19ffbd84e757a6ca0113574d1bf5a2298b3947320a3e9d7d8dc3377f02d9f864", size = 5209968 }, - { url = "https://files.pythonhosted.org/packages/90/45/6745f342bbf41bde7eb5dbf5567b794a4a5498a7a729146cb3101b875b30/debugpy-1.8.8-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:705cd123a773d184860ed8dae99becd879dfec361098edbefb5fc0d3683eb804", size = 2499523 }, - { url = "https://files.pythonhosted.org/packages/5c/39/0374610062a384648db9b7b315d0c906facf23613bfd19527135a7c0a420/debugpy-1.8.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890fd16803f50aa9cb1a9b9b25b5ec321656dd6b78157c74283de241993d086f", size = 4218219 }, - { url = "https://files.pythonhosted.org/packages/cc/19/5b8a68eb9bbafd6bfd27ba0ed93d411f3fd50935ecdd2df242de2110a7c9/debugpy-1.8.8-cp313-cp313-win32.whl", hash = "sha256:90244598214bbe704aa47556ec591d2f9869ff9e042e301a2859c57106649add", size = 5171845 }, - { url = "https://files.pythonhosted.org/packages/cd/04/7381dab68e40ca877d5beffc25ad1a0d3d2557cf7465405435fac9e27ef5/debugpy-1.8.8-cp313-cp313-win_amd64.whl", hash = "sha256:4b93e4832fd4a759a0c465c967214ed0c8a6e8914bced63a28ddb0dd8c5f078b", size = 5206890 }, - { url = "https://files.pythonhosted.org/packages/03/99/ec2190d03df5dbd610418919bd1c3d8e6f61d0a97894e11ade6d3260cfb8/debugpy-1.8.8-py2.py3-none-any.whl", hash = "sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f", size = 5157124 }, -] - -[[package]] -name = "decorator" -version = "5.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, -] - -[[package]] -name = "duckdb" -version = "1.1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/d7/ec014b351b6bb026d5f473b1d0ec6bd6ba40786b9abbf530b4c9041d9895/duckdb-1.1.3.tar.gz", hash = "sha256:68c3a46ab08836fe041d15dcbf838f74a990d551db47cb24ab1c4576fc19351c", size = 12240672 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/ff/7ee500f4cff0d2a581c1afdf2c12f70ee3bf1a61041fea4d88934a35a7a3/duckdb-1.1.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:a433ae9e72c5f397c44abdaa3c781d94f94f4065bcbf99ecd39433058c64cb38", size = 15482881 }, - { url = "https://files.pythonhosted.org/packages/28/16/dda10da6bde54562c3cb0002ca3b7678e3108fa73ac9b7509674a02c5249/duckdb-1.1.3-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:d08308e0a46c748d9c30f1d67ee1143e9c5ea3fbcccc27a47e115b19e7e78aa9", size = 32349440 }, - { url = "https://files.pythonhosted.org/packages/2e/c2/06f7f7a51a1843c9384e1637abb6bbebc29367710ffccc7e7e52d72b3dd9/duckdb-1.1.3-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:5d57776539211e79b11e94f2f6d63de77885f23f14982e0fac066f2885fcf3ff", size = 16953473 }, - { url = "https://files.pythonhosted.org/packages/1a/84/9991221ef7dde79d85231f20646e1b12d645490cd8be055589276f62847e/duckdb-1.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e59087dbbb63705f2483544e01cccf07d5b35afa58be8931b224f3221361d537", size = 18491915 }, - { url = "https://files.pythonhosted.org/packages/aa/76/330fe16f12b7ddda0c664ba9869f3afbc8773dbe17ae750121d407dc0f37/duckdb-1.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ebf5f60ddbd65c13e77cddb85fe4af671d31b851f125a4d002a313696af43f1", size = 20150288 }, - { url = "https://files.pythonhosted.org/packages/c4/88/e4b08b7a5d08c0f65f6c7a6594de64431ce7df38d7258511417ba7989ad3/duckdb-1.1.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4ef7ba97a65bd39d66f2a7080e6fb60e7c3e41d4c1e19245f90f53b98e3ac32", size = 18296560 }, - { url = "https://files.pythonhosted.org/packages/1a/32/011e6e3ce14375a1ba01a588c119ad82be757f847c6b60207e0762d9ec3a/duckdb-1.1.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f58db1b65593ff796c8ea6e63e2e144c944dd3d51c8d8e40dffa7f41693d35d3", size = 21635270 }, - { url = "https://files.pythonhosted.org/packages/f2/eb/58d4e0eccdc7b3523c062d008ad9eef28edccf88591d1a78659c809fe6e8/duckdb-1.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:e86006958e84c5c02f08f9b96f4bc26990514eab329b1b4f71049b3727ce5989", size = 10955715 }, - { url = "https://files.pythonhosted.org/packages/81/d1/2462492531d4715b2ede272a26519b37f21cf3f8c85b3eb88da5b7be81d8/duckdb-1.1.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:0897f83c09356206ce462f62157ce064961a5348e31ccb2a557a7531d814e70e", size = 15483282 }, - { url = "https://files.pythonhosted.org/packages/af/a5/ec595aa223b911a62f24393908a8eaf8e0ed1c7c07eca5008f22aab070bc/duckdb-1.1.3-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:cddc6c1a3b91dcc5f32493231b3ba98f51e6d3a44fe02839556db2b928087378", size = 32350342 }, - { url = "https://files.pythonhosted.org/packages/08/27/e35116ab1ada5e54e52424e52d16ee9ae82db129025294e19c1d48a8b2b1/duckdb-1.1.3-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:1d9ab6143e73bcf17d62566e368c23f28aa544feddfd2d8eb50ef21034286f24", size = 16953863 }, - { url = "https://files.pythonhosted.org/packages/0d/ac/f2db3969a56cd96a3ba78b0fd161939322fb134bd07c98ecc7a7015d3efa/duckdb-1.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f073d15d11a328f2e6d5964a704517e818e930800b7f3fa83adea47f23720d3", size = 18494301 }, - { url = "https://files.pythonhosted.org/packages/cf/66/d0be7c9518b1b92185018bacd851f977a101c9818686f667bbf884abcfbc/duckdb-1.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5724fd8a49e24d730be34846b814b98ba7c304ca904fbdc98b47fa95c0b0cee", size = 20150992 }, - { url = "https://files.pythonhosted.org/packages/47/ae/c2df66e3716705f48775e692a1b8accbf3dc6e2c27a0ae307fb4b063e115/duckdb-1.1.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51e7dbd968b393343b226ab3f3a7b5a68dee6d3fe59be9d802383bf916775cb8", size = 18297818 }, - { url = "https://files.pythonhosted.org/packages/8e/7e/10310b754b7ec3349c411a0a88ecbf327c49b5714e3d35200e69c13fb093/duckdb-1.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00cca22df96aa3473fe4584f84888e2cf1c516e8c2dd837210daec44eadba586", size = 21635169 }, - { url = "https://files.pythonhosted.org/packages/83/be/46c0b89c9d4e1ba90af9bc184e88672c04d420d41342e4dc359c78d05981/duckdb-1.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:77f26884c7b807c7edd07f95cf0b00e6d47f0de4a534ac1706a58f8bc70d0d31", size = 10955826 }, -] - -[[package]] -name = "executing" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/e3/7d45f492c2c4a0e8e0fad57d081a7c8a0286cdd86372b070cca1ec0caa1e/executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab", size = 977485 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", size = 25805 }, -] - [[package]] name = "gitdb" version = "4.0.11" @@ -354,62 +229,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] -[[package]] -name = "ipykernel" -version = "6.29.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "appnope", marker = "sys_platform == 'darwin'" }, - { name = "comm" }, - { name = "debugpy" }, - { name = "ipython" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "matplotlib-inline" }, - { name = "nest-asyncio" }, - { name = "packaging" }, - { name = "psutil" }, - { name = "pyzmq" }, - { name = "tornado" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173 }, -] - -[[package]] -name = "ipython" -version = "8.29.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/85/e0/a3f36dde97e12121106807d80485423ae4c5b27ce60d40d4ab0bab18a9db/ipython-8.29.0.tar.gz", hash = "sha256:40b60e15b22591450eef73e40a027cf77bd652e757523eebc5bd7c7c498290eb", size = 5497513 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/a5/c15ed187f1b3fac445bb42a2dedd8dec1eee1718b35129242049a13a962f/ipython-8.29.0-py3-none-any.whl", hash = "sha256:0188a1bd83267192123ccea7f4a8ed0a78910535dbaa3f37671dca76ebd429c8", size = 819911 }, -] - -[[package]] -name = "jedi" -version = "0.19.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "parso" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, -] - [[package]] name = "jinja2" version = "3.1.4" @@ -449,36 +268,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, ] -[[package]] -name = "jupyter-client" -version = "8.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jupyter-core" }, - { name = "python-dateutil" }, - { name = "pyzmq" }, - { name = "tornado" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105 }, -] - -[[package]] -name = "jupyter-core" -version = "5.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "platformdirs" }, - { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/11/b56381fa6c3f4cc5d2cf54a7dbf98ad9aa0b339ef7a601d6053538b079a7/jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9", size = 87629 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", size = 28965 }, -] - [[package]] name = "markdown-it-py" version = "3.0.0" @@ -529,18 +318,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, ] -[[package]] -name = "matplotlib-inline" -version = "0.1.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -550,38 +327,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] -[[package]] -name = "mypy" -version = "1.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 }, - { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 }, - { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 }, - { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 }, - { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 }, - { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 }, - { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 }, - { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, - { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, - { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, - { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, -] - [[package]] name = "narwhals" version = "1.14.1" @@ -591,15 +336,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/38/63dcb45e12f6e5fcdb7b05d4c3b884502c50613b56c0e6fec78803cf14a7/narwhals-1.14.1-py3-none-any.whl", hash = "sha256:b737db277df174ca41b45950e50f48a738c88bd9b896398ffa8872e4e3930def", size = 220586 }, ] -[[package]] -name = "nest-asyncio" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, -] - [[package]] name = "numpy" version = "2.1.3" @@ -681,27 +417,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, ] -[[package]] -name = "parso" -version = "0.8.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, -] - -[[package]] -name = "pexpect" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ptyprocess" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, -] - [[package]] name = "pillow" version = "11.0.0" @@ -740,15 +455,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828 }, ] -[[package]] -name = "platformdirs" -version = "4.3.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, -] - [[package]] name = "plotly" version = "5.24.1" @@ -784,18 +490,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/28/3d44ddf56a5c95272b202ce8aa0e9b818a1310e83525c4c29176b538ae7c/polars-1.12.0-cp39-abi3-win_amd64.whl", hash = "sha256:a228a4b320a36d03a9ec9dfe7241b6d80a2f119b2dceb1da953166655e4cf43c", size = 33790337 }, ] -[[package]] -name = "prompt-toolkit" -version = "3.0.48" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/4f/feb5e137aff82f7c7f3248267b97451da3644f6cdc218edfe549fb354127/prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", size = 424684 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 }, -] - [[package]] name = "protobuf" version = "5.28.3" @@ -810,39 +504,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/c3/2377c159e28ea89a91cf1ca223f827ae8deccb2c9c401e5ca233cd73002f/protobuf-5.28.3-py3-none-any.whl", hash = "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed", size = 169511 }, ] -[[package]] -name = "psutil" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762 }, - { url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777 }, - { url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259 }, - { url = "https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b", size = 287255 }, - { url = "https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a", size = 288804 }, - { url = "https://files.pythonhosted.org/packages/ea/55/5389ed243c878725feffc0d6a3bc5ef6764312b6fc7c081faaa2cfa7ef37/psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e", size = 250386 }, - { url = "https://files.pythonhosted.org/packages/11/91/87fa6f060e649b1e1a7b19a4f5869709fbf750b7c8c262ee776ec32f3028/psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be", size = 254228 }, -] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, -] - -[[package]] -name = "pure-eval" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, -] - [[package]] name = "pyarrow" version = "18.0.0" @@ -871,15 +532,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/a2/81c1dd744b322c0c548f793deb521bf23500806d754128ddf6f978736dff/pyarrow-18.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b46591222c864e7da7faa3b19455196416cd8355ff6c2cc2e65726a760a3c420", size = 40006508 }, ] -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, -] - [[package]] name = "pydantic" version = "2.10.6" @@ -1013,63 +665,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, ] -[[package]] -name = "pywin32" -version = "308" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729 }, - { url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015 }, - { url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033 }, - { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, - { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, - { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, -] - -[[package]] -name = "pyzmq" -version = "26.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/05/bed626b9f7bb2322cdbbf7b4bd8f54b1b617b0d2ab2d3547d6e39428a48e/pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f", size = 271975 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/2f/78a766c8913ad62b28581777ac4ede50c6d9f249d39c2963e279524a1bbe/pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9", size = 1343105 }, - { url = "https://files.pythonhosted.org/packages/b7/9c/4b1e2d3d4065be715e007fe063ec7885978fad285f87eae1436e6c3201f4/pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52", size = 1008365 }, - { url = "https://files.pythonhosted.org/packages/4f/ef/5a23ec689ff36d7625b38d121ef15abfc3631a9aecb417baf7a4245e4124/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08", size = 665923 }, - { url = "https://files.pythonhosted.org/packages/ae/61/d436461a47437d63c6302c90724cf0981883ec57ceb6073873f32172d676/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5", size = 903400 }, - { url = "https://files.pythonhosted.org/packages/47/42/fc6d35ecefe1739a819afaf6f8e686f7f02a4dd241c78972d316f403474c/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae", size = 860034 }, - { url = "https://files.pythonhosted.org/packages/07/3b/44ea6266a6761e9eefaa37d98fabefa112328808ac41aa87b4bbb668af30/pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711", size = 860579 }, - { url = "https://files.pythonhosted.org/packages/38/6f/4df2014ab553a6052b0e551b37da55166991510f9e1002c89cab7ce3b3f2/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6", size = 1196246 }, - { url = "https://files.pythonhosted.org/packages/38/9d/ee240fc0c9fe9817f0c9127a43238a3e28048795483c403cc10720ddef22/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3", size = 1507441 }, - { url = "https://files.pythonhosted.org/packages/85/4f/01711edaa58d535eac4a26c294c617c9a01f09857c0ce191fd574d06f359/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b", size = 1406498 }, - { url = "https://files.pythonhosted.org/packages/07/18/907134c85c7152f679ed744e73e645b365f3ad571f38bdb62e36f347699a/pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7", size = 575533 }, - { url = "https://files.pythonhosted.org/packages/ce/2c/a6f4a20202a4d3c582ad93f95ee78d79bbdc26803495aec2912b17dbbb6c/pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a", size = 637768 }, - { url = "https://files.pythonhosted.org/packages/5f/0e/eb16ff731632d30554bf5af4dbba3ffcd04518219d82028aea4ae1b02ca5/pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b", size = 540675 }, - { url = "https://files.pythonhosted.org/packages/04/a7/0f7e2f6c126fe6e62dbae0bc93b1bd3f1099cf7fea47a5468defebe3f39d/pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726", size = 1006564 }, - { url = "https://files.pythonhosted.org/packages/31/b6/a187165c852c5d49f826a690857684333a6a4a065af0a6015572d2284f6a/pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3", size = 1340447 }, - { url = "https://files.pythonhosted.org/packages/68/ba/f4280c58ff71f321602a6e24fd19879b7e79793fb8ab14027027c0fb58ef/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50", size = 665485 }, - { url = "https://files.pythonhosted.org/packages/77/b5/c987a5c53c7d8704216f29fc3d810b32f156bcea488a940e330e1bcbb88d/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb", size = 903484 }, - { url = "https://files.pythonhosted.org/packages/29/c9/07da157d2db18c72a7eccef8e684cefc155b712a88e3d479d930aa9eceba/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187", size = 859981 }, - { url = "https://files.pythonhosted.org/packages/43/09/e12501bd0b8394b7d02c41efd35c537a1988da67fc9c745cae9c6c776d31/pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b", size = 860334 }, - { url = "https://files.pythonhosted.org/packages/eb/ff/f5ec1d455f8f7385cc0a8b2acd8c807d7fade875c14c44b85c1bddabae21/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18", size = 1196179 }, - { url = "https://files.pythonhosted.org/packages/ec/8a/bb2ac43295b1950fe436a81fc5b298be0b96ac76fb029b514d3ed58f7b27/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115", size = 1507668 }, - { url = "https://files.pythonhosted.org/packages/a9/49/dbc284ebcfd2dca23f6349227ff1616a7ee2c4a35fe0a5d6c3deff2b4fed/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e", size = 1406539 }, - { url = "https://files.pythonhosted.org/packages/00/68/093cdce3fe31e30a341d8e52a1ad86392e13c57970d722c1f62a1d1a54b6/pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5", size = 575567 }, - { url = "https://files.pythonhosted.org/packages/92/ae/6cc4657148143412b5819b05e362ae7dd09fb9fe76e2a539dcff3d0386bc/pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad", size = 637551 }, - { url = "https://files.pythonhosted.org/packages/6c/67/fbff102e201688f97c8092e4c3445d1c1068c2f27bbd45a578df97ed5f94/pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797", size = 540378 }, - { url = "https://files.pythonhosted.org/packages/3f/fe/2d998380b6e0122c6c4bdf9b6caf490831e5f5e2d08a203b5adff060c226/pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a", size = 1007378 }, - { url = "https://files.pythonhosted.org/packages/4a/f4/30d6e7157f12b3a0390bde94d6a8567cdb88846ed068a6e17238a4ccf600/pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc", size = 1329532 }, - { url = "https://files.pythonhosted.org/packages/82/86/3fe917870e15ee1c3ad48229a2a64458e36036e64b4afa9659045d82bfa8/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5", size = 653242 }, - { url = "https://files.pythonhosted.org/packages/50/2d/242e7e6ef6c8c19e6cb52d095834508cd581ffb925699fd3c640cdc758f1/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672", size = 888404 }, - { url = "https://files.pythonhosted.org/packages/ac/11/7270566e1f31e4ea73c81ec821a4b1688fd551009a3d2bab11ec66cb1e8f/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797", size = 845858 }, - { url = "https://files.pythonhosted.org/packages/91/d5/72b38fbc69867795c8711bdd735312f9fef1e3d9204e2f63ab57085434b9/pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386", size = 847375 }, - { url = "https://files.pythonhosted.org/packages/dd/9a/10ed3c7f72b4c24e719c59359fbadd1a27556a28b36cdf1cd9e4fb7845d5/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306", size = 1183489 }, - { url = "https://files.pythonhosted.org/packages/72/2d/8660892543fabf1fe41861efa222455811adac9f3c0818d6c3170a1153e3/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6", size = 1492932 }, - { url = "https://files.pythonhosted.org/packages/7b/d6/32fd69744afb53995619bc5effa2a405ae0d343cd3e747d0fbc43fe894ee/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0", size = 1392485 }, -] - [[package]] name = "referencing" version = "0.35.1" @@ -1197,27 +792,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] -[[package]] -name = "stack-data" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asttokens" }, - { name = "executing" }, - { name = "pure-eval" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, -] - [[package]] name = "stock-valuation-app" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "asyncio" }, - { name = "duckdb" }, { name = "httpx" }, { name = "plotly" }, { name = "polars" }, @@ -1227,8 +807,6 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "ipykernel" }, - { name = "mypy" }, { name = "pytest" }, { name = "ruff" }, ] @@ -1236,7 +814,6 @@ dev = [ [package.metadata] requires-dist = [ { name = "asyncio", specifier = ">=3.4.3" }, - { name = "duckdb", specifier = ">=1.1.3" }, { name = "httpx", specifier = ">=0.27.2" }, { name = "plotly", specifier = ">=5.24.1" }, { name = "polars", specifier = ">=1.12.0" }, @@ -1246,8 +823,6 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "ipykernel", specifier = ">=6.29.5" }, - { name = "mypy", specifier = ">=1.13.0" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "ruff", specifier = ">=0.7.3" }, ] @@ -1318,15 +893,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/2f/3f2f05e84a7aff787a96d5fb06821323feb370fe0baed4db6ea7b1088f32/tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7", size = 438532 }, ] -[[package]] -name = "traitlets" -version = "5.14.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, -] - [[package]] name = "typing-extensions" version = "4.12.2" @@ -1371,12 +937,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, ] - -[[package]] -name = "wcwidth" -version = "0.2.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, -] From 321e4ddfd9ab6608cc199b9bcc9b5657315d82d2 Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Thu, 3 Apr 2025 22:20:57 -0600 Subject: [PATCH 13/19] Modified app.py file --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 712a2b1..24cb6e7 100644 --- a/src/app.py +++ b/src/app.py @@ -592,7 +592,7 @@ def main(): unsafe_allow_html=True, ) - st.title("Stock Data View") + st.title("StockDataView") # st.divider() st.subheader( "Visualize stock fundamentals, ratings, and historical data using the Financial Modeling Prep (FMP) API", From 6c7fbfbd70259ae23d1b969abd6dd8fb6f71c4dd Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Thu, 3 Apr 2025 23:48:47 -0600 Subject: [PATCH 14/19] Modified toml file. --- pyproject.toml | 4 ++-- uv.lock | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0212ed6..b7206c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "stock-valuation-app" +name = "stockdataview" version = "0.1.0" -description = "A single-page web app displaying stock data, growth charts, and valuation insights." +description = "A containerized application for visualizing stock fundamentals, ratings, and historical data using the Financial Modeling Prep (FMP) API." readme = "README.md" authors = [ { name = "Dimeji Salau", email = "dimejisalau@protonmail.com" } diff --git a/uv.lock b/uv.lock index c10bcd4..2974fcb 100644 --- a/uv.lock +++ b/uv.lock @@ -793,7 +793,7 @@ wheels = [ ] [[package]] -name = "stock-valuation-app" +name = "stockdataview" version = "0.1.0" source = { virtual = "." } dependencies = [ From 6fb14258b1b9c0cc5489761d0012817a82440508 Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Sun, 6 Apr 2025 00:50:19 -0600 Subject: [PATCH 15/19] Updated ci_cd.yml file --- .github/workflows/ci_cd.yml | 22 +++++-- README.md | 119 +++++++++++++++++------------------- 2 files changed, 75 insertions(+), 66 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 2edfa17..2f8247c 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -1,20 +1,25 @@ name: CI/CD Pipeline on: - push: - branches: [dev, main] pull_request: - branches: [main] + branches: + - main jobs: ci: runs-on: ubuntu-latest + container: + image: ubuntu:22.04-slim # Use a lightweight container for the CI job + steps: - uses: actions/checkout@v4 + - name: Build dev image run: make docker-build + - name: Run linting run: make docker-check + - name: Run tests run: make docker-test @@ -22,11 +27,20 @@ jobs: needs: ci if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + container: + image: ubuntu:22.04-slim # Use a lightweight container for the CD job + steps: - uses: actions/checkout@v4 + - name: Build prod image run: make docker-build-prod + - name: Login to Docker Hub - run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Push prod image run: make docker-push \ No newline at end of file diff --git a/README.md b/README.md index b82bf8d..dafb407 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,87 @@ -# Stock Valuation Dashboard +# StockDataView -## Overview +StockDataView is a Streamlit application designed to display and visualize stock data fetched from the Financial Modeling Prep (FMP) API. It provides users with bar charts, tables of stock fundamentals, and ratings for a given stock ticker. -This project is a web-based stock valuation dashboard that helps investors analyze dividend growth stocks. It provides insights into stock quality and valuation based on historical financial data. +## Features -Key features: -- Fetches financial data from Financial Modeling Prep API -- Calculates growth rates for revenue, dividends, cash flow, and other metrics -- Determines if a stock is a quality dividend growth stock -- Assesses whether a stock is undervalued or reasonably valued -- Visualizes key metrics and growth rates +* **Stock Data Visualization:** Presents stock data in clear and interactive bar charts. +* **Fundamental Data Tables:** Displays key stock fundamentals in easy-to-read tables. +* **Stock Ratings:** Shows ratings information for selected stocks. +* **Containerized Application:** Easily deployable via Docker. +* **API Key Input:** Requires users to provide their FMP API key for data retrieval. +* **Automated CI/CD:** Uses GitHub Actions for continuous integration and deployment. +* **Package Management:** Uses `uv` and `pyproject.toml` for dependency management. +* **Makefile Automation:** Includes a Makefile for streamlined development and deployment workflows. -## Tech Stack +## Prerequisites -- Python 3.11+ -- FastAPI: Web framework for building APIs -- Dash: React-based framework for building analytical web applications -- Polars: Fast DataFrame library for data manipulation -- Pydantic: Data validation using Python type annotations -- PyArrow: Efficient data interchange format -- DuckDB: Embedded analytical database -- uv: Python packaging and dependency management tool -- Docker: Containerization -- GitHub Actions: CI/CD pipeline +* Docker (for running the application) +* An API key from Financial Modeling Prep (FMP) -## Project Structure +## Getting Started -stock_valuation_app/ -├── .github/workflows/ # CI/CD configuration -├── src/ -│ └── stock_valuation_app/ -│ ├── api/ # FastAPI routes -│ ├── data/ # Data fetching and processing -│ ├── models/ # Pydantic models -│ ├── services/ # Business logic -│ └── ui/ # Dash dashboard -├── tests/ # Unit and integration tests -├── Dockerfile -├── docker-compose.yml -├── pyproject.toml # Project metadata and dependencies -├── README.md -└── uv.lock # Dependency lock file +1. **Clone the Repository:** + ```bash + git clone https://github.com/dimtics/StockDataView.git + cd StockDataView + ``` +2. **Build the Docker Image:** -## Setup and Installation + ```bash + make docker-build-prod + ``` -1. Clone the repository: -git clone https://github.com/dimtics/stock_valuation_app.git -cd stock_valuation_app +3. **Run the Docker Container:** -2. Install uv and project dependencies: -pip install uv -uv pip install -e . + ```bash + docker run -p 8501:8501 -e FMP_API_KEY= skytics/stockdataview + ``` -3. Set up environment variables: -Create a `.env` file in the project root and add your Financial Modeling Prep API key: -FMP_API_KEY=your_api_key_here + Replace `` with your actual FMP API key. -4. Run the application: -uvicorn stock_valuation_app:app --reload +4. **Access the Application:** -5. Open your browser and navigate to `http://localhost:8000/dashboard` to view the dashboard. + Open your web browser and navigate to `http://localhost:8501`. +5. **Usage** +* Once the app is running, enter a stock ticker (e.g., AAPL) in the Streamlit interface. +* View the displayed bar charts, tables, stock fundamentals, and ratings fetched from the FMP API. -## Docker Deployment +## Development -To run the application using Docker: +### Package Management (uv) -1. Build the Docker image: -docker build -t stock-valuation-app . -docker run -p 8000:8000 -e FMP_API_KEY=your_api_key_here stock-valuation-app +* This project uses `uv` for package management. Dependencies are defined in `pyproject.toml`. +* Sync dependencies from `pyproject.toml`. -Alternatively, use Docker Compose: -docker-compose up +To install dependencies: +```bash +uv sync +``` +## Makefile Commands +The Makefile automates common development tasks: -## Usage +* `make docker-build`: Builds dev image. +* `make docker-check`: Lints code using ruff. +* `make docker-test`: Runs tests. +* `make docker-build-prod`: Builds prod image +* `make docker-run`: Runs the Docker container. -1. Enter a stock symbol in the input field on the dashboard. -2. Click the "Analyze" button to fetch and analyze the stock data. -3. View the growth rates, quality assessment, and valuation results. +## CI/CD (GitHub Actions) + +The `.github/workflows/ci-cd.yml` file defines the CI/CD workflow, which: + +* Builds the Docker image on push to the `main` branch. +* Pushes the image to Docker Hub. ## Contributing -Contributions are welcome! Please feel free to submit a Pull Request. +Contributions are welcome! Please feel free to submit a pull request. ## License -This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file +This project is licensed under the MIT License - see the `LICENSE` file for details. \ No newline at end of file From d4b1e9111023d0fd68bbc70b8660034fea528722 Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Sun, 6 Apr 2025 01:24:04 -0600 Subject: [PATCH 16/19] Modified ci_cd.yml, Makefile and README.md files --- .github/workflows/ci_cd.yml | 16 ++------------- Makefile | 40 ++++++++++++++++++------------------- README.md | 6 +++--- 3 files changed, 25 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 2f8247c..6e59c68 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -8,28 +8,16 @@ on: jobs: ci: runs-on: ubuntu-latest - container: - image: ubuntu:22.04-slim # Use a lightweight container for the CI job - steps: - uses: actions/checkout@v4 - - name: Build dev image - run: make docker-build - - - name: Run linting - run: make docker-check - - - name: Run tests - run: make docker-test + - name: Build and Check (using multi-stage Dockerfile) + run: make docker-check-ci cd: needs: ci if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest - container: - image: ubuntu:22.04-slim # Use a lightweight container for the CD job - steps: - uses: actions/checkout@v4 diff --git a/Makefile b/Makefile index 4129ca3..aa8efbc 100644 --- a/Makefile +++ b/Makefile @@ -1,56 +1,56 @@ -.PHONY: all deps check test docker-build docker-check docker-test docker-build-prod docker-run docker-tag docker-push docker-clean +.PHONY: all deps check test docker-build docker-check docker-test docker-build-prod docker-run docker-tag docker-push docker-clean docker-check-ci # Dependency management deps: - uv sync + uv sync # Linting and formatting check: - - uv run ruff check . --fix - - uv run ruff format . + - uv run ruff check . --fix + - uv run ruff format . # Testing test: - uv run pytest -v + uv run pytest -v # Dev: Build the dev image (builder stage) docker-build: - docker build --target builder -t stock-analysis-project:dev . + docker build --target builder -t stockdataview:dev . # Dev: ruff check docker-check: docker-build - docker run --rm -v $(PWD):/app stock-analysis-project:dev ruff check . + docker run --rm -v $(PWD):/app stockdataview:dev ruff check . # Dev: test docker-test: docker-build - docker run --rm stock-analysis-project:dev pytest --verbose + docker run --rm stockdataview:dev pytest -v # Prod: Build the production image (production stage) docker-build-prod: - docker build --target production -t stock-analysis-project:latest . + docker build --target production -t skytics/stockdataview:latest . # Prod: Run the Streamlit app (production image) docker-run: docker-build-prod - docker run --rm -e API_KEY=${API_KEY} -v $(PWD)/data:/app/data -p 8501:8501 stock-analysis-project:latest + docker run --rm -e API_KEY=${API_KEY} -p 8501:8501 skytics/stockdataview:latest # Prod: Tag production image docker-tag: docker-build-prod - docker tag stock-analysis-project skytics/stock-analysis-project:latest + docker tag skytics/stockdataview skytics/stockdataview:latest # Prod: Push prod image to Docker Hub docker-push: docker-tag - docker push skytics/stock-analysis-project:latest + docker push skytics/stockdataview:latest # Delete images docker-clean: - - docker image rm stock-analysis-project:dev || true - - docker image rm stock-analysis-project:latest || true + - docker image rm stockdataview:dev || true + - docker image rm skytics/stockdataview:latest || true + +# CI specific target: Build up to the builder stage and run checks/tests +docker-check-ci: docker-build + docker run --rm -v $(PWD):/app stockdataview:dev uv run ruff check . + docker run --rm -v $(PWD):/app stockdataview:dev uv run pytest -v # All-in-one all: check test docker-build - @echo "All checks passed!" - - - - - + @echo "All checks passed!" \ No newline at end of file diff --git a/README.md b/README.md index dafb407..d85b7f1 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ StockDataView is a Streamlit application designed to display and visualize stock 2. **Build the Docker Image:** ```bash - make docker-build-prod + make docker-build ``` 3. **Run the Docker Container:** @@ -65,12 +65,12 @@ uv sync ## Makefile Commands The Makefile automates common development tasks: -* `make docker-build`: Builds dev image. +* `make docker-build`: Builds the Docker image. * `make docker-check`: Lints code using ruff. * `make docker-test`: Runs tests. -* `make docker-build-prod`: Builds prod image * `make docker-run`: Runs the Docker container. + ## CI/CD (GitHub Actions) The `.github/workflows/ci-cd.yml` file defines the CI/CD workflow, which: From e4c3c46b961acc2356f438000b1a9bc607a38748 Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Sun, 6 Apr 2025 01:32:25 -0600 Subject: [PATCH 17/19] Removed hanging spaces in makefile --- Makefile | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index aa8efbc..1890583 100644 --- a/Makefile +++ b/Makefile @@ -2,36 +2,36 @@ # Dependency management deps: - uv sync + uv sync # Linting and formatting check: - - uv run ruff check . --fix - - uv run ruff format . + - uv run ruff check . --fix + - uv run ruff format . # Testing test: - uv run pytest -v + uv run pytest -v # Dev: Build the dev image (builder stage) docker-build: - docker build --target builder -t stockdataview:dev . + docker build --target builder -t stockdataview:dev . # Dev: ruff check docker-check: docker-build - docker run --rm -v $(PWD):/app stockdataview:dev ruff check . + docker run --rm -v $(PWD):/app stockdataview:dev ruff check . # Dev: test docker-test: docker-build - docker run --rm stockdataview:dev pytest -v + docker run --rm stockdataview:dev pytest -v # Prod: Build the production image (production stage) docker-build-prod: - docker build --target production -t skytics/stockdataview:latest . + docker build --target production -t skytics/stockdataview:latest . # Prod: Run the Streamlit app (production image) docker-run: docker-build-prod - docker run --rm -e API_KEY=${API_KEY} -p 8501:8501 skytics/stockdataview:latest + docker run --rm -e API_KEY=${API_KEY} -p 8501:8501 skytics/stockdataview:latest # Prod: Tag production image docker-tag: docker-build-prod @@ -48,9 +48,9 @@ docker-clean: # CI specific target: Build up to the builder stage and run checks/tests docker-check-ci: docker-build - docker run --rm -v $(PWD):/app stockdataview:dev uv run ruff check . - docker run --rm -v $(PWD):/app stockdataview:dev uv run pytest -v + - docker run --rm -v $(PWD):/app stockdataview:dev uv run ruff check . + - docker run --rm -v $(PWD):/app stockdataview:dev uv run pytest -v # All-in-one all: check test docker-build - @echo "All checks passed!" \ No newline at end of file + @echo "All checks passed!" \ No newline at end of file From 15ed99b61eaa8fd748b08635bce81702a9606575 Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Sun, 6 Apr 2025 14:00:07 -0600 Subject: [PATCH 18/19] Updated README and Makefile --- Makefile | 55 +++++++++++++++++------------------ README.md | 87 +++++++++++++++++++++++++++++++++---------------------- 2 files changed, 78 insertions(+), 64 deletions(-) diff --git a/Makefile b/Makefile index 1890583..e3502ad 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all deps check test docker-build docker-check docker-test docker-build-prod docker-run docker-tag docker-push docker-clean docker-check-ci +.PHONY: all deps check test docker-build-dev docker-check docker-test docker-check-ci docker-build-prod docker-run docker-push docker-clean # Dependency management deps: @@ -13,44 +13,41 @@ check: test: uv run pytest -v -# Dev: Build the dev image (builder stage) -docker-build: +# Dev: Build the builder stage image +docker-build-dev: docker build --target builder -t stockdataview:dev . -# Dev: ruff check -docker-check: docker-build - docker run --rm -v $(PWD):/app stockdataview:dev ruff check . +# Dev: Run linting in container +docker-check: docker-build-dev + docker run --rm -v $(PWD):/app stockdataview:dev uv run ruff check . -# Dev: test -docker-test: docker-build - docker run --rm stockdataview:dev pytest -v +# Dev: Run tests in container +docker-test: docker-build-dev + docker run --rm stockdataview:dev uv run pytest -v -# Prod: Build the production image (production stage) +# CI specific target: Build up to the builder stage and run checks/tests +docker-check-ci: docker-build-dev + - docker run --rm -v $(PWD):/app stockdataview:dev uv run ruff check . + - docker run --rm -v $(PWD):/app stockdataview:dev uv run pytest -v + + +# Prod: Build the production stage image docker-build-prod: docker build --target production -t skytics/stockdataview:latest . -# Prod: Run the Streamlit app (production image) +# Prod: Run the app locally docker-run: docker-build-prod - docker run --rm -e API_KEY=${API_KEY} -p 8501:8501 skytics/stockdataview:latest + docker run --rm -e FMP_API_KEY=${FMP_API_KEY} -p 8501:8501 skytics/stockdataview:latest -# Prod: Tag production image -docker-tag: docker-build-prod - docker tag skytics/stockdataview skytics/stockdataview:latest +# Prod: Push to Docker Hub +docker-push: docker-build-prod + docker push skytics/stockdataview:latest -# Prod: Push prod image to Docker Hub -docker-push: docker-tag - docker push skytics/stockdataview:latest - -# Delete images +# Clean up images docker-clean: - - docker image rm stockdataview:dev || true - - docker image rm skytics/stockdataview:latest || true - -# CI specific target: Build up to the builder stage and run checks/tests -docker-check-ci: docker-build - - docker run --rm -v $(PWD):/app stockdataview:dev uv run ruff check . - - docker run --rm -v $(PWD):/app stockdataview:dev uv run pytest -v + - docker image rm stockdataview:dev || true + - docker image rm skytics/stockdataview:latest || true -# All-in-one -all: check test docker-build +# All-in-one for local dev +all: check test docker-build-dev @echo "All checks passed!" \ No newline at end of file diff --git a/README.md b/README.md index d85b7f1..6ebc6bd 100644 --- a/README.md +++ b/README.md @@ -15,40 +15,56 @@ StockDataView is a Streamlit application designed to display and visualize stock ## Prerequisites -* Docker (for running the application) -* An API key from Financial Modeling Prep (FMP) +- **Docker**: Required to build and run the application container +- **FMP API Key**: Obtain a free or paid API key from [Financial Modeling Prep](https://financialmodelingprep.com/developer/docs/) +- **Git**: To clone the repository (optional) -## Getting Started +## Installation -1. **Clone the Repository:** +### Option 1: Use the Pre-Built Image - ```bash - git clone https://github.com/dimtics/StockDataView.git - cd StockDataView - ``` +1. **Pull the Image from Docker Hub**: + ```bash + docker pull skytics/stockdataview:latest + ``` -2. **Build the Docker Image:** +2. **Run the Container**: + * Replace `your-api-key` with your FMP API key: + ```bash + docker run -p 8501:8501 -e FMP_API_KEY=your-api-key skytics/stockdataview:latest + ``` - ```bash - make docker-build - ``` +3. Open `http://localhost:8501` in your browser -3. **Run the Docker Container:** - ```bash - docker run -p 8501:8501 -e FMP_API_KEY= skytics/stockdataview - ``` +### Option 2: Build and Run Locally - Replace `` with your actual FMP API key. +1. **Clone the Repository**: + ```bash + git clone https://github.com/your-username/StockDataView.git + cd StockDataView + ``` -4. **Access the Application:** +2. **Build the Production Image**: + ```bash + make docker-build-prod + ``` + * This builds only the `production` stage of the multi-stage Dockerfile - Open your web browser and navigate to `http://localhost:8501`. +3. **Run the Container**: + * Replace `your-api-key` with your FMP API key: + ```bash + docker run -p 8501:8501 -e FMP_API_KEY=your-api-key skytics/stockdataview:latest + ``` -5. **Usage** +4. Open `http://localhost:8501` in your browser + + +## Usage * Once the app is running, enter a stock ticker (e.g., AAPL) in the Streamlit interface. * View the displayed bar charts, tables, stock fundamentals, and ratings fetched from the FMP API. + ## Development ### Package Management (uv) @@ -62,21 +78,22 @@ To install dependencies: uv sync ``` -## Makefile Commands -The Makefile automates common development tasks: - -* `make docker-build`: Builds the Docker image. -* `make docker-check`: Lints code using ruff. -* `make docker-test`: Runs tests. -* `make docker-run`: Runs the Docker container. - - -## CI/CD (GitHub Actions) - -The `.github/workflows/ci-cd.yml` file defines the CI/CD workflow, which: - -* Builds the Docker image on push to the `main` branch. -* Pushes the image to Docker Hub. +### Makefile Commands +* `make docker-build-prod`: Build the production image +* `make docker-run`: Run the production image +* `make docker-build-dev`: Build the dev image (for linting/testing) +* `make docker-check`: Run linting in the dev image +* `make docker-test`: Run tests in the dev image +* `make docker-clean`: Remove all images + +### Multi-Stage Dockerfile +* `builder` stage: Used for development (linting, testing) +* `production` stage: Final app image for running StockDataView + +## CI/CD Workflow +* GitHub Actions automates building the `production` image and pushing it to Docker Hub (`skytics/stockdataview:latest`) on `main` branch updates +* The `builder` stage is used for linting and testing in CI +* See `.github/workflows/ci-cd.yml` for details ## Contributing From a526dc44872c65d38ba1a45324812d2c3028938a Mon Sep 17 00:00:00 2001 From: Dimeji Salau Date: Sun, 6 Apr 2025 19:54:35 -0600 Subject: [PATCH 19/19] Refactored the app code --- Dockerfile | 31 +++++++++++----- README.md | 8 ----- src/app.py | 81 ++++++++++++++++++++++++------------------ src/data_validation.py | 66 +++++++++++++++++++--------------- src/utils.py | 36 ++++++++++++++----- 5 files changed, 132 insertions(+), 90 deletions(-) diff --git a/Dockerfile b/Dockerfile index daae632..02b6498 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,38 @@ -# Build stage (with dev tools for linting, testing, etc.) -FROM python:3.11-slim AS builder +# ------------------------------- Builder Satge ------------------------------- + + # Build stage (with dev tools for linting, testing, etc.) +FROM python:3.13-slim-bookworm AS builder + +# The installer requires curl (and certificates) to download the release archive +RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Download the latest installer, install it, and remove it +ADD https://astral.sh/uv/install.sh /uv-installer.sh +RUN chmod -R 655 /uv-installer.sh && /uv-installer.sh && rm /uv-installer.sh + +# Set up uv environment PATH +ENV PATH="/root/.local/bin/:$PATH" + WORKDIR /app # Prevent Python from writing pyc files and buffering output ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 -# Install uv -RUN pip install --no-cache-dir uv - # Copy dependency files first for caching -COPY pyproject.toml uv.lock /app/ +COPY pyproject.toml /app/ # Sync dependencies (includes streamlit, ruff, pytest if in pyproject.toml) -RUN uv sync --frozen +RUN uv sync # Copy the rest of the project COPY . /app +# ------------------------- Production Stage ------------------------- + # Final stage (production, lean image) -FROM python:3.11-slim AS production +FROM python:3.13-slim-bookworm AS production WORKDIR /app # Prevent Python from writing pyc files and buffering output @@ -51,4 +64,4 @@ USER appuser EXPOSE 8501 # Run Streamlit app -CMD ["streamlit", "run", "app.py"] \ No newline at end of file +CMD ["streamlit", "run", "src/app.py"] \ No newline at end of file diff --git a/README.md b/README.md index 6ebc6bd..eb56a12 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # StockDataView - StockDataView is a Streamlit application designed to display and visualize stock data fetched from the Financial Modeling Prep (FMP) API. It provides users with bar charts, tables of stock fundamentals, and ratings for a given stock ticker. ## Features - * **Stock Data Visualization:** Presents stock data in clear and interactive bar charts. * **Fundamental Data Tables:** Displays key stock fundamentals in easy-to-read tables. * **Stock Ratings:** Shows ratings information for selected stocks. @@ -14,7 +12,6 @@ StockDataView is a Streamlit application designed to display and visualize stock * **Makefile Automation:** Includes a Makefile for streamlined development and deployment workflows. ## Prerequisites - - **Docker**: Required to build and run the application container - **FMP API Key**: Obtain a free or paid API key from [Financial Modeling Prep](https://financialmodelingprep.com/developer/docs/) - **Git**: To clone the repository (optional) @@ -22,7 +19,6 @@ StockDataView is a Streamlit application designed to display and visualize stock ## Installation ### Option 1: Use the Pre-Built Image - 1. **Pull the Image from Docker Hub**: ```bash docker pull skytics/stockdataview:latest @@ -38,7 +34,6 @@ StockDataView is a Streamlit application designed to display and visualize stock ### Option 2: Build and Run Locally - 1. **Clone the Repository**: ```bash git clone https://github.com/your-username/StockDataView.git @@ -68,7 +63,6 @@ StockDataView is a Streamlit application designed to display and visualize stock ## Development ### Package Management (uv) - * This project uses `uv` for package management. Dependencies are defined in `pyproject.toml`. * Sync dependencies from `pyproject.toml`. @@ -96,9 +90,7 @@ uv sync * See `.github/workflows/ci-cd.yml` for details ## Contributing - Contributions are welcome! Please feel free to submit a pull request. ## License - This project is licensed under the MIT License - see the `LICENSE` file for details. \ No newline at end of file diff --git a/src/app.py b/src/app.py index 24cb6e7..f79abca 100644 --- a/src/app.py +++ b/src/app.py @@ -94,6 +94,8 @@ def display_stock_metric(header, value) -> None: def display_metric_tables(data: list[dict[str, Any]]): + """Displays metric tables in main area""" + def generate_table_rows(data: dict[str, Any]): # table_name: str rows_html = "" for metric, value in data.items(): @@ -219,6 +221,8 @@ def display_table(data: dict[str, Any], header_name: str): def display_metrics_charts( metrics_data: list[dict[str, Any]], key_metrics_ttm_data: dict[str, Any] ): + """Displays metrics charts in main area""" + def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value): fig = go.Figure() @@ -230,7 +234,7 @@ def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value) name="Historical", marker=dict( color="#4C78A8", # Modern blue - line=dict(width=1.5, color="#2E2E2E"), # Dark outline + line=dict(width=1, color="#2E2E2E"), # Dark outline ), width=0.75, opacity=0.9, @@ -246,7 +250,7 @@ def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value) y=[ttm_value], mode="markers+text", marker=dict( - size=14, + size=12, color="#F28C38", # Vibrant orange symbol="diamond", line=dict(width=2, color="#D76F1E"), # Darker orange outline @@ -255,7 +259,7 @@ def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value) text=[f"{ttm_value:.2f}"], textposition="top center", textfont=dict( - size=13, color="#2E2E2E", weight="bold" + size=12, color="#2E2E2E", weight="bold" ), # Dark gray for visibility hovertemplate="TTM: %{y:.2f}", ) @@ -270,7 +274,7 @@ def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value) y1=ttm_value, line=dict( color="#F28C38", - width=2, + width=1.5, dash="dash", ), ) @@ -317,7 +321,7 @@ def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value) ticktext=data[x_col].dt.strftime("%Y"), gridcolor="rgba(0,0,0,0.2)", # Darker gridlines for white background linecolor="#666666", - linewidth=1.5, + linewidth=1, ticks="outside", tickfont=dict(size=12), title_font=dict(size=14), @@ -327,7 +331,7 @@ def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value) fig.update_yaxes( gridcolor="rgba(0,0,0,0.2)", # Darker gridlines linecolor="#666666", - linewidth=1.5, + linewidth=1, tickfont=dict(size=12), title_font=dict(size=14), zeroline=False, @@ -345,7 +349,7 @@ def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value) ) return fig - # Create charts + # Create dataframe to use for charts df = ( pl.DataFrame(metrics_data) .with_columns( @@ -362,6 +366,7 @@ def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title, ttm_value) return None def plot_chart(metrics: list[tuple[str, str]]): + """Plots charts for each metric in the list""" for metric, title in metrics: ttm_value = key_metrics_ttm_data[f"{metric}_ttm"] dfx = df.select("FYDateEnding", f"{metric}") @@ -414,6 +419,8 @@ def plot_chart(metrics: list[tuple[str, str]]): def display_growth_charts(growth_data: list[dict[str, Any]]): + """Displays growth charts in main area""" + def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title): fig = go.Figure() @@ -424,7 +431,7 @@ def create_compact_bar_chart(data: pl.DataFrame, x_col, y_col, title): name="", marker=dict( color="#54A24B", # Modern green - line=dict(width=1.5, color="#2E2E2E"), # Dark outline + line=dict(width=1, color="#2E2E2E"), # Dark outline ), width=0.7, hovertemplate="%{x}: %{y:.2f}%", @@ -526,7 +533,7 @@ def plot_chart(metrics: list[tuple[str, str]]): col1, col2 = st.columns(2) col3, col4 = st.columns(2) - col5, col6 = st.columns(2) + col5, _ = st.columns(2) with col1: col1_metrics = [ @@ -553,19 +560,18 @@ def plot_chart(metrics: list[tuple[str, str]]): ("debt_growth", "Debt Growth"), ] plot_chart(col5_metrics) - # with col6: - # col6_metrics = [("fcf_yield", "FCF Yield"),] - # plot_chart(col6_metrics) def main(): - # Main UI + """Main function to run StockDataView app""" + + # Set main page config st.set_page_config( - page_title="Stock Valuation Dashboard", + page_title="Stock Data View", layout="wide", ) - # Custom CSS + # Custom CSS for styling main page st.markdown( """ - """, + """, unsafe_allow_html=True, ) + # Set main page title and description st.title("StockDataView") - # st.divider() st.subheader( "Visualize stock fundamentals, ratings, and historical data using the Financial Modeling Prep (FMP) API", divider="gray", ) - st.markdown('
', unsafe_allow_html=True) - # Sidebar + # Sidebar for user input ticker = st.sidebar.text_input(r"$\textsf{\Large Enter stock symbol:}$") analyze_button = st.sidebar.button("Analyze") if ticker and analyze_button: - # Get data + # Get stock ticker data stock_data = get_validated_stock_data(ticker) - profile_data = stock_data["profile"][0] - quote_data = stock_data["quote"][0] - ratings_data = stock_data["ratings"] - key_metrics_ttm_data = stock_data["key_metrics_ttm"][0] - key_metrics_data = stock_data["key_metrics"] - growth_data = stock_data["growth"] - - table_data = [ - quote_data, - key_metrics_ttm_data, - growth_data, - ratings_data, - ] + if stock_data is None: + st.write(f"No data found for stock ticker: {ticker}") + else: + profile_data = stock_data["profile"][0] + quote_data = stock_data["quote"][0] + ratings_data = stock_data["ratings"] + key_metrics_ttm_data = stock_data["key_metrics_ttm"][0] + key_metrics_data = stock_data["key_metrics"] + growth_data = stock_data["growth"] + + table_data = [ + quote_data, + key_metrics_ttm_data, + growth_data, + ratings_data, + ] - if stock_data: # Display ticker profile display_profile(profile_data) # Display ticker quotes - # st.markdown("#### Key Metrics") display_quotes(quote_data, profile_data) st.markdown('
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) + # Display metric tables display_metric_tables(table_data) + # Define layout for metrics and growth charts st.markdown('
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) @@ -643,12 +652,14 @@ def main(): """

Valuation Metrics

""", unsafe_allow_html=True, ) + # Display valuation charts display_metrics_charts(key_metrics_data, key_metrics_ttm_data) with right: st.markdown( """

Growth Metrics

""", unsafe_allow_html=True, ) + # Display growth charts display_growth_charts(growth_data) st.divider() diff --git a/src/data_validation.py b/src/data_validation.py index 1012570..6c26fb9 100644 --- a/src/data_validation.py +++ b/src/data_validation.py @@ -1,39 +1,44 @@ # data_validation.py +import asyncio +from typing import Any, Optional + from pydantic import ValidationError -from typing import Any + from stock_models import CombinedModel -from utils import StockData, FMPClient -import asyncio +from utils import FMPClient, StockData -async def extract_stock_data(ticker: str) -> StockData: +async def extract_stock_data(ticker: str) -> Optional[StockData]: """Pulls source data from Financial Modeling Prep (FMP) API endpoints for a given ticker""" # Get stock data stock_data = await FMPClient().fetch_data(ticker) - # Get metric types - metric_types = FMPClient().metric_types + if stock_data: + # Get metric types + metric_types = FMPClient().metric_types - # Rename some metric types to match with fields defined in the validation - rename_metric_types: dict[Any, Any] = { - "rating": "ratings", - "key-metrics": "key_metrics", - "key-metrics-ttm": "key_metrics_ttm", - "financial-growth": "growth", - } - new_metric_types = [rename_metric_types.get(item, item) for item in metric_types] + # Rename some metric types to match with fields defined in the validation + rename_metric_types: dict[Any, Any] = { + "rating": "ratings", + "key-metrics": "key_metrics", + "key-metrics-ttm": "key_metrics_ttm", + "financial-growth": "growth", + } + new_metric_types = [ + rename_metric_types.get(item, item) for item in metric_types + ] - # Create combined records dict - records = dict(zip(new_metric_types, stock_data)) - return records + # Create combined records dict + records = dict(zip(new_metric_types, stock_data)) + return records class DataValidationError(Exception): """Custom exception for validation error""" -def get_validated_stock_data(ticker: str) -> StockData: +def get_validated_stock_data(ticker: str) -> Optional[StockData]: """Validates stock data against the CombinedModel schema""" # Keep validation errors here @@ -42,14 +47,17 @@ def get_validated_stock_data(ticker: str) -> StockData: # Extract stock data data = asyncio.run(extract_stock_data(ticker)) - # Validate stock data - try: - validated_data = CombinedModel(**data).model_dump() - except ValidationError as e: - errors.append(f"Failed validation: {str(e)}") - if errors: - error_message = "\n".join(errors) - raise DataValidationError( - f"Data validation failed with following errors: \n{error_message}" - ) - return validated_data + if data is None: + return None + else: + # Validate stock data + try: + validated_data = CombinedModel(**data).model_dump() + except ValidationError as e: + errors.append(f"Failed validation: {str(e)}") + if errors: + error_message = "\n".join(errors) + raise DataValidationError( + f"Data validation failed with following errors: \n{error_message}" + ) + return validated_data diff --git a/src/utils.py b/src/utils.py index af3aa11..b504fe7 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,9 +1,11 @@ -import sys -import logging -from typing import Any import asyncio +import logging +import sys from dataclasses import dataclass, field +from typing import Any, Optional + import httpx + from config import settings # Define stock data type @@ -39,13 +41,19 @@ class FMPClient: ] ) # - async def get_data(self, client: httpx.Client, url: str) -> dict[str, Any]: + async def get_data( + self, client: httpx.Client, url: str + ) -> Optional[dict[str, Any]]: """Call API endpoint asynchronously""" - response = await client.get(url) - data = response.json() - return data + try: + response = await client.get(url) + data = response.json() + return data + except Exception as e: + stock_logger().error(f"Error fetching data from {url}: {e}") + return None - async def fetch_data(self, ticker: str) -> dict[str, list[dict[str, Any]]]: + async def fetch_data(self, ticker: str) -> Optional[StockData]: """Extracts data asynchronously from multiple FMP endpoints""" urls = [] for metric in self.metric_types: @@ -60,4 +68,14 @@ async def fetch_data(self, ticker: str) -> dict[str, list[dict[str, Any]]]: for url in urls: tasks.append(asyncio.create_task(self.get_data(client, url))) results = await asyncio.gather(*tasks) - return results + + # Check if results are empty or contain error messages + if isinstance(results[0], list) and len(results[0]) == 0: + return None + elif ( + isinstance(results[0], dict) + and list(results[0].keys())[0] == "Error Message" + ): + return None + else: + return results